应使用 hash.Sum64() 而非 rand.Intn() 实现 A/B 测试分流,因前者基于用户标识一致性哈希可确保同一用户恒定分组,后者每次请求生成新随机数导致组别漂移;需加盐哈希、选 FNV 算法,并注意权重配置按比例而非百分比、防溢出与并发安全。

为什么用 hash.Sum64() 而不是 rand.Intn() 做分流
随机数无法复现,A/B测试要求同一用户每次请求都落到同一个实验组,否则数据不可信。用 rand.Intn() 会为每次请求生成新随机数,用户在刷新或重试时可能跨组,导致指标污染。
正确做法是把用户标识(如 user_id、device_id 或请求级 trace_id)做一致性哈希:用 hash.Sum64() 计算哈希值,再对实验组总数取模。这样只要输入不变,输出组别就恒定。
- 必须确保输入字符串稳定——比如
user_id不能是空或动态生成的临时 ID - 避免直接对原始字符串哈希,建议统一加盐(如
"ab_salt_" + user_id),防止哈希碰撞或被猜测分桶逻辑 - Go 标准库
hash/fnv比crypto/md5快一个数量级,适合高频请求场景
abtest.Bucket() 的参数陷阱:权重配置不是百分比
很多团队误以为传入 []int{30, 70} 表示 30% 和 70% 流量,实际它只是按数值比例分配——[30, 70] 和 [3, 7] 效果完全一样,但前者容易让人误判精度。真正影响分流精度的是总和是否足够大(比如 [1, 1, 98] 在小流量下可能某组长期无命中)。
- 推荐使用最小公倍数思维:想实现 12.5% / 87.5%,应写成
[1, 7](总和 8),而非[125, 875] - 如果需支持动态权重更新,不要把权重数组存在全局变量里——并发读写需加锁,更稳妥的是用
atomic.Value包裹权重切片 - 注意整数溢出:当权重总和超过
math.MaxUint64时,Sum64() % totalWeight会出错,生产环境建议校验总和
如何让灰度发布和 A/B 测试共用同一套分桶逻辑
灰度常按服务版本号分流,A/B 按实验策略分流,二者底层都是“根据某 key 算哈希 → 映射到桶”,完全可以复用核心函数。关键在于抽象出 BucketKeyGenerator 接口,让不同场景提供自己的 key 构造逻辑。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
例如灰度场景返回 service_version + "_" + user_id,A/B 场景返回 "exp_v2_" + user_id,然后统一走 fnv.New64().Write([]byte(key)).Sum64()。这样既避免重复实现,又保证两套系统哈希行为一致。
- 禁止在 key 中拼接时间戳或随机数,否则破坏稳定性
- 若业务同时跑多个实验,key 中必须包含实验唯一标识(如
exp_id),否则不同实验会相互干扰 - HTTP 中间件里提取
user_id时,优先从 header(如X-User-ID)取,fallback 到 cookie,不要依赖 session —— session 可能未初始化或跨域丢失
线上发现分桶倾斜怎么办:先查 hash distribution 再调权重
上线后发现 B 组流量只有预期的 1/10,第一反应不该是改权重,而是验证哈希分布是否均匀。用线上真实 user_id 样本跑离线统计:取 10 万条 ID,计算 Sum64() % 100,画直方图。如果结果集中在某几个余数,说明输入 key 本身有强规律(比如大量新注册用户 ID 是连续递增整数)。
- 此时加盐(salt)是最有效解法,比如把
user_id转成字符串后拼上固定前缀再哈希 - 若 salt 后仍不均,检查是否用了弱哈希算法(如自写的简单异或),换成
hash/fnv或hash/maphash - 绝对不要通过“给冷门组额外加权”来硬凑流量——这会让实验组之间失去可比性
分桶逻辑看似简单,但真实环境中最棘手的永远不是代码怎么写,而是 key 的语义是否稳定、哈希是否真随机、以及有没有人悄悄绕过中间件直接调用下游服务跳过分桶。

















