go 中需手写带权重轮询选择器(wrr),核心是维护累积权重指针与服务器列表,跳过权重为0或不可用节点,用整数累加+取模确保概率精确;随机选择器须避免 math/rand 默认实例并发不安全及权重忽略问题,推荐别名法优化高频场景。

Go 语言里没有开箱即用的 WeightedRoundRobin 标准库实现,但用 sync/atomic 和简单状态管理就能写出生产可用的版本;随机选择器(RandomSelector)看似简单,但不加权重校验或重试机制,容易把流量打到已下线或响应超时的实例上。
如何手写一个带权重的轮询选择器(WRR)
核心是维护一个“当前累积权重指针”和服务器列表,每次选节点时递增指针并取模,但要跳过权重为 0 或不可用的节点。关键不是循环次数,而是让高权值节点被选中的概率严格等于其权重占比。
-
weight字段必须是正整数,0 表示临时剔除(比如健康检查失败),不能直接从列表中删除——否则破坏轮询序列连续性 - 避免用浮点运算做归一化,否则浮点误差会导致长期累计偏差;推荐用整数累加 + 取模方式,例如:
totalWeight = sum(weights),然后生成rand.Intn(totalWeight)再线性扫描匹配区间 - 如果后端实例动态增删频繁,建议用原子变量维护
currentIdx+sync.RWMutex保护列表快照,而不是每次选节点都锁整个列表 - 不要在
Select()方法里做健康检查——那是发现层(discovery)的事;负载均衡器只负责按策略转发,故障隔离应由独立的探活 goroutine 维护isAvailable状态位
为什么 math/rand 的默认随机器不适合做服务选择
它不是并发安全的,且默认种子固定(1),同一进程内多个 goroutine 并发调用 rand.Intn() 可能返回相同结果;更严重的是,没做权重归一化时,直接对实例数组下标取随机,等价于均匀分布,完全忽略权重配置。
- 必须显式调用
rand.New(rand.NewSource(time.Now().UnixNano()))创建独立实例,或用sync.Pool缓存以避免频繁初始化 - 若用随机 + 权重,推荐“别名法(Alias Method)”,预处理 O(n),查询 O(1),比线性扫描或二分查找更适合高频调用场景;但小规模实例(
- 注意:
crypto/rand虽然更安全,但性能差一个数量级,仅适用于密钥生成等场景,负载均衡器严禁使用
Go Micro 的 rpc.Client 默认负载均衡行为
它默认用的是 RandomSelector,不是轮询,也不是加权——它只做一次随机,且不重试失败节点;如果你启用了服务发现(如 etcd),它会在每次请求前拉取最新实例列表,但不会缓存权重字段,除非你在注册时显式写入 metadata["weight"] 并自行解析。
- 要启用权重,得自己实现
micro.Selector接口,并传给micro.Client构造函数;官方示例里基本都省略了这步,导致很多人误以为 “Go Micro 支持 WRR” -
micro.NewService()默认不开启负载均衡逻辑,只有调用service.Client().Call()时才触发 selector;若直接用底层transport发送,selector 完全不生效 - 它的
Next()方法返回的是micro.Node,其中Id、Address、Metadata是唯一可用字段;权重必须从Metadata解析,且类型是string,需手动转int
真实服务中容易被忽略的边界点
权重不是越大越好,当某台机器权重设为 100、其余都是 1,它大概率会成为单点瓶颈;更麻烦的是,WRR 在实例数变化时,如果不重置累积状态,会出现短期流量倾斜——比如刚上线一台高权值机器,但旧指针还在老序列里打转。
- 权重更新必须同步通知 selector,不能只改注册中心数据;建议用 channel 或 atomic.Value 传递新权重快照
- DNS 轮询(
DNS Round Robin)和客户端 WRR 不互斥,但叠加时要注意 TTL 导致的实例视图滞后;Go 服务若依赖 DNS 解析 IP 列表,net.Resolver默认缓存结果,需设PreferGo: true并禁用系统缓存 - HTTP 客户端(如
http.Client)的连接复用会掩盖负载不均问题——看起来请求分散了,实际 TCP 连接可能全扎在同一个后端上;务必检查Transport.MaxIdleConnsPerHost是否设为 -1 或足够大
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











