火山引擎 A/B 测试平台设计思路与技术实现
导读: 大家思考一下,想要获得一个 A/B 实验系统,需要做些什么事情?火山引擎团队会把这些事情分成四个部分。A/B 实验需要通过人群采样,分出对照组和实验组并且下发不同的配置,让用户体会到不同的策略。因此从实践角度来看,四个部分中首先得有一个可靠的实验系统。通过这个实验系统,我们可以采集数据,从而观测用户在不同的策略下的反应。考虑到业务的迭代过程需要保持高效性,因此系统在第二步需要高效的数据建设。当采集到数据之后,我们还需要借助统计学知识,对各组的结果进行分析,以得到正确的实验结论,这第三步被称为科学的统计分析。有了以上三步,我们离一个完整的实验系统还有最后一步。我们考虑实验平台具有一定的成本,测试人员有可能会犯错误,平台运行也可能会出现异常……这些问题都需要系统通过最后一步——精细的治理和运维,来保证实验始终正常运行。
今天的介绍会围绕下面五点展开:
- A/B 实验系统平台概览
- 灵活的执行组件
- 高效的数据建设
- 科学的统计分析
- 精细的治理运维
分享嘉宾| 王珂 火山引擎 A/B测试研发工程师
编辑整理|陆观
出品社区|DataFun
01 A/B 实验系统平台概览
在本文中,我们将详细讲解火山引擎 A/B 实验系统平台背后的四个部分。
首先,我们对平台进行一个整体介绍。俗话说,“皮之不存,毛将焉附”,可靠的实验系统是整个平台最基础的一个部分。图1-1展示的是火山引擎实验平台所具备的能力图谱。 一个实验平台的最核心点是对于业务场景的支持。 在字节跳动这家公司里,我们在对内对外都拥有比较广泛的业务场景,比如:推荐、搜索、广告、电商、直播、运营以及推送等一系列业务。不同的场景对产品提出了不同的功能要求,这也推动着实验平台去解决业务场景上所带来的新问题,并且不断地往前迭代。
图1-1火山引擎实验平台的能力图谱
围绕着这个核心问题,我们 需要三个基础环节的帮助 ,也就是黄色框中的三个长方形。
- 第一个是执行组件,一个实验进行时,首先需要将准确的配置定向下发给准确的用户,也就是做好流量的配置发布。
- 第二个环节是数据建设,通俗来讲就是我们得将数据采集上来。
- 第三个是显著性计算环节,当采集完数据之后,实验组与对照组之间产生的差距是否代表新策略会带来收益,会依赖于相关统计指标的计算。
以上三点是平台最基础的能力,围绕着这个实验平台,我们还需要四个紫色框中的辅助功能。
- 首先,实验平台本身就具有定向的配置发布能力。在完成一个实验之后,下一步的抉择一般就是将策略废弃或者上线,对接一个完整的配置发布平台,是一个实验必要的后向延续。
- 下一个部分,探索实验室是针对实验无法处理的评估场景,研究怎么样辅助去做一些决策。在探索实验室里面,我们往往会尝试一些非 AB 实验的东西,比如人工合成对照组,对于一些只能将用户分在实验组中的策略,我们可以通过人工合成的方式,给构建出一个对照组进行实验,比较策略的效果。在决策分析部分,我们之前提到的显著性计算,只是分析过程中的一部分。
- 除此之外,我们可能还要分析更多东西,比如策略的功效是否足够,是否需要继续提升;比如实验有没有比较严重的首因效应,用户是真正喜欢这个策略,还是因为策略看起来比较新鲜,所以大家多点击了一下。这样一些分析虽然不在显著性分析的范畴里面,但是对于实验的角色分析而言同样非常重要,是决策分析的一个部分。决策分析可以用数据和事实去说话,为业务提供决策的辅助。
- 最后一个重要的功能是智慧决策。例如,我们在一个典型的推荐场景里面,想通过调整参数来获得收益,可能需要不断尝试新一轮实验来获得比较好的参数。这种方式虽然可行,但是非常耗时。于是,我们想要通过自动调参的方式,根据每次实验所拿到的数据进行一些分析,去选择下一次的实验点位,从而大幅度提升决策的效率。
除了以上提到的几项以外,我们还需要一些别的功能。白色圆圈中指出了迭代控制、需求管理、健康监控、精准熔断四项。所有的组件聚合在一起,就构成了一个可以比较完整的实验平台。
02 灵活的执行组件
平台的第一部分,我们来详细讲解一下执行组件。说到执行组件,我们经常会提及到一个词——分流。所谓分流就是随机采样一部分用户来做实验,然后随机分配一半的用户进对照组,一半的用户进实验组。之后,平台将会对照组的用户下发配置A,实验组的用户下发配置B。这一整套流程实际上就是所谓的分流组件,业界通常会有四种不同的接入方式,对应图2-1中的四个框。
图2-1 业界分流的四种接入方式
最常见的两种分流方式是 RPC 和 SDK 。字节跳动在对内的场景里面比较多地使用 RPC。这种方式的好处是接入简单,并且足够灵活,而且对于平台来说比较可控。我们只需要发一个 RPC 调用,就可以知道用户命中的实验以及发布的配置等信息。每当分流组件需要有功能的升级时,只需要升级该功能自己的服务即可。那与之相对应的还有另外一种方式——SDK。考虑到了调用RPC的服务时的效率问题,SDK 接入直接提供了一个库文件在本地做分流。这种接入方式可以发挥机器的极限性能,经过比较好的优化,SDK 的接入效率一定是最高的。但是与 RPC 相比,SDK很难去避免几个问题。第一,当功能升级的时候,由于升级成本的存在,因此控制性可能会略微弱一点,也就是说我们很难保证所有使用 SDK 接入的业务方,都能够做到很好的升级。另外,业务方可能会使用不同的服务端语言,综合考虑不同服务端的 SDK 时,就需要进行多语言覆盖,这个本质上也是一个限制。
举例来说 ,在推荐系统的一个场景里面,如果需要在三万篇文章里进行召回实验,比如一次性召回五千篇文章。那么 RPC 肯定跑不动这个实验,全部的资源都会去进行 RPC 调用。但是如果用 SDK ,只需要一个 for 循环,就可以完成这个实验。那么,怎么可以结合 RPC 与 SDK 之间的优势呢?这里我们会讲到第三种方式——伴生进程。这种技术方案就是在业务进程的节点上,再添加一个伴生进程,用 C ++ 做封装,然后去做一个 Unix 的 Domain Socket。通过这种方式,网络传输只是在本地本机本节点上去做进程间的一个调用,可以使性能做到很大程度上的提升。同时,我们也可以保证对伴生进程的控制性,当有升级需求的时候,可以一定程度上对他们直接进行升级,而不会影响到业务方的迭代。最后一种是 UDF 分流。这是针对大数据的离线场景下,我们需要用实验去做一些分析和处理,所以会用到离线和流式处理。
这边简单总结一下 火山引擎会更愿意去用 RPC 进行分流的几个理由 :
- 第一是从未停止的高速迭代,我们要求对服务有比较强的控制,需要随时去更新它,去迭代它。
- 第二,多样化的接入场景、功能拼图和服务端技术栈,这些需求会导致在 SDK 这个场景下,成本会略高一些。
- 第三,我们也有苛刻的性能要求,即使是在 RPC 这样的方式下,我们也会进行大量非常强度的性能优化,以应对苛刻的性能要求。这里提一下,当前我们 RPC 的性能大约优化到了毫秒级的状态。
既然讲到了从未停止的高速迭代,那么,我们的分流到底都在迭代些什么东西呢?图2-2展示了分流的迭代就是围绕着灵活多变的流量复用与隔离展开的。通俗来讲,它是在解决实验与实验之间关系的问题。
图2-2 灵活多变的流量复用与隔离
- 最简单的实验关系其实就两个:流量的互斥和流量的正交。由于平台侧比较难以在实验开启之前,去界定这两个实验是不是有冲突、需要互斥。如果两个实验会有一些冲突,通常来说业务方在事件设计的时候,就需要知道这两个实验是有冲突的。如果两个实验是互斥的,我们就需要将实验开在同一个实验层上;如果没有互斥,那么实验将开在不同的实验层上,以保持流量的正交。
- 除此之外,还有互斥域的概念,除了开实验和分实验层这两次基础的哈希,我们会做第三层哈希,以此形成流量层之间的强制隔离。
- 下一个话题是父子实验。一般来说两个实验之间要么互斥,要么正交,应该没有什么特殊关系。但是,有一类实验会有比较强的血缘关系。比如在进行多轮迭代时,第一轮迭代后,我们发现这个实验组确实挺好,可以对着这个实验组再开一个子实验,这个子实验也有自己的对照组和实验组,然后在这个实验组可以在原实验的基础上,我继续进行迭代。这样,两个实验之间就出现了联系,子实验的流量必须来源于父实验的某一个实验组。
- 共享对照组这个概念中提到了一个词,叫勤俭节约。有一些场景下,样本实体是用户请求,而每来一个新的请求都是一个新的样本,这个时候做两次哈希是没有太多意义的,我们可以完全让它退化至一次哈希。退化回去之后,我们可以让实验复用同一个对照组,然后让每一个实验都只有实验组。通过这样的方式,在同样的 100% 的流量下可以开启更多的实验。但是,这样的一种实验方式也是有一定的问题的,大家在使用的时候要谨慎。
- 流量轮转。有一些实验,我们不是想看这个实验的收益,而是确定它就是有害的,只是想看这个实验多有害。但是我们要老针对一群用户进行试验,这对他们的使用体验是有影响的。因此,我们会定期去更新哈希种子,让进实验组的用户定期地进行切换。这样的对用户体验上来说还不错,同时我们又可以看到策略到底有多差的影响。
03 高效的数据建设
第二部分,我们来讲一讲数据建设。“天下武功,唯快不破”,平台的数据建设一定要是够快的。如图3-1所示是数据建设的整个链路图。大家可以看到,整个图分成了两个部分,这是因为数据建设实际上是要解决两个主要问题:
- 第一个是离线计算,目的是要通过报告判断策略是否能够上线。
- 第二个是即席查询,这个部分更倾向于对实验多维度即时分析。
图3-1 数据建设的链路图
离线计算的结果是一个看板或者报告,相对比较固化。业务的核心指标在长时间内大概率是不容易发生变化的。但即席查询需要做多个维度的分析,例如链路分析和漏斗分析等。这些分析经常只针对一个实验有效,甚至是实验分析师临时想到的,即时性会比较强。对于这两个问题,我们会用不同的链路去做执行,但基本逻辑都是做累计指标运算。
其实在数据建设中还有第三个需要解决的问题——流失处理 。通常我们需要监控上线的新实验,比如看看三个小时之内,线上的指标的异常情况。如果异常很严重,我们要赶紧关掉实验,并且让相关人员排查不符合预期的原因。
图3-2高效的数据建设
如上所示,图3-2展示了数据建设是如何保证高效性能的:
- 首先,我们要去做数据管道的建设,来保证指标建设的自动化。
- 其次,SQL 调优当指标组太多、业务线复杂的时候,不能都让人工去做,也需要自动化进行。
- 在需求管理方面,专人去对接业务方的效率太低了,自助建设的平台是不可或缺的。
- 数据管理可以保证数据服务,当我们想查询指标之间的关系时,需要通过数据服务来做灵活查询。
- 最后,打造一个开放的平台和生态系统,可以让一个试验平台的能力成指数型的增长。
通过上述的自动化和第三方共建,可以在人力、有限的资源的条件下,保证我们的数据建设在高效的状态。
04 科学的统计分析
第三部分我们聊一下科学的统计分析。“今天你显著了吗?”,显著对于一个实验来说还是蛮重要的指标。首先来说一下实验指标微涨时,到底是波动还是策略收益?我们需要通过假设检验来进行验证。图4-1的两个方框展示了对不同业务场景的假设检验方案。
图4-1 不同业务场景的假设检验方案
这里给大家举一个例子:显著性水平在业界通常会定到 5%,实际上与业务场景相关时,大家可以自己进行调整,例如 1% 或者是 10% 其实都是可以的。而对于 5% 的显著性水平,在 100 个实验里面会存在 5 个假阳性情况,当实验变多的时候情况还是蛮严重的。那么,这个时候我们应该怎么办呢?有聪明的人想到了这样一个话题,实验前的数据能不能在假阳性的问题上带来一定收益?在业界来看,实验前的数据应用方式分三种:
- 第一种是 SeedFinder 以及变体的 PreAA 校验。原理是在采样用户并且分 AB 组的时候,尽量地让 A 组和 B 组之间的误差减小。方法通过衡量两组用户之间的差异,找到差异最小的两组用户进行实际实验。
- 另一种方法是双重差分。在实验之前,两个组之间本身会存在差异,此时我们选择做一个 A 实验,不应用任何策略,这时用户的指标天然就会有一些差异。我们采用双层插空的方法,可以尽量真实地评估策略带来的收益。
- 在字节跳动这边,我们采用第三种方法 CUPED。这种方法通过一些统计方法对实验前的数据进行修正,使修正之后的结果依然可以作为实验数据的无偏估计。同时,我们可以对实验前的数据缩减方差。同样的样本量缩减方差之后,原来因为流量不够而检测不出来的收益就可能被检测出来,实验的假阳性率也可以进一步降低。
05 精细的运维制度
最后讲讲第四部分,精细的运维制度。对于实验平台,我们可能要“像对待孩子细心照料”。如果一个实验平台能够在一家公司里面很好地应用起来,必须自上而下让整个公司都拥有数据驱动的理念,如图5-1所示。
图5-1 数据驱动理念
此外,由于平台不是简简单单就可以使用的,我们还 需要丰富的用户教育和文档建设 。同时,我们需要有积极的用户响应,只有这样才能够保证业务可以做到高效迭代。
其次,为了保证系统的可靠性,我们 需要在平台上做监控。 例如,通过 AALayer 的方式,我们可以监控系统的安全和健康情况。同时,平台需要透明的数据口径,让所有人都能看到每一个指标的口径、数据的来源以及链路是什么样子的。
异常检查部分 ,在我们内部有一个代号叫 A/BDoctor 的模块 ,主要负责异常实验检查和异常流量检查。
- 第一种是 SeedFinder 以及变体的 PreAA 校验。原理是在采样用户并且分 AB 组的时候,尽量地让 A 组和 B 组之间的误差减小。方法通过衡量两组用户之间的差异,找到差异最小的两组用户进行实际实验。
- 另一种方法是双重差分。在实验之前,两个组之间本身会存在差异,此时我们选择做一个 A 实验,不应用任何策略,这时用户的指标天然就会有一些差异。我们采用双层插空的方法,可以尽量真实地评估策略带来的收益。
- 在字节跳动这边,我们采用第三种方法 CUPED。这种方法通过一些统计方法对实验前的数据进行修正,使修正之后的结果依然可以作为实验数据的无偏估计。同时,我们可以对实验前的数据缩减方差。同样的样本量缩减方差之后,原来因为流量不够而检测不出来的收益就可能被检测出来,实验的假阳性率也可以进一步降低。
|分享嘉宾|
王珂
火山引擎 A/B测试研发工程师
目前就职于字节跳动数据平台,负责内部 AB 实验平台的研发,支撑内部各个业务线的 AB 实验需求。