Go语言无开箱即用WeightedRoundRobin实现,关键在权重真实生效、并发安全与状态不漂移;直接用rand.Intn()模拟会因非并发安全、权重变更不同步、整数截断导致失效。

Go 语言里没有开箱即用的 WeightedRoundRobin 标准库实现,自己写也不必从零造轮子——关键不是“能不能跑通”,而是“权重是否真实生效、并发是否安全、动态调权时状态是否漂移”。
为什么直接用 rand.Intn() 模拟加权轮询会失效
常见错误是把权重数组归一化后用随机数落点区间,比如权重 []int{3, 1, 2} → 总和 6 → rand.Intn(6) 再线性扫描。问题在于:
- 没做原子保护:多个 goroutine 并发调用
rand.Intn()可能返回相同值(math/rand默认实例非并发安全) - 权重变更时无状态同步:新权重生效前,已有请求还在按旧权重区间匹配,导致短期比例严重失真
- 小权重节点易被跳过:整数除法截断或浮点误差累积,使权重为 1 的节点长期不被选中
WeightedRoundRobin 必须维护 per-node 状态
核心不是“轮”,而是每个节点维持一个可累积、可扣减的 currentWeight。它必须是整数、初始等于配置权重,并在每次调度后更新。否则无法保证长期概率收敛。
- 权重字段必须是正整数;
0表示临时下线,不能从列表中删除(否则破坏状态连续性) - 不要用浮点归一化,避免误差漂移;推荐整数累加 + 全局取模或最大值比较
- 每次
Select()前需遍历所有节点,用atomic.LoadInt64(&node.current)读当前值,找最大者 - 选中后执行
atomic.AddInt64(&node.current, -1),若归零则用atomic.StoreInt64(&node.current, node.weight)重置
如何让 WRR 支持运行时动态调权
直接赋值 node.Weight = newWeight 是危险的——此时可能有 goroutine 正在读 node.weight 重置 current,造成重置值错误。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:把
weight和current合并进一个int64,高 32 位存 weight,低 32 位存 current,用atomic.CompareAndSwapInt64原子更新 - 或者引入版本号:
version uint64,每次调权atomic.AddUint64(&node.version, 1),重置current前校验版本是否一致 - 切忌在
Select()中做健康检查——那是发现层的事;负载均衡器只按状态转发,isAvailable应由独立 goroutine 维护
生产环境优先考虑 P2C + EWMA 而非纯 WRR
如果你的后端延迟波动大、扩缩容频繁,硬写 WRR 很容易陷入“理论正确、线上抖动”的陷阱。go-zero 的 P2C(Pick Two Choices)+ EWMA 更实用:
- 每次请求随机挑两个节点,比的是它们各自的
EWMA延迟值(α * rtt + (1-α) * old,α通常取0.2~0.5) -
EWMA必须用sync/atomic读写,每个节点独立存储,不能共用变量 - 未被选中的节点也要按衰减因子缓慢下降
EWMA,否则长期不调用的节点指标会严重失真
真正难的从来不是算法本身,而是怎么让权重变化、节点上下线、RTT 波动这三件事互不干扰地收敛——状态管理比数学公式重要得多。


















