Skip to content

Latest commit

 

History

History
82 lines (45 loc) · 9.54 KB

experiment.md

File metadata and controls

82 lines (45 loc) · 9.54 KB

第四节 前端A/B实验设计

简介

前端一直以来都缺少一条简洁而高效的AB实验接入方案,过去大多数前端在接到AB需求,唯一的选择就是调用客户端提供的方法来实现,首先它依赖需要前端开发人员花大量心思去处理AB的逻辑,此外,它的作用范围局限于端内,并不能覆盖前端所有的场景,比如外投,小程序或PC。因此,打造一个前端标准化的简单高效的AB实验体系,势在必行。

一、设计思路

一条完整的AB实验链路到底需要什么?分流,创建实验,工程接入,实验数据回流,但是如果没有把它们组织成体系,那么很难体系价值,更谈不上架构设计,所以我们需要把这些分散的元素尝试归类。

一条完整的AB实验链路其实可以拆分成两部分:

  • 第一部分是实验配置链路,顾名思义它包含了所有与实验配置相关的操作,配置创建、保存、分组,配置的推送或下发,以及使用配置进行实时分流。
  • 第二部分是实验数据链路,它包含实验数据链路的采集,上报,计算,展示(实验报表)。把这两部分拼成一个闭环,也就是我们设计前端AB实验的核心。下面给大家介绍这两个数据模型。

二、实验配置模型

experiment-sdk.png

从上图可以看出,我们将实验配置以前端应用为维度进行组合,然后下发到CDN,前端工程侧则通过实验SDK读取配置,完成分流并渲染相应的业务组件。

前端应用我们可以理解为一个前端工程,比如小程序就是一个前端应用。应用里面包含了场景,一个独立的流量入口我们称之为场景,比如首页就是一个场景,二级页面也是一个场景。场景同时也是分流的模型。

三、实验数据模型

experiment-data.png

从上图我们可以得出,业务是数据指标的集合,并且同一业务下的实验可以创建并关联这个业务域下的所有指标。这些被关联的指标将最终通过数据采集和计算,反映在实验的报表里。

举个实际的例子,在首页(场景)我们想要跑一个实验,AB模块哪个更高效?那么模块的曝光UV和点击UV就是我们要去创建并关联的指标。该指标也会存在于这个业务域下的某个数据集里。说到这里也许有人会疑惑,如果一个业务既是应用又是业务,为啥还要区分这两个概念?为什么不是应用既下发配置又组合指标呢?原因很简单,因为业务是可以跨应用的,比如投放在两个渠道,这里配置下发就要区分,但是业务指标很多却是一样的,这种情况下,一个业务两个应用是一个相对优化的解决方案。

四、实验SDK架构设计

  • 回顾一下上面实验配置模型图中JSSDK的位置,它运行在前端项目中,读取实验配置,实时分流,并根据分流结果返回要渲染的组件。
  • 回归一下实验数据模型,jssdk在数据这部分要做的就是实验数据上报。
  • 第一:它需要上报分流结果;
  • 第二:它需要上报实验所关联的指标数据,即业务要看的实验数据。

experiment-jssdk-struct.png 4-1图示

我们在工程里引入了一个业务AB实验组件,该组件是实验平台根据用户的配置动态生成的,作为(AB实验相关的)业务组件与页面(或父容器)的连接器,其核心工作就是读取AB实验所需的参数并传入sdk,以此触发sdk的整个AB实验逻辑。

在来看jssdk部分(蓝色),首先从全局来看,我们把jssdk拆成了两个包:一个是核心(core)包,封装了通用的核心逻辑如实验配置读取及缓存策略,实验周期控制及分流算法。另一个是接入具体工程的包,如图所示,sdk逻辑层就像是一个AB实验流程的中转站,它实现了一套接口函数,即在web环境下的请求、缓存及cookie解析(分流因子),并将这些接口函数和实验参数一起透传给Core,这么设计的目的是实现Core与逻辑层彻底解耦,这样极大的增加了jssdk的可扩展性。

在拿到实验参数,及所需的接口函数以后,Core要做的工作就是先获取实验配置,此时Core不会直接去请求实验配置,而是触发版本控制策略,该策略主要是检查远程实验配置的版本号是否更新,若有更新才会去请求配置,否则会读取本地缓存的配置。这份本地缓存的配置是用户第一次触发实验时从CDN请求并缓存下来的,之后每次版本更新,才会重新请求并更新缓存,拿到实验配置后,Core会确认当前是否处于实验周期(实验平台配置)内,校验通过后才会正式触发分流算法,然后将分流结果返回给sdk逻辑层。

sdk逻辑层接着会根据分流结果来判断应该展示哪个对应的业务组件,同时它会将分流结果上报给平台,用户此时已经可以在实验的实时数据报表看到分流数据来,技术同学可以通过实时数据来确认实验是否正常触发。另外,埋点组件会对命中的业务组件做一层封装,这里会传入可供业务组件调用的埋点上报方法,具体的调用我们在实验平台创建实验时,就已经生成好了,前端同学是需要将这些上报实验指标的代码部署到相应的业务即可,这一部分埋点数据是T+1的,业务可以在平台侧看到相应的实验报表,并分析实验结果。

五、扩展方案

前端框架比较多,为了尽量覆盖更多的前端场景,在设计上需要考虑jssdk的可扩展性。这也是为什么需要将jssdk拆成core和逻辑两个包,我们的思路是Core封装AB实验的核心逻辑,不依赖任何前端框架,在Core与前端工程之间引入一个逻辑层,来串联起整条链路。这样如下图所示,我们可以通过这样的架构,非常低的成本扩展到主流的web框架(react、vue)、小程序、Node等,同时也具备良好的可维护性。

experiment-extend.png

通过定义一套接口规范,将Core与不同的DSL彻底解耦,然后在逻辑层中根据这个规范实现一系列的接口函数,当Core在执行某段逻辑调用这些函数时,根本无需关心其底层用的是哪个DSL的API。比如对localStorage的操作,React和小程序的API是完全不同的,所以我们在React和小程序的逻辑层中按照接口规范各自实现来这样一套对localStorage的处理函数,并透传给Core。

六、分流模型

6.1 正交与互斥

实验流量模型中我们提到过场景,我们将一个独立的流量入口定义为场景。小程序的首页可以是一个场景,现在基于这个来扩展下,这个首页同时运行来多个AB实验:

  • 实验一:弹窗AB的两种样式,观测指标是两个弹层的点击率;
  • 实验二:页面头部模块AB两种样式,观测指标是模块点击率;
  • 实验三:投放的一个banner的AB两种样式,透不同的操作功能,观测指标是banner点击率;

现在的问题是,实验二和实验三都是页面上的模块,我们希望这两个实验同时运行,但不希望它们互相影响,即进入实验二和实验三的流量必须互斥。如何做到呢?对实验平台来说,场景不仅是一个流量入口,更是一个分流模型,实验在场景里并不是无序放置的,而是通过层(Layer)来进行规范。我们可以这么理解,场景是一个纵向的容器,而层是一个横向的容器,实验则按照一定规则放在不同的层里。如下图,实验一(EXP1)独占一层,它与下一层的EXP2和EXP3是正交关系,即进入EXP1的流量,同时也会进入EXP2或EXP3。我们把EXP2和EXP3放在同一层,因为我们希望进入EXP2的流量不要与进入EXP3的流量重叠。总结为,在一个场景里,层与层之间的实验流量是正交,层内的实验流量互斥。

experiment-layer.png

6.2 实验推全

在上面的模型图中我们还看到一个特殊的层,Launch Layer,这个层用来放被推全的实验分组。比如说,经过线上验证EXP1的A组效果明显优于B组,用户在平台侧将A组推至全量,这时候场景内部结构会发生变化,即EXP1的A组会从原来的layer被放到这个特殊的Launch Layer里,此时该场景内所有触发EXP1的流量将不会再执行分流算法,而是直接返回组A所对应的前端组件。

6.3 jssdk分流

jssdk的分流规则遵循上文提到的分流模型,前端AB实验用到分流因子是访问者浏览器cookie下的一个定义的字段,即该用户的web设备id。我们用这个字段来唯一的标识一个用户,其好处是,可以很好的覆盖端内、端外、无线和PC等各种场景,缺点是,同一个用户用不同的web设备来访问前端应用时,有可能会展示不同的分组结果。此外,web设备id无法对判定用户的人群,因此并不支持圈选特定人群的分流。

结语

前端AB实验还是一块未被充分开垦的土地。我们的应用迭代频繁,但其实我们很少去验证,或者说用科学的方式去验证某一次迭代的真正价值。在大数据崛起的时代,如果我们的每一次迭代不能最终沉淀出数据,而仅仅依靠“经验”恐怕远远不够。这也恰恰是一个很好的实验基地,可以用实验数据去验证迭代,并以此为基础做决策。