ABTest 平台设计 - 流量分布问题

通过前三篇文章,大家可以了解到一个基本的 ABTest 平台架构建设的要点,看起来构建起一个生产环境可用的 ABTest 平台难度不大。但如果想把这个平台做得更强大,还有很多细节地方要注意。下面举两个常见的细节问题。

跨层分桶不完全正交

理论上来讲,如果每层分桶都是采取的独立不相关的 hash 算法,那么层和层之间的流量应该是完全正交且服从均匀分布的。举个例子:如果每层分 10 个桶,那么第二层的 1 号桶内,应该有第一层每个桶里的 1/10 的用户。

但从实际上来讲,很多情况下我们使用的 hash 算法没有那么地“强”,会导致分桶没那么地随机和均匀。比如几年前非常流行的 MurmurHash,也被 SipHash 的作者找到了攻击的方法,他们更推荐使用更强的 SipHash / HighwayHash

还有就是均匀分布仅仅是统计意义上的期望,而不是实际的效果。比如我们有 1-10 十个数,用 hash 算法一定能保证把它们分到 10 个桶里,每个桶里 1 个数吗?

只不过大多时候,在统计意义上能保证基本的均匀,对大部分产品来说也够用了。

对于一些强迫症公司,或者用户数还没有达到统计意义上可分又开始早早做 ABTest 的公司,或者从数据观察的确存在较大的实验间干涉的公司,可能会做这样的一个事,人工构造层间正交分桶

具体做法是这样的:先用 hash 算法(或其它抽样算法)将用户分成桶数的平方个最小流量单元,然后再选取这些流量单元组成每个分层。比如要构造 2 层,每层 3 个分桶的用户分层,先将用户分为 3*3 个流量单元,那么两层就可以这样构造:

Layer 1: [1, 2, 3] [4, 5, 6] [7, 8, 9]
Layer 2: [1, 4, 7] [2, 5, 8] [3, 6, 9]

可以看到第二层的每一个桶,都包含第一层里所有桶的 1/3 用户。通过精巧地构造来实现层间的正交分桶,可以有效地降低层间实验互相干涉的情况。

发版造成指标波动

当实验本身受到 APP 版本一定影响的时候,AB 分组可能会由于升级节奏不同,导致 AB 指标波动完全不可比。

举一个例子:如果新版本使用时长会上升 10%,正常情况下实验分组 A 比分组 B 使用时长上升 3%。那么如果分组 A 用户升级新版本占比 30%,分组 B 用户升级新版本占比 70%,结果会是怎样呢?

分组 A:70*1.03 + 30*1.1*1.03 = 106.09
分组 B:30 + 70*1.1 = 107

分组 A 反而比分组 B 的数据更差!当然,这里为了效果,举的例子比较极端。实际的情况可能是在发版收敛期间,ABTest 的指标波动较大。

而且升级动作本身对活跃用户和非活跃用户有一定的筛选作用,产生的行为和数据也是有偏的,很难有完美的解决办法。避开发版收敛时段,或者对数据进行多维的分析和组织,也许有一定帮助。

结语

按照我以前的风格,这本应是一篇文章。但在信息快消时代,我发现自己也没耐心读长文了,所以就拆成一个系列发布了。

这篇文章主要从平台建设的角度出发,讨论了一些平台设计中要考虑到的关键功能点,希望能对读者有所助益。至于如何科学地进行 ABTest 实验设计和效果分析,不是我擅长的部分,就不再后续展开了。

ABTest 平台设计 - 如何进行流量分桶

在 2018 年,我相信 ABTest 这个名词已经不用过多地解释了。但我发现很多公司,尤其是初创企业,虽然能理解这件事是什么,却不知道这件事该怎么做,以及该怎么做好。

这一系列文章,就是想讲清楚在设计具体的 A/B 测试平台这种基础架构时,要考虑哪些问题,以及有哪些推荐的做法。

今天先谈一谈:

如何进行用户分桶

我们都知道互联网产品的 ABTest 主要是围绕用户进行的实验,从统计意义上观察用户对不同的产品设计、交互体验、业务流程的反馈,从而指导产品的改进方向。

那么很重要的一点就是,怎么圈定哪些用户进行 A 实验,哪些用户进行 B 实验。

一种错误做法

在我工作过的一家公司,他们是这样做的:

“使用用户的 UserID 对 1000 取模分成 1000 个桶,然后选择不同的桶分配给 A 或者 B。”

我问研发人员为什么这么做?他们给的理由是:

“UserID 是自增 ID,跟用户注册顺序有关,有一定的随机性。可以保证用户随机地分到 A 或者 B 中。”

A/B 的流量圈定的一个重要原则就是无偏,不然无法进行对比评估。上面的做法看起来倒也有一定的道理。(还常见的一种做法是,用手机尾号最后一位来进行分桶,优惠多少就看你手机尾号是否运气好了 ^_^ )

单单考虑孤立实验,这样做也无可厚非。但如果考虑到长期交叉、连续的实验,这样做有很大的问题。

首先,这种设计只能进行单层实验,也就是说一份流量只能通过一个实验。

如果实验人员选择了在任意一个桶中同时进行 X, Y 两个实验的话,那两个实验的结果就会相互干涉,导致最终结果不可信。例如:在尾号为 001 的桶里进行了两个促销活动“降价10%”和“满100减10块”的实验,最终 001 桶的用户订单数比其它桶高,那到底是哪个促销更有效果呢?

其次,这种设计在长期会造成桶间用户行为有偏

也许刚开始因为其随机性,桶间用户行为差异很小。但第一个实验过后,桶间就开始有了行为差异——这也是 ABTest 的目标。N 个实验过后,桶间行为的差异可能就变得非常大了。

比如你总是在 001 桶的用户上实验幅度较大的促销活动,那么 001 桶的用户留存就会显著高于其它桶。那实验人员为了让实验效果更好看,可能会偷偷地继续选择 001 桶进行实验。

最后,这种设计的实验效率太低。因为一份流量只能通过一个实验,无法对流量进行充分的利用。

那该如何设计用户分桶,才能满足 ABTest 的需求呢?

一种正确方法

目前业界应用最多的,是可重叠分层分桶方法。

具体来说,就是将流量分成可重叠的多个层。因为很多类实验从修改的系统参数到观察的产品指标都是不相关的,完全可以将实验分成互相独立的多个层。例如 UI 层、推荐算法层、广告算法层,或者开屏、首页、购物车、结算页等。

单单分层还不够,在每个层中需要使用不同的随机分桶算法,保证流量在不同层中是正交的。也就是说,一个用户在每个层中应该分到哪个桶里,是独立不相关的。具体来说,在上一层 001 桶的所有用户,理论上应该均匀地随机分布在下一层的 1000 个桶中。

通过可重叠的分层分桶方法,一份流量通过 N 个层可以同时中 N 个实验,而且实验之间相互不干扰,能显著提升流量利用率。

从实操上来说,我们通常采取下面的方法:

首先,确定 Layer,确定请求 Tag。例如从 UserID,DeviceID、CookieID、手机号 中选一个,支持匿名流量的,一般会选用 DeviceID 或者 IMSI 等作为请求 Tag。

然后,选一个你喜欢的 Hash 函数,尽量选个使用方便、随机性更强的;

最后,通过 Hash(Layer, Tag) % 1000 确定每层分桶。如果 Hash 函数支持 seed,那么使用 Layer 作为 seed,否则作为 SALT,即将 "Layer+Tag" 作为输入参数。

在完成分桶以后,还可以进行一定的流量筛选。例如来自北京和上海的用户,可以允许分别进行不同的实验。

可重叠分层分桶方法的系统性介绍,可以参见 Google 在 KDD 2010 发表的论文 《Overlapping Experiment Infrastructure: More, Better, Faster Experimentation》,感兴趣的同学可以延伸阅读一下。

更多样的选择

但分层方法并不能让所有人满意,尤其是对并行实验非常多的大公司来说。因为同一层可能有不同份额的其它小流量实验在线上,新实验能够拿到的流量非常有限,需要等待同层其它实验结束,会非常影响迭代效率。而新实验未必一定会修改其它实验的参数,或者影响其它实验的效果。

所以一些公司也逐渐开始探索无分层,也可以称为无限分层的方法。具体的做法是,每个实验都可以看成独立的层,只保证层间流量分桶的正交性,所有的实验都可能存在重叠情况。

这样做用户就有大概率同时中同一类实验,例如两个实验人员同时在实验 UI 布局,实际上用户只能看到一种布局(因为代码分支有先后逻辑),那么实验结果就不可信了。

由于系统上无法保证实验间不冲突,那么只能从组织上来避免冲突,或者从数据上尽早观察到冲突的存在来解决冲突。这对组织的管理能力和企业的数据能力提出了更高的要求。

下篇,我们聊一下实验的开关和分组信息传递