普通 round-robin 不适合带权重场景,因其均等分配请求,无法反映节点性能差异;加权轮询(尤其平滑加权轮询)通过动态维护 current_weight 实现按权重比例平滑调度,兼顾公平性与内存效率。

为什么普通 round-robin 不适合带权重的场景
普通 round-robin 每次只选一个节点,完全平均分配请求,但真实服务节点性能不同——比如一台 CPU 是另一台的 2 倍,硬平均会导致强节点空转、弱节点过载。权重不是“每 N 次选一次”,而是要让概率分布趋近于权重比例,同时保持请求序列尽可能平滑(避免突发集中打到同一节点)。
用加权轮询(Weighted Round-Robin)算法实现
推荐使用“平滑加权轮询”(Smooth Weighted Round-Robin),它比简单重复展开权重数组更省内存、更公平。核心是维护每个节点的 current_weight 和全局最大权重 max_weight,每次选节点时:累加所有节点的权重到 current_weight,选当前 current_weight 最大的节点,然后将其 current_weight 减去总权重。
-
current_weight初始值 = 节点权重;每次调度后减去sum(weights) - 选节点前先对所有
current_weight加上对应原始权重(即“恢复增量”) - 必须用指针或可变结构体字段更新状态,否则下次调用还是初始值
- 并发访问需加锁,
sync.Mutex或atomic都可以,但atomic对 float64 不友好,建议用sync.Mutex
type Node struct {
Name string
Weight int
// currentWeight 需可修改,不能只读
currentWeight int
}
<p>type WRR struct {
mu sync.Mutex
nodes []*Node
total int // sum of weights
}</p><p>func (w *WRR) Next() string {
w.mu.Lock()
defer w.mu.Unlock()</p><pre class="brush:php;toolbar:false;">for i := range w.nodes {
w.nodes[i].currentWeight += w.nodes[i].Weight
}
maxIdx := 0
for i := 1; i w.nodes[maxIdx].currentWeight {
maxIdx = i
}
}
w.nodes[maxIdx].currentWeight -= w.total
return w.nodes[maxIdx].Name}
权重为 0 的节点怎么处理
权重为 0 表示该节点不参与调度,但直接过滤掉会影响下标稳定性(尤其动态增删节点时)。更稳妥的做法是在 Next() 中跳过 Weight == 0 的节点,同时在初始化时检查 total 是否为 0 —— 如果全为 0,Next() 应 panic 或返回空字符串并记录 warn,否则后续 currentWeight -= w.total 会变成负数溢出。
- 初始化时计算
total前,过滤掉Weight 的非法值 - 运行时若发现
total == 0,立刻返回错误标识,不要继续算currentWeight - 不建议在
Next()里重新 build 有效节点列表,频繁 alloc 影响性能
如何验证权重分配是否符合预期
跑 1000 次 Next() 并统计各节点命中次数,看是否接近权重占比。注意:平滑 WRR 是周期性序列,短样本可能偏差大,建议至少跑 lcm(权重数组) * 10 次(例如权重 [3,2,1],lcm=6,跑 60 次以上)。
- 用
map[string]int记录频次,对比count[name] / total_calls与weight[name] / sum_weights - 误差超过 ±5% 就要查
currentWeight更新逻辑是否漏锁或重置 - 别用
rand.Float64()做对比基准——那是随机,不是加权轮询
平滑性比绝对精度更重要:连续两次选到同一节点是允许的(尤其高权重节点),但不能出现“连发 10 次给 A,再连发 10 次给 B”这种块状分布。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











