伴鱼技术团队

Technology changes the world

伴鱼开放平台 上线了! 源于实践的解决方案,助力企业成就未来!

伴鱼企业级AB平台的持续演进

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

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

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

系统设计

平台整体架构

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

接入方案

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

服务端接入

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

客户端接入

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

实验分流

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

流量过滤

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

平台支持实验条件可配置。条件可以由四元组构成,包括:「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 )。

同层实验分配

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

  • 存储不可估计,当前绝大部分的实验是针对用户的,client_id 一般为 uid ,存储规模相对可控。但如果实验是基于事件的,client_id 为事件 ID ,则存储规模将剧增。
  • 白名单方案调整受限,数据一经落盘,调整不再受控制。当然也可以针对白名单的分流结果不进行存储,不过终究又是多了一个堆砌的 if 条件。
  • 同层实验分配产生饥饿现象。早期同层实验分配采用「无权重无分桶」的方案,即将同层中进行的每一个实验看做一个桶,对 key ( layer_id + client_id ) 进行 Hash 取模,命中哪个桶就分配至哪个实验,无需担心实验增减导致分配不稳定,一个相同的 key ,只有一次分配机会,结果持久化后,下次分流直接返回结果了。通过这种方式,同层中前期进行的实验可能“霸占”了总体流量中的绝大部分流量。后期实验只能“捡漏”,导致饥饿现象产生。

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

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

实验内方案分配

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

数据闭环

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

  • 指标服务构建:提供一套公司级的指标服务系统,维护每一个指标元数据信息(包括:统计口径、数据源等)。指标数据可以源于业务中的日志埋点或者是 Mysql 中的 binlog ,经过数据收集、清洗、计算得到。平台通过打通指标系统,获取实验可以订阅的指标。

  • 数据关联:实验数据和订阅的指标数据关联之后,才可以得到我们的实验指标数据。事实上,实验数据自身就是一种指标数据,在每次拿到实验分流结果时,都进行分流结果的日志埋点,通过对这些日志的收集处理便可得到实验数据。值得一提的是,指标数据和实验数据如何进行关联?关联的Key应该是什么?我们以一个简单例子来说明这种情况。产品设计了一个试验(包括方案 A 和 B ),想要关注两种方案下客户订单数的指标。这种场景下,我们可以将数据以 json 形式进行简单地表述:
    1) 实验分流明细数据

    1
    2
    3
    4
    5
    6
    {
    "client_id":"123456789",
    "experiment_key":"TEST",
    "experiment_variant":"A",
    ...
    }

    2) 订单宽表明细数据

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "order_id":"202006301111",
    "uid":"123456789",
    "phone":"18712344321",
    "price":"100.00",
    "discount":"0.95",
    ...
    }

    要得到每种方案订单数,无疑需要进行 Join 操作。实验分流数据肯定是要以 client_id 作为关联 key 的,其他字段从语义上不具备关联的条件 ,但是由于 client_id 的具体语义系统没有概念(例子中 client_id 其实表示的是 uid,但只有实验 Owner 清楚),因此系统不知道该和订单宽表明细数据中的哪个字段进行关联。这里我们采用的方法是:给每一个指标都增加一个表示主体的字段(其实就是通过这个字段,给指标信息中增加了一层和 client_id 对应的语义),那么以 uid 为主体和以 phone 为主体的用户订单数就是两个不同的指标,尽管二者的其他信息都是一样的。这样一来,实验 Owner 在订阅指标的时候,就应该选择与 client_id 语义一致的指标了。

数据分析与结论

通常使用统计学中假设检验的理论来分析实验指标数据。实验组的效果是相对于对照组而言的,因此在查看实验报告时,首先需要选择对照组。

  • 显著性水平( p-value ):就是当原假设(对照组和实验组实验效果没有差异)为真时所得到的样本观察结果或更极端结果出现的概率。如果 P 值很小,说明原假设情况的发生的概率很小,而如果出现了,根据小概率原理,我们就有理由拒绝原假设,P 值越小,我们拒绝原假设的理由越充分。p-value 表示的是实验组相较于对照组统计的显著性(即二者是否存在差异),它并不能告诉我们实验组比对照组有多少提升。
  • 置信区间:实验组相对于对照组的变化,可以使用 $$ \frac{(Value_实-Value_对)*100\%}{(Value_对)} $$ 表示,这个是点估计,它是有误差的。针对统计显著的实验组,我们需要区间估计的方法来得到一个概率范围,以准确地描述实验效果。通常使用 95% 的置信水平来进行区间估计。

实验效果的科学系分析是一个很大的话题,目前我们采用以上两种常用的方式,对实验指标数据给予定性结论。当然结论的可靠性还受实验样本数、实验样本随机性等因素的影响。通常,为了进一步验证实验结论的可靠性还可以采取「AA测试」等方法进行前置验证。此外,一个实验往往可能会订阅多项指标,指标数据并非表现出一致效果,需要整体的权衡和考虑。

index_example

事件通知

实验的生命周期我们总结为以下几个阶段:

  • 实验设计阶段:从平台创建实验开始,至业务代码接入并发布上线。实验 Owner(可以是产品或者业务研发同学)需要明确实验的目的,仔细设计实验方案及流量配比,综合考量同其他实验的相互影响以及订阅实验需要观测的指标。
  • 流量接入阶段:从线上分流请求进入平台开始,至一定的实验效果观测周期为止。此阶段,实验元数据(实验方案、方案流量配比等)应保持稳定,一经调整效果数据将进入新的阶段,观测周期重新开始。
  • 效果产出阶段:经过观测周期后,实验结论便产生了,可好可坏,需要实验 Owner 决策实验的下一步计划。
  • 实验关闭阶段:实验关闭后,需要业务研发同学及时清理实验,避免不断堆积的废弃代码。实验关闭阶段,实验分流结果将默认为兜底方案,不再支持灰度放量,以提升业务研发同学对关闭实验后的代码进行处理的意识。

在以上每个实验阶段的初始节点,通过发送事件,提醒实验 Owner 执行相关的操作。

开放API

为了满足业务多样化的需求,降低试验创建的人工成本。提供开放API的能力,可以支持业务通过脚本、代码(内嵌于上层业务系统)等方式直接创建或修改实验。

未来工作

随着平台在公司内部的继续推广,问题的暴露势必会越来越多。目前可预见的需要加强完善的几个方向:

  • 服务稳定性保障、优化分流效率
  • 更多样化的分流算法,提升实验科学性
  • 更健全、更科学的实验分析手段
  • 内部指标体系深化建设

欢迎关注我的其它发布渠道