设计一个 A/B 测试与实验平台

本文系统设计一个 A/B 测试与实验平台:需求澄清与量级估算、分流与哈希分桶算法、实验配置下发与一致性哈希、指标采集与显著性检验、互斥层与流量管理、防 AA 异常与 SRM 检测、多重比较与序贯检验陷阱,并给出分桶伪代码、指标 SQL 与统计权衡。

A/B 测试(A/B Testing)是现代产品迭代的「科学仪器」:任何一次改版、推荐策略调整、定价实验,都要用实验数据而不是拍脑袋来决定上线与否。它的技术难点不在「随机分两组」,而在——分流必须稳定可复现、实验之间不能互相污染、指标要能正确归因、统计结论不能被系统性偏差带偏。本文按照系统设计面试的标准答题结构,设计一个支持多业务线、每秒百万级分流、支持互斥层与显著性检验的实验平台。

一句话:A/B 测试的核心是「同一用户每次进来都被稳定分到同一组」——分桶靠确定性哈希而非随机数,这是整个平台正确性的地基。

一、需求澄清与量级估算

1.1 需求澄清

  • 实验类型:只有 A/B 两组的简单实验,还是要多组(A/B/C)、多因素(正交实验)?
  • 分流维度:按用户 ID、设备 ID、还是请求 ID 分流?(决定一致性范围)
  • 实验对象:前端 UI、后端策略(推荐/搜索/定价)、还是推送文案?
  • 指标:只看单一主指标(如点击率),还是要有护栏指标(如崩溃率、延迟)?
  • 流量管理:是否需要互斥层(同一用户不进入两个互斥实验)、流量分层复用?

明确假设(面向面试的合理假设):

需求项假设
实验支持多组、支持互斥层与流量分层
分流维度用户 ID 为主,未登录降级设备 ID
生效范围前后端通用(SDK 拉取配置,服务端决策)
指标主指标 + 护栏指标 + 自定义指标
统计频率派显著性检验 + SRM 检测

1.2 量级估算

指标估算值推导
日活用户1 亿多业务线合计
分流 QPS~100 万每次请求都要判定实验组
同时运行实验~2000各业务线并行
指标事件~1000 亿/天曝光、点击、转化等埋点
实验配置大小~MB 级全量配置可本地缓存
分流延迟要求P99 < 1ms不能拖慢主请求

一句话:分流是热路径(每个请求都走),必须本地计算、零网络依赖;配置下发是冷路径,可异步拉取 + 缓存。两条路径分开设计。

二、高层架构设计

   ┌──────────┐  ┌──────────┐  ┌──────────┐
   │ 业务服务  │  │ 客户端 SDK│  │ 数据平台  │
   └────┬─────┘  └────┬─────┘  └────┬─────┘
        │ 分流(本地)   │ 埋点上报     │ 查指标
   ┌────▼─────────────▼─────────────▼──────────┐
   │             实验平台 (Experiment Platform)   │
   │  ┌──────────┐ ┌──────────┐ ┌────────────┐  │
   │  │ 实验管理  │ │ 分流引擎  │ │ 指标计算    │  │
   │  │ (配置CRUD)│ │ (SDK/服务)│ │ (统计检验)  │  │
   │  └──────────┘ └──────────┘ └────────────┘  │
   │  ┌──────────┐ ┌──────────┐ ┌────────────┐  │
   │  │ 互斥层    │ │ 流量分层  │ │ SRM/AA 校验 │  │
   │  └──────────┘ └──────────┘ └────────────┘  │
   └──────────────────┬──────────────────────────┘
                      │
   ┌──────────────────▼──────────────────────────┐
   │  配置中心(推送) + 埋点管道(Kafka) + 数仓(OLAP)  │
   └─────────────────────────────────────────────┘

四层职责:

  1. 实验管理:实验创建、流量分配、上下线、审批流。
  2. 分流引擎:SDK/服务端按哈希分桶,本地决策,零网络依赖。
  3. 指标计算:从埋点管道聚合指标,跑显著性检验。
  4. 流量治理:互斥层、流量分层、SRM/AA 校验。

2.1 分流为什么必须在本地

分流在每个请求的热路径上,如果每次都要 RPC 问实验平台「我该进哪组」,延迟和可用性都无法接受。做法是:把实验配置(含分桶盐值、流量比例)全量下发到各业务进程/SDK 本地缓存,分流时纯本地哈希计算,实验平台挂了也不影响线上分流。

一句话:实验平台是「配置的生产者 + 结果的消费者」,而不是分流路径上的同步依赖——本地计算保证分流永远可用。

三、核心组件设计

3.1 哈希分桶(核心算法)

分桶要满足:确定性(同一用户每次结果相同)、均匀性(各组流量比例精确)、稳定性(扩缩流量时已入组用户不漂移)。

import hashlib

def bucket(unit_id: str, salt: str, n_buckets: int = 10000) -> int:
    # 用 salt 隔离不同实验,避免同一用户在所有实验里都进同一组
    h = hashlib.md5(f"{salt}:{unit_id}".encode()).hexdigest()
    return int(h[:8], 16) % n_buckets          # 落在 [0, n_buckets)

def assign(unit_id, exp):
    b = bucket(unit_id, exp.salt, 10000)
    acc = 0
    for group in exp.groups:                    # 按组流量比例划分桶区间
        acc += group.traffic_permille * 10      # 千分比 → 万分桶
        if b < acc:
            return group.name
    return "control"                            # 未命中任何组 → 对照组

关键点:

  • 盐值(salt):每个实验一个盐,保证不同实验的分桶独立(正交)。
  • 分桶数:10000 桶,支持 0.01% 精细流量。
  • 区间划分:组按累计流量区间切分,扩组时只扩大区间,已入组用户尽量不动(用一致性哈希思想)。

3.2 流量分层(Layer)与互斥(Mutual Exclusion)

机制目的做法
流量分层让多个实验复用同一批流量但互不干扰不同层用不同盐值,同层内流量互斥
互斥层保证同一用户不同时进入互斥实验互斥实验共用同一分桶空间,区间不重叠
分层:  Layer1(UI层) 盐=ui     ── 实验A占 [0,5000), 实验B占 [5000,10000)
       Layer2(算法层) 盐=algo ── 实验C占 [0,3000)   ← 与Layer1可重叠(正交)
互斥:  同层内,实验A与实验B区间不重叠 → 用户不会同时进A和B

分层的价值:不同层的实验维度不同(UI vs 算法),可以同时进行且互不污染;同层实验共享流量池,因此天然互斥。

3.3 实验配置下发

  • 配置中心:实验配置(盐、组、流量、开关、受众定向)存配置中心,变更时推送到各业务节点。
  • 本地缓存 + 版本号:节点本地缓存全量配置,带版本号;配置变更通过长连接/轮询增量下发。
  • 兜底:拉取失败时用本地缓存旧版本;无缓存则全部进对照组(安全降级)。
  • 受众定向:支持按城市、版本、用户分群定向,定向规则在分桶前评估。

3.4 指标采集与归因

  • 埋点:曝光、点击、转化等事件带 unit_id + 实验组标识(由分流时注入)上报到 Kafka。
  • 归因:以分流时确定的组为准,而不是看埋点时刻的配置(防止实验中途改配置导致归因错乱)。
  • 指标体系:主指标(如 CTR)、护栏指标(延迟、崩溃率、负反馈)、漏斗指标(曝光→点击→下单)。
埋点结构: { event, unit_id, exp_id, group, ts, ...props }
→ Kafka → 实时聚合(Flink) + 离线数仓(ClickHouse/Spark)

3.5 显著性检验

实验跑完要回答「差异是真实的还是随机波动」,核心是统计检验:

import math
from scipy import stats

def ab_test(control_conv, control_n, treat_conv, treat_n):
    p1, p2 = control_conv / control_n, treat_conv / treat_n
    p_pool = (control_conv + treat_conv) / (control_n + treat_n)
    se = math.sqrt(p_pool * (1 - p_pool) * (1/control_n + 1/treat_n))
    z = (p2 - p1) / se if se else 0
    p_value = 2 * (1 - stats.norm.cdf(abs(z)))     # 双尾
    # 置信区间
    se_diff = math.sqrt(p1*(1-p1)/control_n + p2*(1-p2)/treat_n)
    ci = (p2 - p1 - 1.96*se_diff, p2 - p1 + 1.96*se_diff)
    return {"lift": p2 - p1, "p_value": p_value, "ci95": ci,
            "significant": p_value < 0.05}
概念含义常见陷阱
p 值差异由随机造成的概率p<0.05 不等于「效果大」
置信区间效应的可能范围区间跨 0 则不显著
统计功效能检出真实效应的概率样本不足导致「假阴性」
多重比较同时看很多指标不校正会假阳性暴涨(Bonferroni)
序贯检验边跑边看随时看随时停会抬高假阳性

一句话:p<0.05 只说明「不太可能是随机」,不说明「差异重要」;平台必须同时给出效应量、置信区间和护栏指标,否则会诱导「只要显著就上线」。

四、数据模型

存储用途说明
exp_config实验配置盐、组、流量、定向、状态
exp_layer流量分层层名、盐、层内实验列表
exp_mutex_group互斥组互斥实验集合
assignment_log分流日志unit_id→组的映射(可选,用于回溯)
metric_event指标事件Kafka,曝光/点击/转化
exp_result实验结果聚合后的指标与检验结果

配置表关键字段:exp_id、salt、status(草稿/运行/暂停/结束)、start_ts/end_ts、groups(jsonb)、audience(jsonb)。

五、关键流程

5.1 一次分流(热路径)

业务请求 → SDK.get_group(unit_id, exp_id)
  1. 本地配置里查 exp_id(无则返回 control)
  2. 检查状态:未运行 → control
  3. 检查受众定向:不匹配 → control
  4. 哈希分桶 → 命中区间 → 返回组名
  5. 注入埋点上下文(exp_id, group)
全程无网络调用,P99 < 1ms

5.2 实验上线流程

1. 创建实验:定义假设、主指标、分流比例、受众
2. AA 校验:先让两组跑一段,验证无系统性差异
3. 灰度:5% → 20% → 50% 逐步放量,观察护栏指标
4. 正式运行:收集样本到足够功效
5. 分析:显著性检验 + 分群下钻 + 护栏检查
6. 决策:全量 / 迭代 / 下线

5.3 指标计算

实时: 埋点 → Flink 窗口聚合 → 每 5 分钟更新实验看板
离线: 埋点 → 数仓 → 每日跑完整检验(含分群、CUPED 方差缩减)

离线的每日全量检验、分群下钻、回溯重算都是典型的大批量定时作业,可复用 分布式任务调度 的编排能力,把「实验结束→自动出报告」串成 DAG。

六、可靠性与一致性

6.1 分流的一致性

  • 确定性哈希:同一 unit_id + salt 永远同一桶,跨机器、跨重启都一致(无状态)。
  • 降级:配置拉不到时用旧缓存;完全没有则全进对照组,保证线上不因实验平台故障而异常。
  • 未登录用户:用设备 ID 分流;设备 ID 也没有则用会话 ID(体验会抖动,可接受)。

6.2 SRM 与 AA 检测

  • SRM(Sample Ratio Mismatch):实际分流比例与配置不符(如配置 50/50 实测 52/48),说明分流有 bug,此时实验结论不可信。用卡方检验检测。
  • AA 测试:上线前让两组跑相同代码,验证指标无显著差异,排除埋点/分流偏差。
  • 异常检测:某组样本量骤降、指标突变、护栏指标恶化时自动告警。

6.3 数据一致性

  • 归因固定:以分流时的组为准,配置变更不影响已入组用户的归因。
  • 事件去重:同一事件重复上报用 event_id 幂等去重。
  • 晚到数据:允许事件晚到(如离线补报),用事件时间窗口而非处理时间聚合。

一句话:实验结论的可信度建立在「分流比例正确(SRM 合格)+ 归因稳定 + 埋点完整」之上,任何一环出错,再漂亮的 p 值都是假的。

七、性能与扩展

  • 本地分流:全量配置本地缓存,分流零网络,支撑百万 QPS。
  • 配置增量下发:只推送变更的实验,减少带宽。
  • 埋点异步:埋点走本地队列 + 批量上报,不阻塞主链路。
  • OLAP 加速:指标聚合用 ClickHouse/Doris 列存,秒级响应多维下钻。
  • 方差缩减:用 CUPED(用实验前数据做协变量)降低方差,小流量也能快速出结论。

容量与热点

  • 2000 个并行实验 × 每实验几十组 ≈ 配置仅数 MB,全量缓存无压力。
  • 埋点量 1000 亿/天,靠 Kafka 分区 + 列存压缩扛住。

八、权衡与备选

决策点本文选型备选权衡说明
分流位置本地 SDK服务端 RPC本地零延迟高可用;RPC 灵活但成瓶颈
分桶哈希MD5 + 取模一致性哈希MD5 简单均匀;一致性哈希利于扩流量不漂移
检验方法频率派(t/z 检验)贝叶斯频率派通用;贝叶斯直观但需先验
指标存储ClickHouseDruid/SparkClickHouse 快且便宜;Spark 适合重批处理
配置下发推送 + 本地缓存纯轮询推送实时;轮询简单但延迟高

关键取舍

  • 统计严格 vs 决策速度:严格检验要足够样本、跑够时间,与「快速迭代」冲突;可用序贯检验/贝叶斯做折中。
  • 流量复用 vs 实验隔离:分层让流量复用(更多实验并行),但同层实验必须互斥。
  • 实时 vs 准确:实时看板快但可能有晚到数据,最终结论以离线为准。

九、扩展场景与面试追问

9.1 推荐与广告的 A/B

推荐策略实验(召回/排序)天然适合 A/B,与 设计一个推荐系统 的在线排序结合,实验直接改排序模型;广告场景要额外看「广告收入 + 用户体验」双指标,参见 设计一个广告平台 。搜索排序实验同理,见 设计一个搜索引擎 。

9.2 正交实验与多因素

多个独立因素(如「按钮颜色 × 文案」)用正交实验(正交表)设计,用最少实验次数估计各因素主效应,避免全组合爆炸。

9.3 面试常见追问

追问关键回答
怎么保证同一用户分到同一组?确定性哈希(unit_id + salt),无状态、跨机器一致
实验之间会互相干扰吗?同层实验互斥(共用分桶空间),不同层正交(不同盐)
p<0.05 就能上线吗?不能,还要看效应量、置信区间和护栏指标
为什么上线前要 AA 测试?排除分流/埋点系统偏差,验证两组同代码下无显著差异
SRM 是什么?实际分流比例偏离配置,说明分流有 bug,结论不可信
小流量实验怎么快点出结论?用 CUPED 方差缩减 + 选择高灵敏指标

十、总结

模块关键设计一句话记忆
分桶确定性哈希 + 盐同人同组,跨机一致
流量治理分层 + 互斥不同层正交,同层互斥
下发本地缓存 + 推送分流零网络依赖
指标埋点 + 列存聚合归因固定、事件幂等
统计显著性 + 护栏显著≠重要
质量AA + SRM 检测先验证系统无偏差

一句话:A/B 平台的面试核心是讲清楚「如何用确定性哈希保证分流稳定、分层与互斥如何管理流量、指标怎么采集归因、以及显著性检验的陷阱(显著≠重要、多重比较、序贯检验)」,把「分流本地化、AA/SRM 先验证」挂在嘴边,而不是只会跑个 t 检验。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「design」更多文章

  1. 设计一个视频会议系统(WebRTC SFU)
  2. 设计一个分布式锁服务
  3. 设计一个 LBS 附近的人系统(Geohash 与空间索引)