直接用rand.float64()做加权轮询会因缺乏状态维护导致短期分布不稳定和权重变更时状态不一致;正确方案是用atomic+环形数组维护每个节点的current剩余权重,原子选取并扣减,支持动态调权。

为什么直接用 rand.Float64() 做加权轮询会出问题
很多初学者会尝试对权重归一化后,用随机数落点区间来选节点,比如权重 [3, 1, 2] → 区间 [0,0.5), [0.5,0.666), [0.666,1)。这看似合理,但实际在高并发、动态调权场景下会暴露两个硬伤:无法保证短期分布稳定性(某次连续请求全打到同一高权节点),权重变更时状态不一致(新权重生效前已有请求正在按旧权重执行)。真正可用的动态加权负载均衡必须带状态缓存和原子更新能力。
用 sync/atomic + 环形数组实现线程安全的动态加权轮询
核心思路是把“当前权重消耗进度”作为状态维护在每个节点上,每次选择时找“当前剩余权重最大”的节点,并原子扣减。这样既避免锁竞争,又天然支持运行时调权——你只需原子更新对应节点的权重字段即可。
实操建议:
- 每个节点结构体至少包含:
addr string、weight int64(用int64防止原子操作溢出)、current int64(当前剩余权重,初始 =weight) - 选择逻辑:遍历所有节点,用
atomic.LoadInt64(&node.current)读当前值;取最大值对应的节点;再用atomic.AddInt64(&node.current, -1)扣减 - 当某个节点
current归零时,需用atomic.StoreInt64(&node.current, node.weight)重置——这步必须放在扣减后判断,且不能和其他节点重置竞争 - 注意:该算法不是严格按比例分配,而是长期趋近于权重比,适合服务发现类场景;若要求强比例(如压测流量配比),得改用蓄水池采样(Reservoir Sampling)
如何安全地在运行时调整某个节点的权重
关键不是“怎么改”,而是“改完怎么不影响正在执行的选择”。错误做法是直接写 node.weight = newWeight —— 此时若有 goroutine 正在执行 atomic.AddInt64(&node.current, -1),可能刚读完旧 weight 就被你改了 weight,导致重置值错误。
正确做法是引入一个版本号或使用 CAS 循环:
- 给节点加一个
version uint64字段,每次调权时atomic.AddUint64(&node.version, 1) - 在选择逻辑中,读
current前先读version,扣减后重置时校验 version 是否变化;若变化,则放弃本次重置,由下一轮再处理 - 更轻量的做法:把
weight和current合并为一个int64,高 32 位存 weight,低 32 位存 current,用atomic.CompareAndSwapInt64做原子更新——但要注意 current 溢出时的截断风险
别忽略 context.Context 和健康检查的耦合点
动态权重常和节点健康度联动,比如某节点延迟升高,自动降权。但这里有个隐蔽陷阱:权重调整本身不该阻塞请求路由。如果你在每次 Select() 里同步调用 HTTP 探活,整个 LB 就变成串行瓶颈。
实操建议:
- 健康检查必须异步运行(例如用
time.Ticker定期探测),只更新节点的weight字段 -
Select()函数内部只做纯内存计算,不涉及 I/O 或锁等待 - 如果节点权重被设为 0,记得在选择逻辑中跳过它——但不要从节点列表中删除,否则索引错乱影响原子操作安全性
- 导出一个
GetStats()方法,返回各节点的weight、current、最近一次更新时间,方便排查“为什么没按预期分流”
current 归零瞬间,多个 goroutine 可能同时看到 0 并尝试重置,导致权重被多次叠加。解决它不能靠加锁,得靠 CAS 或版本号控制,这点在压测时才会暴露。











