背景原因

  • 实验系统本质上是为了在不直接全量上线的情况下, 用随机分流去验证某个改动到底有没有提升业务指标
  • 这套系统需要同时解决几个问题:
    • 随机性: 用户要被尽可能均匀地打散
    • 稳定性: 同一个用户在实验周期内不能反复漂移到不同组
    • 可复用性: 不同流量层可以并行做实验, 尽量互不干扰
    • 可归因性: 最终要能把用户行为准确归到某个实验组上

系统设计

  • Libra 大致可以拆成 4 块:
    1. Experiment Service: 管实验、版本、配置等元数据
    2. Traffic Allocation Service: 在线分流, 决定一个 request 命中哪些实验组
    3. Metrics Service / Libra Gallery: 管指标元信息, 生成离线计算任务
    4. Report / Data Portal: 聚合实验结果, 给报表侧查询
  • 整体主链路就是: 平台配置实验 -> 分流服务命中 -> 命中日志/埋点落表 -> 离线算指标 -> 报表展示

分流命中

如何做用户分流

  • 业务请求会带 token, token 对应一个 Product, 一个 Product 下会挂多个 Layer
  • 分流不是给用户只算一次哈希, 而是会把这个 token 下的所有 Layer 都遍历一遍
  • 每个 Layer 都会独立执行下面的判断:
    1. 测试用户检查
    2. 流量分桶
    3. 过滤条件
    4. 用户分群
  • 流量分桶的本质是:
按 uid 分流: hash("uid_type:uid:layer_name") % 1000
按 did 分流: hash("did:layer_name") % 1000
  • 这里的 hash 一般用的是 MurmurHash, 它本身会先输出一个很大的整数, 最后通过 % 1000 才映射到 0-999 的桶号
  • 1000 个桶不是哈希函数决定的, 而是系统设计上的选择, 好处是最小流量粒度是 0.1%, 同时分配和管理成本也比较可控
  • 如果当前桶被分配给某个实验, 才算进入这个实验; 进入实验之后还会再做一次版本分组计算, 决定命中 v0/v1/v2 哪个组
hash("id:flight_name") % version_num
  • 同一个 Layer 内一个用户最多命中一个实验, 所以同层天然互斥

多个流量层命中

  • 不同 Layer 之间不会复用同一个桶位, 而是每一层都重新计算一次哈希
  • 同一个 did 在不同层会进不同桶, 关键是哈希输入里带了 layer_name
  • MurmurHash 的雪崩效应保证输入哪怕只差一个字符, 输出结果也会明显不同, 这就是层间正交的基础
层A: hash("device_123:layer_A") % 1000 -> 347
层B: hash("device_123:layer_B") % 1000 -> 891
层C: hash("device_123:layer_C") % 1000 -> 203
  • 所有 Layer 都判断完之后, 分流服务会把命中的实验配置 merge 后返回
  • 所以一个 request 可以同时命中多个 Layer, 但每个 Layer 内最多只会命中一个实验

调试用户命中

  • 调试用户本质上就是 Libra 里的测试用户, 它是配在“某个实验的某个实验组”上的, 不是直接配在 Layer
  • 但由于分流流程是逐层执行的, 某一层一旦通过测试用户命中了某个实验组, 这一层就不需要再继续做正常的哈希分桶
  • 测试用户检查的优先级高于正常分桶, 通常顺序是:
    1. 测试用户
    2. 哈希分桶
    3. 过滤条件
    4. 用户分群
  • 常见口径里会支持 uid/did/uuid 这类标识做测试命中, 一般优先级是 uid > did > uuid
  • 测试用户通常还有两种模式:
    • 只要命中标识就直接生效
    • 命中标识后还要继续满足过滤条件才生效
  • 测试用户只影响当前命中的那一层, 其他 Layer 还是正常走各自的哈希和分桶逻辑

数据上报

  • 实验组和对照组的数据不是分开建两套链路, 核心做法是先记录“用户命中了哪个实验组”, 后续再按实验组字段聚合
  • 核心区分字段是 vid/version_id, 也就是实验组 ID
  • 服务端实验:
    • 业务服务调用分流接口时, 分流服务会记录命中结果
    • 后续把命中日志和业务行为表 join, 再按 flight_id + vid 统计指标
  • 客户端实验:
    • 端上主动上报命中曝光埋点到 eventlog
    • 再从埋点日志抽到实验命中表, 后续和业务表 join
  • 统计流程可以简单理解成:
分流命中 -> 记录 uid/did + flight_id + vid -> join 业务行为表 -> 按 vid 分组统计
  • 从实验语义上看, 同一个用户在同一个实验里同一时刻只会对应一个有效 vid
  • 但底层日志表里可能会因为多次请求、按天落表、曝光链路等出现多条记录, 真正做统计时还是要按 flight_id/vid/date 这类口径聚合去看
  • 指标计算本身一般不是同步完成的, Libra Gallery 会给实验指标生成周期任务, 最终再汇总到报表侧