轮询算法:从普通轮询到平滑加权轮询
轮询算法,说白了就是「大家轮流来」。
在负载均衡里,它是最朴素的分配策略。请求排成队,服务器排成圈,一个挨一个分下去。我早期做网关选型时,第一个接触的就是它——简单,但坑也不少。
这一章我们拆开揉碎了讲。从普通轮询讲到加权轮询,再讲到平滑加权轮询。你跟着走一遍,不仅能写代码,还能明白为什么 Nginx 选了最后那种。
普通轮询:最原始的「转圈圈」
普通轮询的逻辑,你闭上眼睛都能想出来。
服务器列表排好队,请求来了按顺序发。发完一圈回到开头,继续循环。实现就是一个计数器取模:
// 普通轮询 —— 核心就这一行
int index = counter.getAndIncrement() % serverList.size();
return serverList.get(index);
我早期做一个内部管理系统时就用的这个。三台服务器配置一模一样,跑起来挺稳。但后来加了一台新机器,配置是老机器的两倍——问题来了。
普通轮询不管服务器性能差异,每台分到的请求数一样。如果某台机器慢,就会拖慢整体响应。你想想看,一个请求队列里,慢机器处理一个请求的时间,快机器能处理两个。结果就是快机器闲着等慢机器。
所以普通轮询的适用场景很窄:所有服务器规格完全相同,且处理能力稳定。但凡有点差异,它就不太行了。
核心结论:普通轮询适合同构集群,简单但「看不见」服务器差异。
加权轮询:给服务器贴上「能力标签」
加权轮询就是为了解决上面那个问题。给每台服务器配一个权重,性能好的权重高,分到的请求就多。
实现思路也不复杂。把权重展开成虚拟节点列表,再轮询:
// 假设服务器A权重3,服务器B权重1,服务器C权重1
// 虚拟列表 = [A, A, A, B, C]
// 然后在这个列表上做普通轮询
这样A拿到的请求是B和C的三倍。听起来很合理对吧?
但有个隐藏问题。你注意看上面的虚拟列表——A连续出现了三次。这意味着在真实流量中,A服务器会连续收到三个请求,然后才轮到B和C。
这就是请求倾斜。短时间内大量请求涌向同一台机器,虽然总量是对的,但流量不平滑。我在一个电商项目中就踩过这个坑——权重配了5:1,结果高权重机器在高峰时段的CPU直接飙到90%,低权重机器却只有30%。
为什么?因为连续的请求突发,把高权重机器的连接池打满了。
注意:加权轮询虽然支持了异构服务器,但「连续分配」的特性可能导致局部流量倾斜。权重差距越大,倾斜越明显。
平滑加权轮询:Nginx 的优雅解法
这个算法是 Nginx upstream 模块默认使用的轮询方式。我第一次看到它的实现时,觉得真是巧妙——用很小的计算代价,换来了非常平滑的分配效果。
算法核心就三个变量:
- weight:固定权重,配好后不变
- current_weight:当前权重,动态变化
- total_weight:所有服务器 weight 之和
每次选一台服务器的过程分三步:
- 每台服务器的 current_weight 加上自己的 weight
- 选出 current_weight 最大的那台
- 被选中的服务器 current_weight 减去 total_weight
光看文字有点绕,我们直接上代码:
class SmoothServer {
String name;
int weight; // 固定权重
int currentWeight; // 当前权重
public SmoothServer(String name, int weight) {
this.name = name;
this.weight = weight;
this.currentWeight = 0;
}
}
public SmoothServer select(List<SmoothServer> servers) {
int totalWeight = 0;
SmoothServer best = null;
for (SmoothServer s : servers) {
s.currentWeight += s.weight;
totalWeight += s.weight;
if (best == null || s.currentWeight > best.currentWeight) {
best = s;
}
}
best.currentWeight -= totalWeight;
return best;
}
这段代码如果你第一次看,可能会觉得「为什么 currentWeight 要减 totalWeight」?
我举个例子你就明白了。假设两台服务器,权重比是 3:1。手动跑几轮:
| 轮次 | A 的 currentWeight | B 的 currentWeight | 选中结果 |
|---|---|---|---|
| 初始 | 0 | 0 | — |
| 第1轮 | +3 → 3 | +1 → 1 | A(3-4=-1) |
| 第2轮 | -1+3 → 2 | 1+1 → 2 | A(2-4=-2) |
| 第3轮 | -2+3 → 1 | 2+1 → 3 | B(3-4=-1) |
| 第4轮 | 1+3 → 4 | -1+1 → 0 | A(4-4=0) |
看到没?分配序列是 A, A, B, A。没有连续的三个A,而是穿插着来。整个分配过程非常平滑。
我个人比较推崇这个算法。它没有增加多少计算量,却完美解决了加权轮询的请求倾斜问题。Nginx 选它作为默认算法,不是没有道理的。
小提示:平滑加权轮询的数学本质,是在保证权重比例的前提下,让高权重服务器的请求在时间轴上尽量「分散」开。
三种算法的对比与选型
我把它们的核心差异整理成了一张表。你实际选型时对着看就行:
| 维度 | 普通轮询 | 加权轮询 | 平滑加权轮询 |
|---|---|---|---|
| 分配策略 | 完全平均 | 按权重比例 | 按权重比例 + 平滑 |
| 适用集群 | 同构 | 异构 | 异构 |
| 请求倾斜风险 | 无 | 有(权重差距大时明显) | 无 |
| 实现复杂度 | 低 | 中 | 中 |
| 典型代表 | 早期 Nginx 默认 | LVS 加权调度 | Nginx upstream |
选型其实很简单:
- 服务器都一样 → 普通轮询,够用
- 服务器有差异,流量不大 → 加权轮询,简单
- 服务器有差异,流量大/要求稳定 → 平滑加权轮询,首选
避坑指南
讲几个我实际踩过的坑,你遇到了能少走弯路。
坑一:权重配比与流量的非线性关系
我曾经把一台机器的权重配成10,另一台配成1。心想10倍流量没问题。结果高权重机器先扛不住了。为什么?因为权重10意味着它经常被连续选中,连接池瞬间被打满。
后来我换成了平滑加权轮询,同样10:1的权重,分配变得均匀,高权重机器的CPU峰值降了40%。
坑二:动态权重调整时的「抖动」
有些场景需要根据服务器负载动态调整权重。如果你在平滑加权轮询中直接改 weight 值,会导致 current_weight 和新的 weight 不匹配,出现短暂的分配抖动。
我建议的做法是:先停止当前节点的流量,等 current_weight 归零后再调整权重,然后重新上线。
坑三:服务器宕机后的权重残留
如果一台服务器宕机了,但权重信息没清理,current_weight 还保留着。等它恢复上线时,可能因为累积的 current_weight 过高,刚上线就被「猛灌」一波请求。
解决办法:服务器重新上线时,把它的 current_weight 重置为0。
一句话总结:普通轮询看均等,加权轮询看比例,平滑加权轮询看节奏。理解了这三个层次的演进,你就能在项目中选对算法、用好算法。