马蜂窝ABTest多层分流系统的设计与实现
什么是 ABTest
产品的改变不是由我们随便「拍脑袋」得出,而是需要由实际的数据驱动,让用户的反馈来指导我们如何更好地改善服务。正如马蜂窝 CEO 陈罡在接受专访时所说:「有些东西是需要 Sense,但大部分东西是可以用 Science 来做判断的。」
说到 ABTest 相信很多读者都不陌生。简单来说,ABTest 就是将用户分成不同的组,同时在线试验产品的不同版本,通过用户反馈的真实数据来找出采用哪一个版本方案更好的过程。
我们将原始版本作为对照组,以每个版本进行尽量是小的流量迭代作为原则去使用 ABTest。一旦指标分析完成,用户反馈数据表现最佳的版本再去全量上线。
很多时候,一个按钮、一张图片或者一句文案的调整,可能都会带来非常明显的增长。这里分享一个ABTest 在马蜂窝的应用案例:
如图所示,之前我们交易中心的电商业务团队希望优化一个关于「滑雪」的搜索列表。可以看到优化之前的页面显示从感觉上是比较单薄的。但是大家又不确定复杂一些的展现形式会不会让用户觉得不够简洁,产生反感。因此,我们将改版前后的页面放在线上进行了 ABTest。最终的数据反馈表明,优化之后的样式 UV 提高了 15.21%,转化率提高了 11.83%。使用 ABTest 帮助我们降低了迭代的风险。
通过这个例子,我们可以更加直观地理解 ABTest 的几个特性:
-
先验性:采用流量分割与小流量测试的方式,先让线上部分小流量用户使用,来验证我们的想法,再根据数据反馈来推广到全流量,减少产品损失。
-
并行性:我们可以同时运行两个或两个以上版本的试验同时去对比,而且保证每个版本所处的环境一致的,这样以前整个季度才能确定要不要发版的情况,现在可能只需要一周的时间,避免流程复杂和周期长的问题,节省验证时间。
-
科学性:统计试验结果的时候,ABTest 要求用统计的指标来判断这个结果是否可行,避免我们依靠经验主义去做决策。
为了让我们的验证结论更加准确、合理并且高效,我们参照 Google 的做法实现了一套算法保障机制,来严格实现流量的科学分配。
基于 Openresty 的多层分流模型
大部分公司的 ABTest 都是通过提供接口,由业务方获取用户数据然后调用接口的方式进行,这样会将原有的流量放大一倍,并且对业务侵入比较明显,支持场景较为单一,导致多业务方需求需要开发出很多分流系统,针对不同的场景也难以复用。
为了解决以上问题,我们的分流系统选择基于 Openresty 实现,通过 HTTP 或者 GRPC 协议来传递分流信息。这样一来,分流系统就工作在业务的上游,并且由于 Openresty 自带流量分发的特性不会产生二次流量。对于业务方而言,只需要提供差异化的服务即可,不会侵入到业务当中。
选型 Openresty 来做 ABTest 的原因主要有以下几个:
整体流程
在设计 ABTest 系统的时候我们拆分出来分流三要素,第一是确定的终端,终端上包含了设备和用户信息;第二是确定的 URI ;第三是与之匹配的分配策略,也就是流量如何分配。
首先设备发起请求,AB 网关从请求中提取设备 ID 、URI 等信息,这时终端信息和 URI 信息已经确定了。然后通过 URI 信息遍历匹配到对应的策略,请求经过分流算法找到当前匹配的 AB 实验和版本后,AB 网关会通过两种方式来通知下游。针对运行在物理 web 机的应用会在 header 中添加一个名为 abtest 的 key,里面包含命中的 AB 实验和版本信息。针对微服务应用,会将命中微服务的信息添加到 Cookie 中交由微服务网关去处理。
稳定分流保障:MurmurHash算法
分流算法我们采用的 MurmurHash 算法,参与算法的 Hash 因子有设备 id、策略 id、流量层 id。
MurmurHash 是业内 ABTest 常用的一个算法,它可以应用到很多开源项目上,比如说 Redis、Memcached、Cassandra、HBase 等。MurmurHash 有两个明显的特点:
-
快,比安全散列算法快几十倍
-
变化足够激烈,对于相似字符串,比如说「abc」和「 abd 」能够均匀散布在哈希环上,主要是用来实现正交和互斥实验的分流
下面简单解释下正交和互斥:
-
互斥。指两个实验流量独立,用户只能进入其中一个实验。一般是针对于同一流量层上的实验而言,比如图文混排列表实验和纯图列表实验,同一个用户在同一时刻只能看到一个实验,所以他们互斥。
-
正交。正交是指用户进入所有的实验之间没有必然关系。比如进入实验 1 中 a 版本的用户再进行其它实验时也是均匀分布的,而不是集中在某一块区间内。
流量层内实验分流
流量层内实验的 hash 因子有设备 id、流量层 id。当请求流经一个流量层时,只会命中层内一个实验,即同一个用户同一个请求每层最多只会命中一个实验。首先对 hash 因子进行 hash 操作,采用 murmurhash2 算法,可以保证 hash 因子微小变化但是结果的值变化激烈,然后对 100 求余之后+1,最终得到 1 到 100 之间的数值。
示意图如下:
实验内版本分流
实验的 hash 因子有设备 id、策略 id、流量层 id。采用相同的策略进行版本匹配。匹配规则如下:
稳定性保障:多级缓存策略
刚才说到,每一个请求来临之后,系统都会尝试去获取与之匹配的实验策略。实验策略是在从后台配置的,我们通过消息队列的形式,将经过配置之后的策略,同步到我们的策略池当中。
我们最初的方案是每一个请求来临之后,都会从 Redis 当中去读取数据,这样的话对 Redis 的稳定性要求较高,大量的请求也会对 Redis 造成比较高的压力。因此,我们引入了多级缓存机制来组成策略池。策略池总共分为三层:
第一层 lrucache,是一个简单高效的缓存策略。它的特点是伴随着 Nginx worker 进程的生命周期存在,worker 独占,十分高效。由于独占的特性,每一份缓存都会在每个 worker 进程中存在,所以它会占用较多的内存。
第二层 lua_shared_dict,顾名思义,这个缓存可以跨 worker 共享。当 Nginx reload 时它的数据也会不丢失,只有当 restart 的时候才会丢失。但有个特点,为了安全读写,实现了读写锁。所以再某些极端情况下可能会存在性能问题。
第三层 Redis。
从整套策略上来看,虽然采用了多级缓存,但仍然存在着一定的风险,就是当 L1、L2 缓存都失效的时候(比如 Nginx restart),可能会面临因为流量太大让 Redis 「裸奔」的风险,这里我们用到 lua-resty-lock 来解决这个问题,在缓存失效时只有拿到锁的这部分请求才可以进行回源,保证了 Redis 的压力不会那么大。
我们在缓存 30s 的情况下对线上数据的进行统计显示,第一级缓存命中率在 99% 以上,第二级缓存命中率在 0.5 %,回源到 Redis 的请求只有 0.03 %。
关键特性
-
吞吐量:当前承担全站 5% 流量
-
低延迟:线上平均延时低于 2ms
-
全平台:支持 App、H5、WxApp、PC,跨语言
-
容灾:
-
自动降级:当从 redis 中读取策略失败后,ab 会自动进入到不分流模式,以后每 30s 尝试 (每台机器) 读取 redis,直到读取到数据,避免频繁发送
-
请求手动降级:当出现 server_event 日志过多或系统负载过高时,通过后台「一键关闭」来关闭所有实验或关闭 AB 分流
性能表现
响应时间分布
TPS 分布
测试工具采用 JMeter,并发数 100,持续 300s。
从响应时间来看,除了刚开始的时候请求偏离值比较大,之后平均起来都在 1ms 以内。分析刚开始的时候差距比较大的原因在于当时的多级缓存里面没有数据。
TPS的压测表现有一些轻微的下降,因为毕竟存在 hash 算法,但总体来说在可以接受的范围内。
A/B发布
常规 A/B 发布主要由 API 网关来做,当面临的业务需求比较复杂时, A/B 发布会通过与与微服务交互的方式,来开放更复杂维度的 A/B 发布能力。
小结
需要注意的是,ABTest 并不完全适用于所有的产品,因为 ABTest 的结果需要大量数据支撑,日流量越大的网站得出结果越准确。通常来说,我们建议在进行 A/B 测试时,能够保证每个版本的日流量在 1000 个 UV 以上,否则试验周期将会很长,或很难获得准确(结果收敛)的数据结果推论。
要设计好一套完整的 ABTest 平台,需要进行很多细致的工作,由于篇幅所限,本文只围绕分流算法进行了重点分享。总结看来,马蜂窝 ABTest 分流系统重点在以下几个方面取得了一些效果:
-
采用流量拦截分发的方式,摒弃了原有接口的形式,对业务代码没有侵入,性能没有明显影响,且不会产生二次流量。
-
采用流量分层并绑定实验的策略,可以更精细直观的去定义分流实验。通过和客户端上报已命中实验版本的机制,减少了服务数据的存储并可以实现串行实验分流的功能。
-
在数据传输方面,通过在 HTTP 头部增加分流信息,业务方无需关心具体的实现语言。
近期规划改善:
-
监控体系。
-
用户画像等精细化定制AB。
-
统计功效对于置信区间、特征值等产品化功能支持。
-
通过 AARRR 模型评估实验对北极星指标的影响。
这套系统未来需要改进的地方还有很多,我们也将持续探索,期待和大家一起交流。
本文作者:李培,马蜂窝基础平台信息化研发技术专家;张立虎,马蜂窝酒店研发静态数据团队工程师。
(马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,谢谢配合。)
关注马蜂窝技术,找到更多你需要的内容