多节点访问轮询算法:从基础到实战

多节点访问轮询算法:从基础到实战

前言

当你的服务从单节点变成多节点,第一个要解决的问题就是:请求该发给谁?

轮询(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默认算法)通过动态调整当前权重来避免连续命中。

核心思路:

  1. 每个节点有一个current_weight,初始为0
  2. 每次选择时:current_weight += weight
  3. 选出current_weight最大的节点
  4. 选中后: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; }

总结

轮询算法的演进本质上是在解决三个问题:

  1. 公平→ 基础轮询解决
  2. 性能差异→ 加权轮询解决
  3. 短期均匀 + 节点动态变化→ 平滑加权 / 一致性哈希解决

没有最好的算法,只有最适合你场景的算法。大多数情况下,平滑加权轮询是生产环境的最优解