不能用math/rand默认实例做加权选择,因其非并发安全、种子固定为1导致多goroutine调用返回相同下标,且rand.Intn(len(servers))仅均匀采样下标,完全忽略权重配置;必须显式创建独立rand实例并结合权重扫描或别名法实现真正加权。

Go 语言标准库不提供 WeightedRoundRobin 或带权重的随机选择器,必须手写;直接用 rand.Intn 或简单取模会忽略权重,等价于均匀分布。
为什么不能用 math/rand 默认实例做加权选择
默认的 rand 实例不是并发安全的,且种子固定为 1,多个 goroutine 同时调用 rand.Intn 可能返回完全相同的下标。更严重的是:它根本不读取权重字段——你传进去的是一个服务器切片,rand.Intn(len(servers)) 只是在下标范围里均匀采样,和配置的 weight 毫无关系。
必须显式创建独立实例:
rng := rand.New(rand.NewSource(time.Now().UnixNano())) idx := rng.Intn(totalWeight) // 再配合线性扫描或别名法
- 不要在全局复用同一个
*rand.Rand实例,高并发下会竞争 seed 状态 - 不要用
rand.Seed()—— Go 1.20+ 已废弃,且它是全局副作用 - 如果调用频次极高(比如每秒万级),建议用
sync.Pool缓存*rand.Rand实例
平滑加权轮询(SWRR)的核心状态怎么维护
关键不是“轮着来”,而是让每个节点维持一个动态的 currentWeight,每次调度前统一增加自身权重,再选最大值,选中后减去总权重。这个过程必须原子或加锁,否则状态错乱。
立即学习“go语言免费学习笔记(深入)”;
典型结构体定义:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
type Backend struct {
Addr string
Weight int64
currentW int64 // 初始为 0,不是 Weight
}
-
currentW初始值必须是 0,否则第一轮就偏斜 - 所有节点的
Weight必须是正整数,0 表示临时剔除(如健康检查失败),不能从列表中删除 - 更新顺序不能错:先对所有节点执行
atomic.AddInt64(&b.currentW, b.Weight),再找最大值,再对选中节点执行atomic.AddInt64(&b.currentW, -totalWeight) - 避免用
float64存currentW—— 浮点误差会随迭代放大,最终导致权重漂移
动态调权时如何避免状态撕裂
运行时修改某个节点的 Weight 字段本身不危险,危险的是:此时可能有 goroutine 正在执行 atomic.AddInt64(&b.currentW, -totalWeight) 或重置逻辑,而你刚把 b.Weight 改了,后续重置 currentW 就会用错值。
安全做法是把权重和当前余量合并存储,用原子 CAS 控制:
// 高 32 位存 weight,低 32 位存 currentW
func (b *Backend) updateWeight(newWeight int64) {
for {
old := atomic.LoadUint64(&b.weightAndCurrent)
weight := int64(old >> 32)
current := int64(old & 0xFFFFFFFF)
newVal := (uint64(newWeight) << 32) | uint64(current)
if atomic.CompareAndSwapUint64(&b.weightAndCurrent, old, newVal) {
break
}
}
}
- 不要直接赋值
b.Weight = newWeight,这是竞态源头 - 如果不用位拆分,就加一个
version uint64字段,每次调权atomic.AddUint64,并在扣减/重置逻辑中校验 version 是否一致 - 重置
currentW的动作(比如归零后加回 weight)必须放在扣减之后、且只在当前 version 未变时才执行
什么时候该用别名法而不是线性扫描
当后端实例数超过 50 且每秒调度超 1000 次时,线性扫描 currentWeight 或按累积区间二分查找都会成为瓶颈。别名法(Alias Method)预处理 O(n),单次查询 O(1),适合高频稳定场景。
但它不支持实时调权:一旦权重变更,就得重建别名表。所以实际选型要看权重要不要热更新:
- 权重基本不变(如部署时配死)→ 用别名法,性能最优
- 权重需运行时调整(如根据 CPU 使用率自动升降)→ 用 SWRR + 原子状态,牺牲一点常数时间换语义正确性
- 实例数很少(
真正容易被忽略的点是:SWRR 的「平滑」不是靠时间间隔,而是靠 currentWeight 的累加-扣减节奏天然拉开了同一节点连续被选中的距离;一旦你在里面塞 time.Sleep,整个调度器就变成串行阻塞模型,既无法并发,也失去响应权重变更的能力。

















