加权轮询不均匀的根本原因是静态展开丢失全局状态,真正均匀需用平滑加权轮询(swrr):维护currentweight和effectiveweight,每次更新后选最大值并减去总权重,确保长期比例严格收敛。

为什么 round-robin 加权后还是不均匀?
Go 标准库没有内置加权轮询(Weighted Round Robin, WRR),直接按权重重复添加节点(比如权重 3 就塞 3 次进 slice)看似简单,但实际请求分布会严重偏斜——尤其在并发高、后端数量少、权重差异大时。根本原因是:这种“静态展开”方式丢失了请求序列的全局状态,无法动态补偿累积误差。
真正均匀的加权轮询,核心是维护每个节点的「当前权重」和「最大权重」,每轮选出当前权重最大的节点,用完后减去总权重,其他节点则加上自身原始权重。这就是 Nginx 实现的平滑加权轮询(Smooth Weighted Round Robin, SWRR)逻辑。
- 别用
append多次塞同一节点模拟权重——它会导致 burst 请求扎堆到高权节点 - 不要在每次选择时重新计算所有节点的累计权重——性能差且无法平滑
- 注意浮点权重:SWRR 必须用整数权重,否则浮点误差会破坏收敛性
swrr.Select() 怎么写才不丢精度?
关键不是选最大值,而是维护两个字段:currentWeight(当前动态权重)和 effectiveWeight(原始权重)。每次选择前,所有节点的 currentWeight += effectiveWeight;选中后,该节点 currentWeight -= totalEffectiveWeight。这样能保证长期比例严格收敛到权重比。
示例结构体:
type Node struct {
Name string
EffectiveWeight int
CurrentWeight int
}
选择逻辑简写:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
func (s *SWRR) Select() *Node {
for i := range s.nodes {
s.nodes[i].CurrentWeight += s.nodes[i].EffectiveWeight
}
var selected *Node
max := -1
for _, n := range s.nodes {
if n.CurrentWeight > max {
max = n.CurrentWeight
selected = n
}
}
if selected != nil {
selected.CurrentWeight -= s.totalWeight // totalWeight = sum(effectiveWeight)
}
return selected
}
-
totalWeight必须是所有EffectiveWeight的整数和,不能动态重算 - 初始化时所有
CurrentWeight设为 0,不要设为EffectiveWeight——那会干扰首几轮收敛 - 如果节点权重为 0,跳过参与计算,否则
CurrentWeight会持续负增长导致异常
并发下 CurrentWeight 竞态怎么防?
多个 goroutine 同时调用 Select() 会同时读写 CurrentWeight,不加锁必然错乱。但用 sync.Mutex 会成为瓶颈;用 atomic 又没法原子地完成“加权 → 比较 → 减总权”三步。
稳妥做法是:把整个选择逻辑包进一个互斥段,但粒度要细——只锁状态更新部分,不锁节点业务逻辑(如健康检查、网络调用)。
- 别在锁里做 HTTP 请求或 DB 查询——延迟会拖垮吞吐
- 避免在锁内新建对象或触发 GC,尤其是高频调用场景
- 如果后端节点数极少(≤ 5),用
sync/atomic+ CAS 手写无锁循环也可行,但可读性差,调试成本高
和 Nginx 的 upstream 配置对齐要注意什么?
Nginx 的 weight= 参数就是 SWRR 的 EffectiveWeight,但它的内部 current_weight 初始值设为 weight,而 Go 实现通常从 0 开始。这会导致前几轮行为不一致——不过只要运行足够久(> sum(weight) 次请求),两者收敛结果完全相同。
真正影响对齐的是权重归一化:Nginx 不要求权重和为 100 或其他特定值,只看相对比。所以 Go 侧也应直接使用原始整数权重,不要除以 GCD 或缩放到 100。
- 别把
weight=3和weight=6改成1和2——虽然比例一样,但会影响收敛速度和初始抖动幅度 - Nginx 在节点 down 时会临时降低其
current_weight,Go 实现若要做故障剔除,得额外维护isAlive状态,并在Select()中跳过失效节点 - 测试时用固定请求序列(如 100 次循环)对比计数分布,比压测更易发现偏差
平滑加权轮询的难点不在算法本身,而在状态一致性、并发安全和权重语义对齐。一旦某个环节用错整数类型、漏掉初始化、或在锁里干了重活,均匀性就立刻崩掉——而且这种问题往往只在流量高峰或长尾节点故障时才暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










