[大厂实践] Uber 如何实现端到端测试左移
本文介绍了 Uber 在尝试将端到端测试左移的过程中开发的 BITS 系统和测试架构,讨论了对于测试颗粒度的取舍,对于任何一个致力于优化产品测试流程的团队都有很好的借鉴意义。原文:Shifting E2E Testing Left at Uber
若干年前,Uber 主要依靠增量部署以及生产环境探针/告警来发现捕捉问题。这种方法虽然合理,但运维开销非常昂贵,而且很容易遗漏问题。检测到问题后,需要开发人员能准确隔离并修复造成问题的变更,然后再执行整个流程。
图 1:尽早发现问题(即将检测左移)可以减少上述运维负担。
对许多造成服务中断的问题进行事后分析,发现除了几个基本的单元测试之外,几乎没有测试,而且测试非常依赖模拟组件,因此很难理解实际能够提供多少保护。
Uber 以完全采用微服务架构而闻名,核心业务逻辑被封装在大型微服务组中,因此很难在不过度模拟的情况下验证单个服务的功能。基于这样的体系架构,唯一完备的测试方法就是执行端到端测试。同时,内部 NPS 调查一直强调端到端测试是开发工作中最难的部分,因此经常被跳过也就不足为奇了。
2015 年有一篇著名的文章 对更多的端到端测试说不 指出,端到端测试难以维护、编写成本高、不稳定、速度慢、难以调试。Uber 的测试之旅同样遇到了上述所有问题,因此我们不得不想出创造性方法来解决。
本文将介绍我们如何构建测试系统,将所有代码和配置更改都与核心后端系统(1000 多个服务)隔离。我们有几千个端到端测试,每次测试的平均通过率为 90%以上。想象一下,这些测试中的每一个都经历了真实的端到端用户流程,就像真实的 Uber Eats 订单一样。我们做得足够快,可以在落地前跑完每一个变更。
旧方案
Docker Compose
过去,Uber 的某些团队曾尝试在本地启动所有相关服务和数据存储,以运行一套针对特定技术栈的集成测试。在微服务的复杂性达到一定程度时,随着需要启动的容器数量越来越多,这将变得非常昂贵。
图 2:早期利用 docker compose,但随着微服务的激增而无法扩展。
部署到预发环境
图 3:一旦在单机上达到运行 docker-compose 的极限,一些人就转向将代码部署到共享的预发或测试环境中以运行测试。
在 Uber,共享平台很难保证能够可靠的用于测试,因为一旦某个开发人员部署了一个糟糕的变更,就可能会影响到其他所有人的测试。这种方法只适用于可以在几个开发团队之间进行人工协调的小范围测试。
端到端测试真的能在公司级别的规模上发挥作用吗?下面就是我们如何实现这一目标的故事。
基于 BITS 的左移 —— Uber 当前的测试策略
利用 BITS 对生产环境进行切片
为了测试左移,需要能够即使不不部署到生产环境,也能测试变更。为了解决这个问题,我们在全公司范围内启动了名为 BITS(Backend Integration Testing Strategy,后端集成测试策略)的计划,它支持按需部署和路由到测试沙箱。实际上,这意味着单个提交可以在部署前并行测试,并且不会干扰其他任何人!
数据隔离
为了安全的左移测试,需要在生产和测试流量之间提供隔离。在 Uber,生产服务预计将同时接收测试和生产流量。
用户上下文
大多数 API 都是实体范围的,这意味着访问范围仅限于特定实体(例如用户帐户)。使用测试用户会自动限制该帐户的副作用范围。少数跨实体交互(如算法匹配或 Eats 购物)使用以下策略进行隔离。
图 4:在 Uber,请求被标记为“租户”标识符,服务通常遵循以下方法之一:
存储客户端将测试流量路由到逻辑上分离的数据存储。
数据保存到带有租户列的生产数据库;基于范围的查询还传递一个用于过滤的租户标识符。
架构
BITS 体系架构可以表示为一系列工作流,在不同基础设施组件之间进行编排,为开发人员定义有意义的体验。BITS 基于 Cadence,一个由 Uber 开发的开源工作流引擎,为我们提供了用于表达重试、工作流状态和资源销毁定时器的编程模型。
CI 工作流运行测试/服务选择算法、构建和部署测试沙箱、安排测试、分析并报告结果。配置工作流控制容器资源利用率和跨不同 CI 队列的工作负载调度,这些队列针对特定工作负载类型进行了优化(例如,高 IO 负载和高计算负载等)。
图 5:BITS 架构组件。
我们之前还讨论过 SLATE,这是一个建立在 BITS 基础架构原语之上的姊妹项目,用于手动执行回归测试。
基础设施隔离
我们做了大量工作来正确隔离 BITS 的开发容器,避免在生产环境造成副作用。
BITS 测试沙箱不接收任何生产环境流量。
Uber 的原生 Apache Kafka 协议通过一个消费者代理进行抽象,该代理通过 gRPC 将消息重新映射到基于推送的转发模型中。这种设计允许将消息智能路由到 BITS 测试沙箱,还可以防止测试沙箱无意中拉取生产环境的消息。
测试时创建的 Cadence 工作流被隔离在测试沙箱内。
指标和日志被标记为租户,因此可以被过滤掉。
测试沙箱不参与生产环境领导者选举协议。
SPIFFE™ 和 SPIRE™ 为 BITS 沙箱和测试流量提供了强有力的身份证明。
Uber 的转发代理检查请求,如果检测到路由覆盖,则执行有条件的 P2P 重定向。
图 6:将 routing-override 编码为上下文包,并在 RPC 和客户端中间件中实现 OpenTelemetry™ 协议,以便跨微服务传播。
测试配置变更
在 Uber,多达 30% 的事故是由配置变更引起的,因此 BITS 还为我们最大的后端配置管理系统 Flipr 提供了测试覆盖。
图 7:如何基于 BITS 测试配置变更。
测试管理
开发人员能够通过 UI 实时管理测试套件,以执行停机测试、跟踪运行状况以及检查服务的端点覆盖率等操作。
图 8:如何在 BITS UI 中管理测试。
跟踪索引
每个测试执行都基于 Jaeger™ 进行强制采样。在每次测试执行之后,我们获取“跟踪”并构建若干索引:
测试流覆盖了哪些服务和端点
哪些测试覆盖了每个服务和端点
图 9:如何使用跟踪索引来配置测试。
这些索引用于为团队提供端点覆盖度量,但更重要的是,允许我们在给定服务更改时智能的确定需要运行哪些测试。
图 10:如何使用跟踪索引显示端点覆盖率。
在测试问题中,获得正确的基础设施一直是比较容易的部分。机器问题可以解决,但 Uber 的开发人员对生产力和体验的下降非常敏感。下面是我们在减少测试维护开销、提高可靠性以及提高信号可信度方面的一些策略。
挑战
挑战 #1:维护、状态和工具管理
假设想要测试车辆调度,即司机在完成当前的订单之前收到新的请求。或者拼车,即司机可能会也可能不会在去目的地的路上搭载另一个乘客。在每种情况下应该如何表现出行状态?
可组合测试框架(CTF,Composable Testing Framework)
我们构建的一个基本构建块是 CTF ,这是一个代码级 DSL ,提供了用于组合单个操作和流的编程模型,并对其中心化出行状态的副作用进行建模。CTF 最初是为了验证 Uber 的履约重组架构而开发的,后来被整个公司采用。
图 11:如何基于 CTF 定义出行流测试。
测试目录(Test Cataloging)
每个测试用例都会自动注册到部署对应的数据存储中。跟踪测试稳定性的开发人员可以获取并分析所有历史通过率、常见故障原因和所有权信息。对常见故障进行汇总,可以更方便对问题进行调查。
图 12:汇总常见的失败测试。
挑战 2:可靠性和速度
测试者每次尝试的测试通过率 ≥90%(相对于主线),平台通过重试将其提高到 99.9%。
我们最大的服务有 300 个测试,如果每个测试在每次尝试中通过率是 90%,则有 26% 的可能性由于不确定性而导致测试失败。在这种情况下,我们让 300 次测试以 95% 的通过率完成,并通过重试将其进一步提高到 ≥99%。
以上内容每天重复数千次,戳破了端到端测试不稳定的神话。
延迟
我们的测试直接基于 Uber API,所以大多数测试的运行时间都不到一分钟。
挑战 3:可调试性和可操作性
执行安慰剂组
在每次测试运行中,我们并行执行最新主分支版本代码的沙盒,作为“安慰剂组”。这两种执行的组合允许我们基于二进制信号构造如下真值表:
图 13:由 BITS 安慰剂组计算的真值表。
基于重试,很少有不能确定结果的情况。要么是产品或测试被破坏了,要么是引入了一个真正的缺陷。无论如何,开发者都可以进行明确的行动。
自动隔离测试
测试通过率遵循双峰分布 —— 绝大多数测试都非常可靠,只有一小部分测试被破坏(持续失败),而更小的测试子集有一定概率通过。
图 14:Uber 端到端测试状态高层分布。
单个失败的测试可能会阻塞 CI/CD,因此要特别注意防止这种情况。一旦测试通过率低于安慰剂组的 90%,就会被自动标记为非阻塞,并自动向所属团队提交工单和告警。考虑到对业务的重要性,核心流和合规性测试被明确标识为特例。
结论
最后,测试需要在质量和速度之间取得平衡。调优后,开发人员花在回滚部署和解决问题上的时间更少,速度和质量都提高了。作为测试工作的一部分,我们跟踪了每 1000 个变更引入的问题,在 2023 年这一数字减少了 71%。
测试既是技术问题,也是人的问题
微服务架构的诞生是出于在一个不断发展的工程组织中赋予独立团队权力的愿望。完美情况下,应该在任何地方都有干净的 API 抽象来加强这个模型。团队能够在不影响其他团队的情况下构建和测试。但实际上,Uber 发布大多数功能都需要大量合作 —— 端到端测试就是一种强制独立团队之间进行沟通的契约,这种契约不可避免会被破坏,因为我们都是人!
测试和架构是内在联系的
在 Uber,没人故意想要严重依赖端到端测试,但微服务密集型架构和多租户的历史架构投资决定了这是最好的解决方案。
我们试图将测试策略与经典“测试金字塔”相匹配,但在 Uber 并不适用。好的测试要求我们批判性思考实际尝试验证的内容,应该尝试测试有意义的用户功能的最小模块集。在 Uber,这通常涉及几十项服务。
致谢
感谢与我们合作的十几个团队,他们使测试成为 Uber 基础设施的一等公民,以及所有相信 BITS 倡议的各位领导者。
你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
版权声明: 本文为 InfoQ 作者【俞凡】的原创文章。
原文链接:【http://xie.infoq.cn/article/cb37ffda94d0aeef7889737b4】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论