备案 控制台
学习
实践
活动
专区
工具
TVP
写文章

从0到1搭建技术中台之A / B Testing 平台实践

自去年开始,中台话题的热度不减,很多公司都投入到中台的建设中,从战略制定、组织架构调整、协作方式变动到技术落地实践,每个环节都可能出现各种各样的问题。技术中台最坏的状况是技术能力太差,不能支撑业务的发展,其次是技术脱离业务,不能服务业务的发展。前者是能力问题,后者是意识问题。在 本专题 中,伴鱼技术团队分享了从0到1搭建技术中台的过程及心得。

随着伴鱼业务的快速发展,公司内部越来越多的业务团队希望通过更加科学的 AB 实验( 以下简称「实验」)来进行产品方案的决策。起初公司 AB 平台(以下简称「平台」)的技术方案参考借鉴于 google 论文以及业界分享的实践案例,详细设计和实现可参阅:AB 测试平台的设计与实现。平台上线后,通过不断收集用户的使用反馈,总结试验周期内各环节存在的问题,我们归纳出平台下一步需要重点改进的方向,主要包括:

  • 支持客户端的接入渠道:前期只提供了服务端实验的接入渠道,客户端的实验接入成本高。
  • 统一收敛分流逻辑:定向实验的流量过滤逻辑散落在接入方的业务代码中,一方面增加了业务接入的成本,另一方面过滤条件也难以在平台进行直观展示。
  • 优化分流及存储方案:提供多样化的分流方式,摒弃持久化的存储方案,解决同层实验饥饿现象。
  • 打造平台数据闭环:从业务方提出实验需求到进行实验再到结果分析,整个链路的数据都能够在平台上做到统一流转,避免一对一的线下沟通,减少人力成本,提升用户体验。
  • 优化实验结果表达:基于统计学原理,对各项实验结果进行科学的数据分析,并给出相应的量化指标,真实准确地反映实验效果。
  • 支持事件通知机制:实验周期内的关键节点通知责任人,做到实验报告生成、实验关闭等事件的及时感知。
  • 开放 API 能力:支持业务系统快速创建实验,降低人力成本。

为此,我们结合业务特点,对现有的框架进行了重构。目前新版平台已经上线投入使用。

1. 系统设计

1.1. 平台整体架构

以下是平台整体架构示意图:

通过上面这张图,我们可以将平台实现分为以下几个部分: 1、平台接入方(即实验代码实现的位置),我们提供服务端和客户端两种接入渠道。 2、平台内部实现,分为「分流」和「管理」两个部分。分流模块主要是供在线业务调用,通过分流模型,得出分流结果。管理模块则是实验元数据、实验效果数据等信息的管理后台,提供可视化的操作界面。 3、周边数据配套设施,包括实验分流数据的上报、采集,指标数据的聚合计算。涉及指标库以及数据处理等系统。

1.2. 接入方案

接入方式主要有 Service 和 SDK 两种。Service 的方式即每次都通过 Rpc 来获取实验分流结果,有一定的网络开销,同时平台将承受较大的压力,但优点则是具有较低的接入成本以及平台功能的迭代成本。而 SDK 的方式,是把分流模型直接放在接入方实现,平台仅仅是下发影响实验分流结果的元数据,这种方式大大降低了平台承受的压力,但接入成本提升了不少,每种类型的接入方都要实现统一的分流模型,进行统一的功能迭代。最终我们选择采取 Service 的方式来进行方案的落地,接入方 SDK 保持了轻量化的设计。尽管随着时间的推移,平台承载的流量会逐渐增加,如何保障服务自身的稳定以及分流效率的稳定或许会成为平台未来发展的一大挑战,不过这些都是后话了。

1.2.1. 服务端接入

服务端接入无需任何成本,采用公司统一的Rpc Client Adapter(由代码生成服务直接生成) 直接访问实验分流接口,返回分流结果。

1.2.2. 客户端接入

早期我们并没有直接提供客户端的接入渠道,客户端上的实验需要经由服务端配合才可以完成,成本非常的高。考虑到客户端的流量成本以及对分流结果的时效性要求高,平台提供了批量获取特定类型APP(我们的业务存在多种类型的APP,如伴鱼绘本,自然拼读等)上全部进行中的实验分流结果。端在启动的时候会批量拉取各实验的分流结果,并在本地进行存储,页面上的各实验通过 ABE SDK 直接从本地加载结果,并在固定的时间间隔后,再次拉取刷新数据。采用这种方式,分流结果的及时性得不到保障。但从实验的整个生命周期来看,实验效果的产出需要较长的时间周期(我们设定最短的实验周期至少需要持续两周,低于这个时间,实验效果是不显著的),这点时间的延迟是可以忽略的。

1.3. 实验分流

一条实验流量从进入系统到最终分配方案需要经历三个阶段:流量过滤、同层实验分配、实验内部方案分配。这些阶段应当在平台内部分流模块中闭环实现。

1.3.1. 流量过滤

定向实验指对特定的实验流量开展实验,这就涉及到对不满足实验条件的流量进行过滤。

平台支持实验条件可配置。条件可以由四元组构成,包括:「Key」、「Value」、「Operator」和「Type」。Key、Value 分别对应实验的条件字段和期望值,Operator 表示比较算子,Type 表示条件类型,Operator 和 Type 是绑定的。由于 Operator 表达的语义有限(以 “大于” 比较算子来说,两个字符串 A: “1.10.0” 和 B: “1.5.0” ,从普通字符串的语义理解,B > A ,但从版本号的语义理解,则 A > B ),因此增加了 Type 的概念。每种条件类型对应一系列比较算子。绝大部分场景「通用Type」即满足我们的需求。目前通用Type提供了以下几种算子,包括:

  • “=” 算子:等于比较符
  • “!=” 算子:不等于比较符
  • “>” 算子:大于比较符
  • “<” 算子:小于比较符
  • “in” 算子:包含比较符
  • “not in” 算子:不包含比较符

考虑到流量标识 client_id 含义的不确定性以及多样性,平台暂不支持通过 client_id 来定位实验条件字段的条件值(即平台内部打通各类型数据字典系统,如用户画像等)。获取实验分流结果时,实验条件可以抽象成一个 Map ,接入方自行定义,传入即可。

平台对过滤条件逐一判断,不满足条件的流量直接返回兜底方案( fallback )。

1.3.2. 同层实验分配

早期为了追求分流结果的绝对稳定,采用了持久化的方案将结果存储到了 DB 。这种方案实践起来存在多种问题:

存储不可估计,当前绝大部分的实验是针对用户的,client_id 一般为 uid ,存储规模相对可控。但如果实验是基于事件的,client_id 为事件 ID ,则存储规模将剧增。 白名单方案调整受限,数据一经落盘,调整不再受控制。当然也可以针对白名单的分流结果不进行存储,不过终究又是多了一个堆砌的 if 条件。

同层实验分配产生饥饿现象。早期同层实验分配采用「无权重无分桶」的方案,即将同层中进行的每一个实验看做一个桶,对 key ( layer_id + client_id ) 进行 Hash 取模,命中哪个桶就分配至哪个实验,无需担心实验增减导致分配不稳定,一个相同的 key ,只有一次分配机会,结果持久化后,下次分流直接返回结果了。通过这种方式,同层中前期进行的实验可能“霸占”了总体流量中的绝大部分流量。后期实验只能“捡漏”,导致饥饿现象产生。

因此,分流结果持久化的方案需要废弃。结果数据进行日志埋点,最终收集起来,作为实验效果数据的数据源。同时不必担心分流结果不稳定,分流算法可以保证。而对于实验方案调整(增减方案或调整方案流量)等现象,我们增加了「实验版本」的概念,一经调整,实验版本将发生变更,实验进入一个新的阶段(类似于一次新的功能发布上线),实验效果重新计算。

既然废弃了持久化的方案,同层实验分配采用「无权重无分桶」的方案就站不住脚了(依赖持久化,否则随着同层实验的增减,分配结果就不稳定了)。同层实验的分配方案应当是一个可选的集合。随着业务的发展和平台的迭代,将衍生出更多贴近业务的优秀方案。「分桶」的方式是最本质的一种实现方案,设定层的桶的总量(比如1000),实验在设计阶段,指定占用流量的区间大小(平台予以提示,防止一个实验占用过多流量,其他实验无流量区间可以进行试验的情况)。同时过期实验占用的桶,将采用「惰性驱逐」的方式进行清除,即每次新实验在平台创建时,如果准备选择某一层进行试验,在获取该层可用的流量区间时,会进行驱逐操作。「城市分桶」的方案,则是对「分桶」方案进行了更贴近业务的一层抽象,把每个城市看成一个桶,试验可以选择在固定的一些城市进行。

1.3.3. 实验内方案分配

实验内部的方案分配应当同样也是一个可选的集合。目前我们采用的是业界最常用的方式:随机分组( Hash 取模分桶)的方式,对 client_id 加盐后进行哈希取模,结果值进入不同的桶,每个桶归属于一个分组。这种分组方式是否均匀,有待结合后续更多的实验进一步探究。业内也有提出其他类型的分组算法,相信随着时间的推移,更多的算法将被我们在实践中运用起来。

1.4. 数据闭环

实验报告关注的数据(通常指业务指标数据)以及实验报告的数据展现应当统一收敛在平台内部,以达到更好的用户体验。试想,每次做实验,都需要直接和数据同学人肉对接,无论是从成本还是体验方面都很糟糕。而要到达数据闭环,得从以下两个方面着手:

  • 指标服务构建:提供一套公司级的指标服务系统,维护每一个指标元数据信息(包括:统计口径、数据源等)。指标数据可以源于业务中的日志埋点或者是 Mysql 中的 binlog ,经过数据收集、清洗、计算得到。平台通过打通指标系统,获取实验可以订阅的指标。
  • 数据关联:实验数据和订阅的指标数据关联之后,才可以得到我们的实验指标数据。事实上,实验数据自身就是一种指标数据,在每次拿到实验分流结果时,都进行分流结果的日志埋点,通过对这些日志的收集处理便可得到实验数据。值得一提的是,指标数据和实验数据如何进行关联?关联的Key应该是什么?我们以一个简单例子来说明这种情况。产品设计了一个试验(包括方案 A 和 B ),想要关注两种方案下客户订单数的指标。这种场景下,我们可以将数据以 json 形式进行简单地表述:
    1. 实验分流明细数据
{
    "client_id":"123456789", 
    "experiment_key":"TEST",
    "experiment_variant":"A",
}
  1. 订单宽表明细数据
{
    "order_id":"202006301111",
    "uid":"123456789",
    "phone":"18712344321",