目录
- 1. swrr负载均衡算法的二宗罪
- 1.1 第一宗罪: 共振引起系统崩溃
- 1.2 第二宗罪: 吃CPU大户
- 2. 对swrr负载均衡算法的改进的思考
- 2.1 “共振”问题的解决
- 2.2 “吃CPU大户”问题的解决
1. swrr负载均衡算法的二宗罪
swrr是一种基于加权轮询的负载均衡算法。它根据服务器的权重来分配请求。具体而言,swrr维护一个服务器列表,每个服务器都有一个权重值,表示其处理请求的能力。当一个请求到达时,swrr会按照服务器的权重顺序依次将请求转发到服务器上,然后更新服务器的权重。这样可以实现将请求均匀地分配给具有不同处理能力的服务器。关于swrr的实现原理可以参考我的博文《深入理解nginx负载均衡round-robin算法》。
nginx的官方原生的round-robin功能就是基于swrr算法来实现的。就如其名字swrr表明的,Smooth Weighted round-robin,它比最简单的round-robin功能有了重要的改进,它不是简单的依次轮询,而是按照服务器权重平滑分配的能力,一定程度上避免了rs服务器的突发峰值的问题。
本文要细数一下swrr的两宗罪:
1.1 第一宗罪: 共振引起系统崩溃
我们先来看看阿里的真实案例。《阿里七层流量入口负载均衡算法演变之路》中提到的一个压测案例,当一个机房内的接入层tengine统一将某个rs服务器的权重从1改成2以后,被调高权重rs服务器的流量瞬间冲高了50倍左右,并且持续了7s左右的时间后QPS开始下降,流量才逐渐恢复到预期的值。
该文中给出了相关现象的详细描述,但是具体什么原因却没有详细说明。这里就来分析一下。对于swrr,如果按照nginx单个进程来说,其实它已经做得足够好了,顾名思义,它已经是平滑的了。但是,如果我们放在一个拥有32核心,甚至64核心或者更多的服务器上,那么我们开启的nginx worker进程就是32,或者64个,如果再像阿里的压测环境,可能又有几十台作为负载均衡功能的nginx前端接入服务器,那么可能就是并发运行的nginx进程就有几百个甚至上千个。swrr在每个进程中都会被初始化为一个状态,如果有三个rs server分别为A、B、C,并假设通过swrr算法,但个进程依次分配rs得到以下序列 B、C、A、B、A、C…
另外,假设总共有100个nginx worker进程在运行,我们可以想象到,第一个请求进来的时候,因为所有的nginx进程分配的第一个rs必然是B,因此第一个请求分配到的rs是B的概率是100%,那么第二个请求进来的时候,因为有99个进程将分配B,而之前分配了B的服务器将分配C,那么分配到B的概率将是99%,依次类推,第三个请求分配到B的概率也97%,这就导致了B服务器的压力在短时间内会飚得特别高,就像《阿里七层流量入口负载均衡算法演变之路》中给出的那个图像一样:
所以,关键的问题在于每个nginx的worker进程因为每个进程都步调一致,导致了“共振现象”,引起了部分rs服务器的流量突发性增高,和原本的预期背道而驰了。我们回顾一下中学物理上面介绍的一个经典的军队用统一步伐过桥最终引起大桥垮塌的案例就可以与这个案例引起共鸣,一言以蔽之,swrr引起的流量突发也是因为“共振”引起的流量突发。
当然,随着时间的推移,最终每个进程由于分配到的请求的随机性因素,由于swrr的平滑负载均衡能力,必然会慢慢达到预期的负载效果,但是,我们要注意的是,前提是因为流量突发,这些rs服务器都能够扛过流量洪峰的袭击才可以,而往往可能因为流量洪峰过来的时候,部分rs服务器服务器早就挂掉了,导致引起整个系统的雪崩,根本来不及达到最终的预期的负载均衡效果。
1.2 第二宗罪: 吃CPU大户
swrr算法在我们平常的小规模的负载均衡系统中,譬如rs服务器只有几台,最多10几台的情况下,其实是可以运行得很好的,所以我们一般也不会碰到这种问题。但是,放在一个大规模的集群环境中,就像《 QPS 比 Nginx 提升 60%,阿里 Tengine 负载均衡算法揭秘》提到的那样,如果upstream中设置的rs服务器有2000台,swrr的“吃CPU大户”的弊端就会暴露无遗了。我们先来看看swrr算法的代码,以下是我参照nginx的round-robin代码写的一段c代码:
// effective_weight 三台rs服务器,设置了每台rs服务器对应的权重
int ew[] = {1,2,4};
#define PN sizeof(ew)/sizeof(int)
// current_weight 对应三台rs服务器的计算出来的本轮权重
int cw[PN] = {0};
int rr()
{
int i;
int ttl = 0;
int cur_w = -1;
int cur = -1;
for (i=0; i < PN; i++) {
cw[i] += ew[i];
ttl += ew[i];
if (cur_w < cw[i]) {
cur = i;
cur_w = cw[i];
}
}
cw[cur] -= ttl;
return cur;
}
具体算法的逻辑就不再这里介绍了,可以参考《 QPS 比 Nginx 提升 60%,阿里 Tengine 负载均衡算法揭秘》。需要注意的一点是,swrr算法每次进行负载均衡选择一台rs服务器,都需要进行一次完整的遍历,有2000台rs那么就需要循环2000次,这就是问题的所在。就像上面说的那样,对于upstream中rs数量少的情况大家可能感受不到,如果有2000台rs的规模,那大量的cpu资源就白白浪费在循环上了,确实和nginx是从头到尾塑造的“高效率”web服务器的形象大相径庭。
2. 对swrr负载均衡算法的改进的思考
2.1 “共振”问题的解决
在中学物理军队用统一步伐过桥最终引起大桥垮塌的案例中,物理老师告诉我们的解决办法是改齐步走为便步走,便步走就引入了随机性,这样就很好地避免了共振的发生。而我们swrr算法也同样可以通过引入随机性来解决“共振”现象的发生。所以阿里的tengine中引入了vnswrr算法,该算法就是引入了“随机性”,虽然每个nginx进程还是使用swrr算法,但是每个nginx进程引入了一个随机值,在其第一次初始化完首批所谓的“虚拟节点”列表后,每个进程会设置一个随即的起始轮询位置,这样每个进程各自走各自的“便步”,从而避免了“共振”的发生。
emsp;当然,我们如果不考虑解决CPU消耗的问题,我们完全可以不用虚拟节点的办法,在nginx初始化负载均衡算法加载完成peer列表以后,每个进程随机地取一个[0,N)中的值n, N为peer数量,预先执行n遍swrr算法,从而强行使得各个进程的步调不一致,一样可以解决这个问题。
当然,如果我们只有少数几台nginx服务器作为负载均衡服务器,那么我们完全不用那么负载,利用nginx官方原生的ngx_http_upstream_zone_module模块也就能很好地解决问题了。关于ngx_http_upstream_zone_module模块的详细说明可以参考《深入理解nginx upstream共享内存机制》。因为ngx_http_upstream_zone_module将peer的分配状态放在共享内存中,每个worker进行负载均衡的时候,会线性地执行swrr算法,swrr算法的当前状态也会更新的共享内存中,还是拿军队过大桥的例子来说,ngx_http_upstream_zone_module模块会限制每个人一个一个过桥,而不是大家一起过桥,从而避免了"共振"问题的发生。
2.2 “吃CPU大户”问题的解决
解决这个问题最理想的方法就是在负载均衡的时候把循环去掉,把算法的复杂度从O(n)改称O(1)。如果理解了swrr算法,要改起来也不是太复杂,因为swrr算法每Total Weight(即所有服务器分配的权重的总和)次负载均衡请求,就会回到原始状态并重新开始。因此,我们可以提前把分配表建好,后续进行负载均衡的时候,只要对着这个分配表每次往后移动一格就可以了,自然就实现了O(1)的性能。
不过,tengine的vnswrr算法考虑得更加全面一些,因为如果rs服务器数量非常多的情况下,一次性生成这个分配表的计算量也是不少的,计算量是O(n*w)(w为总的权重)的,如果是两前台服务器,计算量也是相当可观的,所以vnswrr就将这个分配表的生成任务化整为零,每次只生成固定的几个,用完再分配,直到整个分配表填充完毕后就循环利用分配表进行执行负载均衡分配rs服务器就可以了,这样就大大降低了CPU使用率的突发问题,提升了系统的抗压能力。
从阿里压测给到的数据来看,用wrk 压测工具、500 并发、长连接场景、upstream 配置 2000 个 server的测试条件下。Nginx 官方的 SWRR 和改进的 VNSWRR 算法下的 QPS 处理能力如下图所示:其中 VNSWRR 的 QPS 处理能力相对于 SWRR 算法提升 60% 左右。
至于vnswrr的详细算法原理,可以参考《阿里七层流量入口负载均衡算法演变之路》](https://www.upyun.com/opentalk/444.html),后续本人也将单独写一篇关于ngx_http_upstream_vnswrr_module模块源码分析的文章,敬请期待。