
变好/变差都希望有一个科学的衡量指标, 确保我们都在朝着同一个方向努力!
if (getBoolFlagValueOrDefaultValueByUser(Flag Name, User ID)) {
// 实验组逻辑
} else {
// 对照组逻辑
}
为了让同时进行的实验, 互不干扰, 我们可以先对用户 (随机) 分层, 每一层仅进行一个实验. 拿切蛋糕举例, 你要做实验了, 先按你的需求横切一片给你, 然后你自己在这一片上分组, 开实验, 这样肯定互不干扰, 这一片就是一个 Layer, 因此用 Layer 这个英文单词来表示实验.
这种理想方式不一定适用, 首先需要用户体量很大, 其次是实验结束, 如何放回. 对于公司现状是不适合用这种方式的, 我们需要在每次实验中都用完所有的用户流量. 在这种情况下, 我们要让不同实验的实验组是正交的, 这就好比切方形蛋糕, 竖着切一刀, 左边染成红色, 右边染成绿色; 再切一刀, 希望保证两部分的红/绿占比一致, 那么我们需要垂直第一刀切; 如果再切一刀, 还希望保证两部分的四种组成比例一致, 就需要同时垂直前两刀, 就是要横着切.
当两部分各种组成比例一致, 就可以认为在实验前两者是平衡的, 通过调节变量, 控制实验组的表现, 与对照组进行对照, 引入显著性检验.
回到前面的切蛋糕, 然后需要再增加一个实验, 再切一刀, 还是希望保证两部分的八种组成比例一致, 我们要怎么实现呢? 只需要在切之前, 把蛋糕揉搓一番, 即先将这个 Layer 统一打散, 对应代码如下
public static int getBucketNo(String paramId, int layerId, int bucketTotalNum, String seed) {
if (0 == bucketTotalNum) {
bucketTotalNum = BUCKET_TOTAL_NUM;
}
// 计算 MD5
String destKey = (StringUtils.isNotBlank(paramId) ? paramId : "") + layerId;
if (StringUtils.isNotBlank(seed)) {
destKey = destKey + seed;
}
String md5Hex = DigestUtils.md5Hex(destKey);
long hash = Long.parseLong(md5Hex.substring(md5Hex.length() - 16, md5Hex.length() - 1), 16);
if (hash < 0) {
hash = hash * (-1);
}
// 取模
return (int) (hash % bucketTotalNum) + 1;
}
如何验证这种方式的有效性, 那就统计一下分布占比.
注意: Flag Name 全局唯一
Client 通过 Caffeine 缓存每 10 秒同步一次实验平台的全部配置信息, 如下逻辑, 详情参见 com.xiaohongshu.imitation.client.service.ExperimentData.
private LoadingCache<String, ExpDicInfo> expInfoCache = Caffeine.newBuilder()
.refreshAfterWrite(10, TimeUnit.SECONDS)
.expireAfterAccess(25, TimeUnit.SECONDS)
.maximumSize(4)
.build(k -> updateExpInfo());
Client getXxxFlagValueOrDefaultValueByUser 方法 Flag Name + User ID 获取控制因子的值, 计算逻辑如下:
由于 Flag 是全局唯一的, 根据 Flag 反查出 Layer ID
User ID + Layer ID (字符串拼接) => md5 => 取后 15 个字符, 转为 Long => 取模 => 分桶 => 分桶映射 => 哪个实验组 => Flag 配置信息
将 ID 映射为分桶的过程参见 com.xiaohongshu.imitation.common.utils.BucketUtils, 逻辑如下
实验组流量控制, 用户先分桶, 在映射到对应的实验组, 连续 id, 压缩存放, 分组的变换, 全部存成历史记录
线上 SDK 打点, 不管实验信息, 仅记录如下数据
user_id, timestamp, event, end, action...
打点数据发送到 kafka, 数据收集模块, 消费 kafka 消息, 然后做如下操作
将用户打点日志, 通过离线计算, 形成用户维度的统计明细表, 放入 ClickHouse, 同时 join 上用户所在的所有分组 exp list. 具体看实验结果时, 就是一个聚合运算, 类似于
select
expi,
count(1) as dau,
sum(imp) as imp
...
from ck
where exp list contains exp1, exp2
group by exp