多节点访问轮询算法:从基础到实战
前言
当你的服务从单节点变成多节点,第一个要解决的问题就是:请求该发给谁?
轮询(Round Robin)是最直觉的方案——挨个发,循环往复。但真实场景远比"挨个发"复杂:
- 节点性能不同,不能等量分配
- 节点会上下线,不能硬循环
- 某些请求有状态,不能随机跳
本文从基础轮询讲到加权轮询、平滑加权轮询,最后给出实战选型建议。
一、基础轮询(Round Robin)
原理
维护一个指针,每次请求指向下一个节点,循环往复。
请求1 → 节点A 请求2 → 节点B 请求3 → 节点C 请求4 → 节点A (回到起点)代码实现
classRoundRobin:def__init__(self,nodes):self.nodes=nodes self.index=0defget_node(self):node=self.nodes[self.index]self.index=(self.index+1)%len(self.nodes)returnnode# 使用rr=RoundRobin(['A','B','C'])for_inrange(6):print(rr.get_node())# A B C A B C优点
- 简单,无状态
- 请求均匀分配
缺点
- 不考虑节点权重:3台8核机器和1台2核机器被同等对待
- 节点故障时仍会分配:需要额外的健康检查机制
二、加权轮询(Weighted Round Robin, WRR)
原理
给每个节点设置权重,权重高的节点被选中的概率更大。
假设节点权重:A=5, B=3, C=2
分配序列:A A A A A B B B C C → 循环 请求1 → A 请求2 → A 请求3 → B 请求4 → C 请求5 → A ...代码实现(平滑加权轮询)
classWeightedRoundRobin:def__init__(self,nodes,weights):self.nodes=nodes self.weights=weights self.current_weights=[0]*len(nodes)self.total_weight=sum(weights)defget_node(self):# 每次选择 current_weight + weight_i 最大的节点max_weight=-1selected=0fori,nodeinenumerate(self.nodes):self.current_weights[i]+=self.weights[i]ifself.current_weights[i]>max_weight:max_weight=self.current_weights[i]selected=i# 选中后,减去总权重,保证下一轮公平self.current_weights[selected]-=self.total_weightreturnself.nodes[selected]# 使用wrr=WeightedRoundRobin(['A','B','C'],[5,3,2])for_inrange(10):print(wrr.get_node())# A A A A A B B B C C (近似)优点
- 考虑节点性能差异
- 长期来看分配比例符合权重
缺点
- 短期内可能不均匀(前几个请求可能全是A)
- 节点上下线需要重新计算权重
三、平滑加权轮询(Smooth WRR)
原理
WRR的问题在于权重大的节点可能连续被选中。平滑加权轮询(也叫Nginx默认算法)通过动态调整当前权重来避免连续命中。
核心思路:
- 每个节点有一个
current_weight,初始为0 - 每次选择时:
current_weight += weight - 选出
current_weight最大的节点 - 选中后:
current_weight -= total_weight
这样权重大的节点虽然更容易被选中,但不会连续命中。
效果对比
| 算法 | 请求序列(A:5, B:3, C:2) |
|---|---|
| 基础轮询 | A B C A B C A B C A |
| 加权轮询 | A A A A A B B B C C |
| 平滑加权 | A B A C A B A C A B |
平滑加权的请求分布更均匀,不会出现连续5个A的情况。
四、一致性哈希轮询(Consistent Hashing)
严格来说不是轮询,但常与轮询一起对比,因为它解决了节点增删时的重新分配问题。
原理
将节点和请求都映射到一个哈希环上,请求顺时针找到最近的节点。
请求3 ↓ [节点A]--------[节点B] ↑ ↑ 请求1 请求2优点
- 节点增减时,只有相邻节点受影响
- 适合有状态的场景(如session sticky)
缺点
- 节点权重不好处理(需要虚拟节点)
- 实现比轮询复杂
五、随机轮询(Random + Weight)
原理
不按顺序,按权重概率随机选。
importrandomclassRandomWeighted:def__init__(self,nodes,weights):self.nodes=nodes self.weights=weights self.total=sum(weights)# 构建累积权重区间self.ranges=[]cum=0forwinweights:cum+=w self.ranges.append(cum)defget_node(self):r=random.uniform(0,self.total)fori,limitinenumerate(self.ranges):ifr<=limit:returnself.nodes[i]# 使用rw=RandomWeighted(['A','B','C'],[5,3,2])对比
| 算法 | 均匀性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 基础轮询 | ✅ 长期均匀 | ⭐⭐⭐ | ⭐ | 无状态、节点等价 |
| 加权轮询 | ⚠️ 短期不均 | ⭐⭐⭐ | ⭐⭐ | 节点性能差异大 |
| 平滑加权 | ✅ 短期也较均匀 | ⭐⭐ | ⭐⭐⭐ | 生产环境首选 |
| 一致性哈希 | ✅ | ⭐⭐ | ⭐⭐⭐⭐ | 有状态、频繁扩缩容 |
| 随机加权 | ✅ 概率均匀 | ⭐⭐⭐ | ⭐⭐ | 请求量大、可接受随机性 |
六、实战选型建议
| 你的场景 | 推荐算法 |
|---|---|
| 节点性能相同,无状态 | 基础轮询 |
| 节点性能不同(如8核 vs 2核) | 平滑加权轮询 |
| 节点频繁上下线(K8s Pod) | 一致性哈希 |
| 有状态服务(sticky session) | 一致性哈希 |
| 流量极大,可接受随机性 | 随机加权 |
Nginx默认用的就是平滑加权轮询
upstream backend { server 192.168.1.1:8080 weight=5; server 192.168.1.2:8080 weight=3; server 192.168.1.3:8080 weight=2; }总结
轮询算法的演进本质上是在解决三个问题:
- 公平→ 基础轮询解决
- 性能差异→ 加权轮询解决
- 短期均匀 + 节点动态变化→ 平滑加权 / 一致性哈希解决
没有最好的算法,只有最适合你场景的算法。大多数情况下,平滑加权轮询是生产环境的最优解。
