3、权重轮询算法:加权轮询(WRR)原理、平滑加权轮询(SWRR)实现、Nginx中的SWRR源码分析

3.1 为什么需要权重轮询?

先聊个实际场景。我在做支付网关的时候,后端挂了3台服务器。一台是32核的物理机,两台是8核的虚拟机。你想想看,如果我用普通的轮询,每台机器分到的请求一样多,那32核的机器岂不是在「摸鱼」?

这就是权重轮询的用武之地。说白了,就是给每台机器分配一个权重值,权重高的多干活,权重低的少干活。比如32核的机器权重设成4,8核的机器权重设成1,这样请求分配比例就是4:1:1。

核心思想: 权重轮询不是简单的「轮流」,而是「按劳分配」。权重越高,被选中的概率越大。

3.2 加权轮询(WRR)的基本原理

加权轮询的实现方式有好几种。我最早接触的是「按权重比例分配」的方式。举个例子:

服务器A:权重 5
服务器B:权重 3
服务器C:权重 2

总权重 = 5 + 3 + 2 = 10

轮询序列:A A A A A B B B C C

这种实现很简单,但有个明显的问题——请求会「扎堆」。你看上面的序列,A连续被选中5次,B连续3次,C连续2次。如果A刚好在高峰期挂了,那连续5个请求都会失败。

我在项目中就踩过这个坑。有一次线上告警,发现某台机器负载突然飙升,查了半天才发现是权重轮询导致的「请求洪峰」。嗯,从那以后我再也不敢用这种朴素的WRR了。

3.3 平滑加权轮询(SWRR)的实现

平滑加权轮询(Smooth Weighted Round Robin)就是为了解决上面那个问题而生的。它的核心思想是:让请求分布得更均匀,避免某台机器被连续命中。

SWRR的算法逻辑是这样的:

  1. 每个节点维护两个值:当前权重(current_weight)有效权重(effective_weight)
  2. 每次调度时,所有节点的当前权重都加上自己的有效权重
  3. 选出当前权重最大的节点
  4. 被选中的节点,当前权重减去总权重

听起来有点绕?我画个图你就明白了。

SWRR算法执行流程(权重 5:3:2) 初始状态: A: cur=0, eff=5 B: cur=0, eff=3 C: cur=0, eff=2 第1轮: A: cur=5, eff=5 ← 选中 B: cur=3, eff=3 C: cur=2, eff=2 第2轮: A: cur=0+5=5 B: cur=3+3=6 ← 选中 C: cur=2+2=4 第3轮: A: cur=5+5=10 ← 选中 B: cur=-4+3=-1 C: cur=4+2=6

你看,经过SWRR处理后,请求序列变成了 A, B, A, C, A, B, A, C, A, B。分布均匀多了吧?

我的经验: SWRR的妙处在于,它用「减法」来平衡权重。每次选中后减去总权重,相当于给其他节点「让路」。这样就不会出现某台机器被连续命中的情况。

3.4 Nginx中的SWRR源码分析

Nginx的upstream模块里,就用了SWRR算法。我直接带你看看核心代码:

// nginx/src/http/ngx_http_upstream_round_robin.c

ngx_uint_t
ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp)
{
    ngx_uint_t                    i, n;
    ngx_http_upstream_rr_peer_t  *peer;
    
    peer = &rrp->peers->peer[0];
    n = rrp->peers->number;
    
    // 关键逻辑:找到当前权重最大的节点
    for ( ;; ) {
        // 遍历所有节点,累加有效权重
        for (i = 0; i < n; i++) {
            peer[i].current_weight += peer[i].effective_weight;
            
            // 累加总有效权重
            total += peer[i].effective_weight;
            
            // 更新有效权重(如果节点异常,会降低权重)
            if (peer[i].effective_weight < peer[i].weight) {
                peer[i].effective_weight++;
            }
        }
        
        // 找到当前权重最大的节点
        best = NULL;
        for (i = 0; i < n; i++) {
            if (best == NULL || 
                peer[i].current_weight > best->current_weight) {
                best = &peer[i];
            }
        }
        
        if (best == NULL) {
            return NGX_BUSY;
        }
        
        // 选中节点,减去总权重
        best->current_weight -= total;
        
        // 检查节点是否可用
        if (best->fails <= best->max_fails) {
            return best->index;
        }
        
        // 节点不可用,继续下一轮
    }
}

这段代码有几个关键点:

  • current_weight:每次调度都会累加,选中后减去总权重
  • effective_weight:初始等于weight,但会根据节点健康状态动态调整
  • fails/max_fails:Nginx的熔断机制,失败次数超限就暂时不选这个节点
注意: Nginx的SWRR实现里,effective_weight是会变化的。如果某个节点连续失败,它的effective_weight会逐渐降低,直到恢复。这个设计很巧妙——既保证了负载均衡,又实现了故障隔离。

3.5 实战中的坑与优化

我在支付系统里用SWRR时,遇到过几个问题:

问题 现象 解决方案
权重突变 某台机器权重从10降到1,导致请求分布剧烈波动 使用「渐进式权重调整」,每次只调整20%
节点恢复慢 故障节点恢复后,effective_weight需要很长时间才能恢复到正常值 增加「快速恢复」机制,连续成功3次就恢复权重
权重精度 权重值很大时(如1000),计算开销增加 限制权重范围在1-100之间
我的建议: 如果你在实现自己的SWRR,记得加上「权重衰减」和「快速恢复」两个机制。前者防止权重突变,后者让故障节点能快速回归。我在支付网关里就是这么做的,效果很好。

3.6 总结

SWRR的核心就三句话:

  1. 累加权重:每次调度,所有节点都加上自己的有效权重
  2. 选最大值:谁当前权重大,谁就干活
  3. 减总权重:干完活的节点,减去总权重,给其他人机会

这个算法看起来简单,但实际用起来坑不少。我建议你在实现时,先写单元测试,把各种边界情况都覆盖到。比如所有节点权重相同、某个节点权重为0、节点数量变化等场景。

好了,SWRR就聊到这儿。记住一句话:好的负载均衡算法,不是让所有机器都忙,而是让每台机器都忙得刚刚好。


公众号:蓝海资料掘金营,微信deep3321