多节点访问轮询算法:从基础到实战
前言当你的服务从单节点变成多节点第一个要解决的问题就是请求该发给谁轮询Round Robin是最直觉的方案——挨个发循环往复。但真实场景远比挨个发复杂节点性能不同不能等量分配节点会上下线不能硬循环某些请求有状态不能随机跳本文从基础轮询讲到加权轮询、平滑加权轮询最后给出实战选型建议。一、基础轮询Round Robin原理维护一个指针每次请求指向下一个节点循环往复。请求1 → 节点A 请求2 → 节点B 请求3 → 节点C 请求4 → 节点A 回到起点代码实现classRoundRobin:def__init__(self,nodes):self.nodesnodes self.index0defget_node(self):nodeself.nodes[self.index]self.index(self.index1)%len(self.nodes)returnnode# 使用rrRoundRobin([A,B,C])for_inrange(6):print(rr.get_node())# A B C A B C优点简单无状态请求均匀分配缺点不考虑节点权重3台8核机器和1台2核机器被同等对待节点故障时仍会分配需要额外的健康检查机制二、加权轮询Weighted Round Robin, WRR原理给每个节点设置权重权重高的节点被选中的概率更大。假设节点权重A5, B3, C2分配序列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.nodesnodes self.weightsweights self.current_weights[0]*len(nodes)self.total_weightsum(weights)defget_node(self):# 每次选择 current_weight weight_i 最大的节点max_weight-1selected0fori,nodeinenumerate(self.nodes):self.current_weights[i]self.weights[i]ifself.current_weights[i]max_weight:max_weightself.current_weights[i]selectedi# 选中后减去总权重保证下一轮公平self.current_weights[selected]-self.total_weightreturnself.nodes[selected]# 使用wrrWeightedRoundRobin([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.nodesnodes self.weightsweights self.totalsum(weights)# 构建累积权重区间self.ranges[]cum0forwinweights:cumw self.ranges.append(cum)defget_node(self):rrandom.uniform(0,self.total)fori,limitinenumerate(self.ranges):ifrlimit:returnself.nodes[i]# 使用rwRandomWeighted([A,B,C],[5,3,2])对比算法均匀性性能复杂度适用场景基础轮询✅ 长期均匀⭐⭐⭐⭐无状态、节点等价加权轮询⚠️ 短期不均⭐⭐⭐⭐⭐节点性能差异大平滑加权✅ 短期也较均匀⭐⭐⭐⭐⭐生产环境首选一致性哈希✅⭐⭐⭐⭐⭐⭐有状态、频繁扩缩容随机加权✅ 概率均匀⭐⭐⭐⭐⭐请求量大、可接受随机性六、实战选型建议你的场景推荐算法节点性能相同无状态基础轮询节点性能不同如8核 vs 2核平滑加权轮询节点频繁上下线K8s Pod一致性哈希有状态服务sticky session一致性哈希流量极大可接受随机性随机加权Nginx默认用的就是平滑加权轮询upstream backend { server 192.168.1.1:8080 weight5; server 192.168.1.2:8080 weight3; server 192.168.1.3:8080 weight2; }总结轮询算法的演进本质上是在解决三个问题公平→ 基础轮询解决性能差异→ 加权轮询解决短期均匀 节点动态变化→ 平滑加权 / 一致性哈希解决没有最好的算法只有最适合你场景的算法。大多数情况下平滑加权轮询是生产环境的最优解。