平滑加权轮询算法介绍
一个简单的Nginx负载均衡配置。
http {
upstream cluster {
server 192.168.100.1 weight=5; //下面使用a表示
server 192.168.100.2 weight=1; //下面使用b表示
server 192.168.100.3 weight=1; //下面使用c表示
}
server {
listen 80;
location / {
proxy_pass http://cluster;
}
}
}
当在nginx配置文件的upstream配置块中没有指定使用的负载均衡算法时,默认使用的是加权轮询。
按照上述配置,Nginx每收到7个客户端的请求,会把其中的5个转发给后端a,把其中的1个转发给后端b,把其中的1个转发给后端c。这就是所谓的加权轮询,看起来很简单,但是最早使用的加权轮询算法有个问题,就是7个请求对应的后端序列是这样的:{ c, b, a, a, a, a, a },会有5个连续的请求落在后端a上,分布不太均匀。
目前使用的加权轮询叫做平滑的加权轮询(smooth weighted round-robin balancing),它和前者的区别是:
每7个请求对应的后端序列为 { a, a, b, a, c, a, a },转发给后端a的5个请求现在分散开来,不再是连续的。
摘录此算法的描述:
On each peer selection we increase current_weight of each eligible peer by its weight,
select peer with greatest current_weight and reduce its current_weight by total number
of weight points distributed among peers.
To preserve weight reduction in case of failures the effective_weight variable was introduced,
which usually matches peer’s weight, but is reduced temoprarily on peer failures.
算法实现
nginx-1.10.1中的加权轮询算法实现:
static ngx_http_upstream_rr_peer_t *
ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp)
{
time_t now;
uintptr_t m;
ngx_int_t total;
ngx_uint_t i, n, p;
ngx_http_upstream_rr_peer_t *peer, *best;
now = ngx_time();
best = NULL;
total = 0;
#if (NGX_SUPPRESS_WARN)
p = 0;
#endif
for (peer = rrp->peers->peer, i = 0;
peer;
peer = peer->next, i++)
{
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
if (rrp->tried[n] & m) {
continue;
}
if (peer->down) {
continue;
}
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
{
continue;
}
peer->current_weight += peer->effective_weight;
total += peer->effective_weight;
if (peer->effective_weight < peer->weight) {
peer->effective_weight++;
}
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
p = i;
}
}
if (best == NULL) {
return NULL;
}
rrp->current = best;
n = p / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
rrp->tried[n] |= m;
best->current_weight -= total;
if (now - best->checked > best->fail_timeout) {
best->checked = now;
}
return best;
}
每个后端peer都有三个权重变量,先解释下它们的含义。
(1) weight
配置文件中指定的该后端的权重,这个值是固定不变的。
(2) effective_weight
后端的有效权重,初始值为weight。
在释放后端时,如果发现和后端的通信过程中发生了错误,就减小effective_weight。
此后有新的请求过来时,在选取后端的过程中,再逐步增加effective_weight,最终又恢复到weight。
之所以增加这个字段,是为了当后端发生错误时,降低其权重。
(3) current_weight
后端目前的权重,一开始为0,之后会动态调整。动态调整:
每次选取后端时,会遍历集群中所有后端,对于每个后端,current_weight增加effective_weight,
同时累加所有后端的effective_weight,保存为total。
如果该后端的current_weight是最大的,就选定这个后端,然后把它的current_weight减去total。
如果该后端没有被选定,那么current_weight不用减小。
弄清了三个weight字段的含义后,加权轮询算法可描述为:
- 对于每个请求,遍历集群中的所有可用后端,对于每个后端peer执行:
peer->current_weight += peer->effecitve_weight。
同时累加所有peer的effective_weight,保存为total。 - 从集群中选出current_weight最大的peer,作为本次选定的后端。
- 对于本次选定的后端,执行:peer->current_weight -= total。
算法示例
上述描述可能不太直观,来看个例子。
现在使用以下的upstream配置块:
upstream backend { server a weight=4; server b weight=2; server c weight=1; }
按照这个配置,每7个客户端请求中,a会被选中4次、b会被选中2次、c会被选中1次,且分布平滑。
我们来算算看是不是这样子的。
initial current_weight of a, b, c is { 0, 0, 0 }
请求序号 | current_weight before selected | select peer | current_weight after selected |
---|---|---|---|
1 | {4,2,1} | a | {-3,2,1} |
2 | {1,4,2} | b | {1,-3,2} |
3 | {5,-1,3} | a | {-2,-1,3} |
4 | {2,1,4} | c | {2,1,-3} |
5 | {6,3,-2} | a | {-1,3,-2} |
6 | {3,5,-1} | b | {3,-2,-1} |
7 | {7,0,0} | a | {0,0,0} |
通过上述过程,可得以下结论:
- 7个请求中,a、b、c分别被选取了4、2、1次,符合它们的权重值。
- 7个请求中,a、b、c被选取的顺序为a, b, a, c, a, b, a,分布均匀,权重大的后端a没有被连续选取。
- 每经过7个请求后,a、b、c的current_weight又回到初始值{ 0, 0, 0 },因此上述流程是不断循环的。
这个平滑的加权轮询算法背后应该有数学论证,这里就不继续研究了)