实验分桶原理:哈希分桶、一致性哈希、分桶策略与流量分配

分桶,说白了就是把用户扔进不同的实验组里。你想想看,一个AB测试系统,每天几百万甚至上亿的用户进来,怎么保证同一个用户每次看到的实验版本都一样?怎么保证不同实验之间不互相干扰?这些问题的答案,都藏在分桶原理里。

我个人习惯把分桶比作「分房间」。用户来了,根据他的身份证号(用户ID)算一下,把他分配到1号房间还是2号房间。1号房间看实验组,2号房间看对照组。就这么简单。但真正落地的时候,坑可不少。

哈希分桶:最基础的分桶方式

哈希分桶,就是拿用户的某个标识(比如user_id)算一个哈希值,然后对分桶数取模。伪代码长这样:

def hash_bucket(user_id, total_buckets=100):
    hash_val = hash(user_id)  # 比如用MD5或MurmurHash
    bucket = hash_val % total_buckets
    return bucket

嗯,这里要注意。直接用 hash() 函数?不行。Python自带的 hash() 每次进程重启结果都不一样。我在项目中遇到过这个问题,上线后发现同一个用户今天在实验组,明天跑到对照组去了,数据直接废了。

避坑指南: 生产环境一定要用确定性哈希函数,比如MD5、SHA256或者MurmurHash。而且最好自己实现,不要依赖语言内置的hash方法。

那分桶数设多少合适?我一般建议设成100或者1000。100的话,每个桶就是1%的流量。想切10%的流量做实验,就选10个桶。灵活,也好理解。

分桶数 最小流量粒度 适用场景
100 1% 大多数AB测试
1000 0.1% 小流量实验、灰度发布
10000 0.01% 超大规模、多实验并行

一致性哈希:解决扩缩容的难题

哈希分桶有个硬伤。你想想,如果分桶数从100变成101,所有用户的桶号都得重新算。这意味着什么?所有实验的流量分配全部乱套。这就是所谓的「哈希雪崩」。

一致性哈希就是来解决这个问题的。它的核心思想是:把哈希值空间看成一个环,用户和桶都映射到这个环上。用户顺时针找到的第一个桶,就是他的归属。

我画了一张图,帮你理解这个结构:

一致性哈希环结构图 桶A 桶B 桶C 桶D 桶E 桶F 用户1 用户2 用户3 用户顺时针找到第一个桶:用户1→桶A,用户2→桶F,用户3→桶B 新增桶时,只有相邻区域的用户需要重新分配,其他用户不受影响

你看,用户2顺时针找到的第一个桶是F。如果这时候新增一个桶G,插在用户2和桶F之间,那只有用户2需要重新分配,其他用户纹丝不动。这就是一致性哈希的魅力。

我的经验: 一致性哈希在实验平台里用得不多,但在缓存分片、数据库分库分表场景下是标配。如果你做的是超大规模实验平台(千万级用户),可以考虑用一致性哈希来管理分桶配置。

分桶策略:怎么分才科学?

分桶策略,说白了就是「用什么规则把用户分到桶里」。常见的策略有这么几种:

  • 用户ID分桶: 最常用。用user_id做哈希,同一个用户永远进同一个桶。适合长期实验。
  • 设备ID分桶: 用户没登录怎么办?用设备ID。适合未登录用户的实验。
  • 分层分桶: 同一个用户在不同实验层里进不同的桶。这是解决「多实验并行」的关键。
  • 随机分桶: 每次请求随机分配。适合短期、不计用户一致性的实验。

我个人最推荐的是「用户ID + 分层分桶」的组合。为什么?因为实际业务中,往往同时跑十几个实验。如果所有实验都用同一个分桶逻辑,那实验之间就会互相污染。

举个例子:实验A和实验B都用user_id分桶。用户123在实验A里进了实验组,在实验B里也进了实验组。这时候你发现实验B的效果好,但到底是实验B本身好,还是因为实验A的改动影响了用户行为?说不清楚。

分层分桶怎么解决?每个实验层用不同的盐值(salt)做哈希:

def layer_bucket(user_id, layer_name, total_buckets=100):
    # 盐值 = 层名 + 用户ID
    hash_input = f"{layer_name}:{user_id}"
    hash_val = hash_md5(hash_input)
    bucket = hash_val % total_buckets
    return bucket

这样一来,同一个用户在「推荐算法层」和「UI样式层」里,进的桶是完全独立的。实验之间互不干扰。

分桶策略 优点 缺点 适用场景
用户ID分桶 一致性高,实现简单 多实验互相干扰 单实验、长期实验
设备ID分桶 覆盖未登录用户 设备更换导致分桶变化 移动端、Web端
分层分桶 多实验并行无干扰 需要管理层配置 大规模实验平台
随机分桶 实现最简单 用户一致性差 短期效果验证

流量分配:切蛋糕的艺术

流量分配,就是决定「多少用户进实验组,多少用户进对照组」。常见的分配方式有:

  • 等比例分配: 实验组和对照组各50%。最公平,但样本量需求大。
  • 不对称分配: 比如实验组10%,对照组90%。适合高风险实验,先小流量验证。
  • 动态分配: 根据实时效果调整流量比例。比如实验效果好,逐步从10%扩大到50%。

我曾经踩过一个坑。有个实验,我设了实验组10%、对照组90%。结果跑了三天,实验组的效果显著优于对照组。我兴冲冲地准备全量上线,结果发现实验组的样本量太小,统计功效不够,结论不可靠。

避坑指南: 不对称分配时,实验组流量不要低于总流量的5%。否则样本量太小,统计检验的效力会大打折扣。我一般建议最低10%。

流量分配还有一个关键点:流量正交。什么意思?就是不同实验之间的流量不能重叠。比如实验A用了1-10号桶,实验B就不能再用这些桶。否则同一个用户同时参与两个实验,结果就乱了。

实现流量正交,我常用的方法是「桶位预留」:

# 桶位分配表
bucket_allocation = {
    "experiment_A": list(range(1, 11)),   # 1-10号桶
    "experiment_B": list(range(11, 21)),  # 11-20号桶
    "experiment_C": list(range(21, 31)),  # 21-30号桶
    # 预留70个桶给其他实验
}

def get_experiment_group(user_id, experiment_name):
    bucket = hash_bucket(user_id, 100)
    if bucket in bucket_allocation[experiment_name]:
        return "treatment"
    else:
        return "control"

这样设计,每个实验独占一部分桶位,互不干扰。而且预留的桶位可以随时分配给新实验,灵活得很。

核心要点: 分桶的本质是「确定性映射」。同一个用户,同一个实验,永远进同一个组。这是AB测试的基石。哈希分桶保证一致性,分层分桶保证独立性,流量分配保证可控性。三者缺一不可。

好了,分桶的原理就聊到这儿。记住一句话:分桶做不好,实验数据全是垃圾。我见过太多团队,辛辛苦苦跑了一个月的实验,最后发现分桶逻辑有bug,所有结论都得推翻重来。嗯,希望你不要踩这个坑。