简介
这篇博文描述了我们如何对 Uber 的实验平台进行效率改进,将实验评估的延迟从毫秒级降低到微秒级,提升了100倍。我们通过从远程评估架构(客户端到服务器的 RPC 请求)转向本地评估架构(客户端计算)来实现这一目标。本文中的一些术语(如参数、实验等)参考自我们之前关于 Uber 实验的博文。要了解更多信息,请查看 在 Uber 提升 A/B 测试效率[1] 。
实验在 Uber 的后端微服务、移动应用和网页界面中被广泛使用,用于衡量所有业务部门(外卖、出行、货运、Uber For Business (U4B)等)产品发布的改进效果。我们将介绍系统的当前和新架构,以及技术挑战和经验教训。本文特别关注后端 Golang 微服务的延迟改进,而不是基于移动或网页的实验。我们也主要关注 A/B 测试,而不是其他实验设计。
背景
2020年,Uber 开始了重写 A/B 测试平台的漫长旅程(内部称为 Citrus 项目)。这个新系统的一个关键架构决策是将 Uber 现有的后端配置平台 Flipr[2] 与实验平台统一起来。我们将实验构建为 Flipr 配置(也称为参数)之上的临时覆盖层,以执行随机化、进行因果推断,并帮助团队确定参数的最佳表现值。
下面是一个说明参数和实验区别的例子。这里我们有一个参数,它控制乘客应用程序特定屏幕上的按钮颜色。默认值是红色,但在某些情况下(例如,如果用户在 city_id = 1 ),该城市的_所有用户_的值将是蓝色。
图 1: 参数表示。
这些参数规则引擎使用 Flipr 客户端进行评估:
图 2: Flipr 客户端参数评估。
对 get 方法的调用会根据传入的运行时参数(例如 city_id、user_id、app_version 等)返回不同的参数值。
[继续翻译剩余内容...]
图10:中间架构:参数服务中的影子评估。
经过彻底的影子测试过程,我们发现并解决了13个不同的bug,这个旅程的最后一步是允许团队直接选择使用本地实验评估功能,完全移除对PrefetchParameters API的使用。
图11:最终架构:后端微服务直接使用本地实验评估。
挑战与经验
1) 大规模验证
构建这个新的本地评估功能的主要挑战之一是确保其在大规模下正常运行。为此,我们实现了影子测试功能,确保对V1遗留评估代码路径的请求采样也会触发一个单独的异步goroutine来评估并与V2本地实验评估代码路径进行结果比较。
这种评估的规模非常大(大约每秒2000万次评估),需要大量采样以防止参数服务在执行这些比较时出现性能下降。此外,评估类型在后端、移动和网页界面之间差异很大。因此,我们发现有必要证明我们遇到的每个不匹配都是可以解释的。最终我们实现了>99.999%的匹配率(平均每10万次评估中有1个可解释的差异)。
图12:影子评估匹配率。
从这个过程中获得的一些关键经验包括:
参数或实验更新期间的已知差异 - 当用户更新参数或实验的值或规则引擎时,这会可预见地导致竞争条件。V1评估方法可能使用参数或实验的版本X,而使用V2方法的影子比较可能基于Flipr分发的时间使用版本X+1。这成为系统中唯一可接受的差异来源。
旧方法中的bug - 虽然与旧系统100%一致是目标,但有时我们决定"向前修复",在新系统而不是旧系统中解决次要bug和边缘情况。这证明是此次迁移的一个主要好处:除了提供新功能外,我们还能够修复遗留系统中难以检测的边缘情况。
竞争条件和超时 - 类似于参数或实验更新期间的"已知差异"问题,我们意识到一些微妙的竞争条件可能会影响我们的影子评估匹配率。一个例子是为影子评估设置适当的超时 - 它应该模仿提供给原始评估的相同上下文截止时间,不仅是为了逻辑正确性,还为了性能的苹果对苹果比较。
2) 日志量问题
评估速度的显著提高带来了实验日志生成和处理的风险。评估正在进行实验的参数可能会向Apache Kafka®生成曝光日志。如果参数的评估速度提高100倍,这意味着日志的生成速度也可能提高100倍,并且存在可能压垮生成、分发和处理日志的基础设施的风险。特别是,我们担心由于达到Kafka主题的速率限制(字节速率或消息速率吞吐量限制)而丢失实验日志。
一个常见的实验用例是批量处理大量用户群体,比如为涉及数亿用户的电子邮件营销活动生成营销文案。
为了降低这个风险,我们采取了以下措施:
日志量生成的遥测和告警 - 首先,我们添加了遥测来了解每个客户端每秒生成了多少实验日志,如果其中任何一个生成的日志超过我们Kafka主题总配额的5%,就会向我们的值班团队发出警报。这使我们能够识别热点并构建解决方案来解决它们。
内存LRU缓存 - 由于我们的实验日志处理管道只关心每个用户首次进入实验的情况,我们能够去重大量日志。我们决定使用高性能的内存LRU缓存( golang-lru[3] )来减少冗余日志,最终能够去重大约80%的日志。
Flipr客户端库中的参数 - 在Flipr客户端库和ExperimentPlugin中添加"内部"参数的能力对于安全开发功能变得至关重要。为了避免循环依赖,我们有一个单独的机制在文件缓存引导时加载这些"内部参数",而不是提供运行时上下文来评估它们。在紧急情况下,我们能够使用这些内部Flipr客户端参数完全禁用本地评估,而无需重新部署服务。
3) 分散评估
最后,我们必须面对的一个新挑战是实验评估现在作为客户端库的一部分分散进行,而不是单个集中式微服务。基于RPC的评估的一个主要好处,特别是在Citrus项目的初始开发阶段,是任何核心评估逻辑的问题都可以通过重新部署单个微服务轻松回滚。我们建议其他人只有在平台处于成熟状态时才转向客户端评估,因为回滚涉及单独版本客户端库的多个单独微服务是一个更加昂贵和耗时的工作。
帮助我们应对这种范式转变的一些工具包括:
- 客户端库版本跟踪 - 尽管我们的后端微服务定期部署,但并不总是每个Uber的微服务都使用最新的客户端代码更改。客户端版本跟踪遥测使我们能够更容易地识别使用过时或有bug版本SDK的服务。
图13:客户端库版本采用曲线
- 批量部署基础设施 - Up[4] 是Uber的平台,管理着数百万主机实例上数千个微服务的编排和代码部署。我们能够在最坏情况下(例如,发布了有问题的客户端版本)添加批量部署服务的功能。
影响
最终,新架构对Uber的许多用例来说都证明是成功的。实验评估的p99延迟降低了100倍(从10毫秒降至100微秒)。
图14:实验评估p99延迟。
在2023年下半年首次发布此功能后,我们战略性地针对贡献实验评估最多的Go微服务进行推广,目前已覆盖100多个服务和近70%的实验流量。
图15:本地实验评估采用KPI。
此外,我们能够观察到Uber应用程序中面向用户功能的一些明显改进。例如,UberEats搜索建议索引的端到端后端延迟降低了20%(p99延迟从250毫秒降至200毫秒),这转化为移动应用体验的显著改善。
图16:本地实验评估对UberEats商店索引服务的影响。
下一步
本地实验评估的第一个迭代是针对使用Golang的服务,这服务于我们大部分的后端实验。下一个直接的步骤是也包括Java,这是Uber支持的另一个主要后端服务语言。
除此之外,还有计划和正在进行的工作,将复杂性从客户端转移到服务器端。复杂的客户端比服务器端更难推广,在两种或更多语言中拥有复杂的客户端会增加工作重复和差异风险。
更具表现力的语言将允许我们将一些复杂性转移到服务器。系统可以在服务器上解析实验规则引擎,并输出更简单的程序,由Golang和Java客户端执行,而不是客户端使用ExperimentPlugin模式为Golang和Java解析实验规则引擎。这将导致更精简的客户端和更多的服务器端控制。这些细节将在未来的博客文章中讨论。
结论
总之,我们用一个新功能替换了基于RPC的实验系统,该功能通过读取分发到所有主机代理的文件缓存来本地评估实验规则引擎。为实验堆栈添加本地评估帮助改进了以下方面:
- 延迟: 评估延迟降低了100倍(p99从10毫秒降至100微秒),解锁了新的集成机会
- 可靠性: 我们移除了单个高流量微服务的单点故障,用更可靠的分布式配置管道替代它
- 开发者生产力: 开发者不再需要担心在批量请求中预取参数并缓存它们以避免RPC开销,使代码更简单
我们从这个计划中学到了很多经验,这对于试图构建内部实验平台或对现有平台进行大规模更新的其他人可能有用:
- 从集中式开始 - 建议创建一个新的实验平台,通过RPC调用集中式微服务返回评估结果,特别是在初期快速开发新功能期间
- 成熟时去中心化 - 当平台成熟时,切换到分散式库架构可以解锁许多好处(如上所列)
- 安全开发库 - 为了使(分散式)库开发更安全,利用客户端库内的轻量级功能标记、强大的遥测和告警,以及跟踪微服务之间的库版本使用等概念很重要
- 更快的评估可能意味着更快的日志生成 - 在转向客户端、低延迟评估架构时,确保实验日志基础设施能够处理增加的容量至关重要
参考链接
- 在 Uber 提升 A/B 测试效率: https://www.uber.com/blog/supercharging-a-b-testing-at-uber/
- Flipr: https://www.uber.com/blog/flipr/
- golang-lru: https://github.com/hashicorp/golang-lru
- Up: https://www.uber.com/blog/up-portable-microservices-ready-for-the-cloud/?uclick_id=b017bf4f-abd9-415e-aee6-3540742d49ad
- Pixabay: https://pixabay.com/illustrations/alchemical-alchemy-magician-witch-8848871/