Lyft 如何使用负载测试来确保高峰事件期间的可靠服务(译文)

“生产中的负载测试很棒。”

我们知道您在想什么 — 在生产中进行测试是软件开发的主要错误之一。然而,在 Lyft,我们已经意识到生产中的负载测试是一个强大的工具,可以让系统为意外的突发流量和高峰事件做好准备。我们将探讨 为什么 Lyft 需要一个可在生产环境中运行的自定义性能测试框架,我们如何构建跨职能解决方案,以及自 2016 年推出以来我们如何继续改进这个测试平台。

“负载测试”到底是什么意思?在本文的上下文中,我们指的是任何为压力测试系统创建流量并查看它们如何在其容量极限下执行的工具。

即使在需求激增时,Lyft 也必须无缝运行

Lyft 必须在万圣节和新年前夜等高峰活动期间保持高度可用。骑手依靠我们满足他们的出行需求,司机依靠我们谋生,尤其是在一年中最繁忙的日子里。从历史上看,高峰期事件给 Lyft 带来了巨大挑战,因为我们会遇到前所未有的交通流量,与正常工作周相比,这种交通流量通常以不寻常的方式形成。此外,由于 Lyft 的发展速度非常快,流量经常与上一个高峰期时不存在的新服务交互。

在采用模拟之前,我们最初考虑了多种负载测试方法

从历史上看,在这些高峰事件期间我们最大的瓶颈之一是对数据存储的写入操作。我们最初寻求开源和行业标准工具来帮助我们解决这些扩展问题。

录制和回放工具不适用于概率测试

负载测试的行业标准方法之一是使用录制和回放工具,例如 Gatling、K6 和 Bees With Machine Guns。使用这些工具,您可以记录一个会话,编写一些简单的脚本来处理重复的场景,例如登录,然后从预先录制的会话中重播此网络流量。

对于完全确定的服务来说,这是一个很好的解决方案;就像在电子商务中一样,网站通常会允许客户订购更多产品。

像 Lyft 这样的双向市场要棘手得多。请求乘车仅在实际有司机在场时才有效。司机需要与骑手实际匹配 ,并且 位置离骑手足够近才能开始骑行,等等。即便如此,Lyft 的许多系统也不是确定性的——例如,即使司机和乘客距离很近,也不能保证他们会匹配。

捕获和重放生产流量笨重、缓慢且不完整

2016 年,Lyft 推动其主数据库集群突破了 QPS 和连接限制。我们最初的负载测试方法按照您的预期进行:我们开始记录数据库流量并针对不为真实用户提供服务的副本数据库重放它。这使我们能够看到如果我们进一步增加每秒查询数或连接数会发生什么。

这种方法涉及自定义代码,我们很快意识到几个问题——创建和恢复庞大的数据库备份既困难又耗时(这实际上每次都需要我们的供应商提供支持票)。此外,如果出现问题,我们对原因的了解有限。例如,我们是否记录了正确的流量?这种方法的反馈循环也很长,如果在测试过程中出现任何错误,我们就需要从头开始负载测试。

此外,人们对记录/重放模式的未来发展表示担忧。鉴于 Lyft 服务的状态特性,重放流量可能会导致客户被重复计费或被重放保护抓住等问题,并绕过首先导致性能下降的代码路径。

我们考虑过根据分段回放生产流量,但考虑到在这些测试期间扩展分段以匹配生产的成本,并且由于这些系统的高度状态和概率性质,这种方法似乎并没有提供太多好处。此外,暂存环境通常包含大量垃圾或测试数据,这意味着在这种环境中进行测试可能会导致误报或漏报。

鉴于这种记录/重放方法缺乏成功,加上我们越来越担心我们错过了测试数据库之上所有有趣的部分(例如我们的 Envoy 代理和服务网格),我们沿着模拟路径前进。这有助于将我们的注意力从数据存储上移开,转而致力于开发一种产品,该产品将有助于识别在一个或多个服务降级或经历异常高负载时可能发生的级联故障。

我们需要一个超越历史流量回放的性能测试框架

我们创建了一个大规模模拟真实世界高峰场景的系统:SimulatedRides

Lyft如何使用负载测试来确保高峰事件期间的可靠服务,译文

模拟游乐设施架构

我们将想法转向了一个系统,在该系统中,我们将为要测试的场景指定一些设置或配置(我们称之为“模拟”),服务所有者将测量此模拟产生的紧急行为。也就是说,这些模拟的排气可以在我们的各种可观察性系统中提供信号,例如指标、警报、日志等。把这想象成一个“自动化 QA 工程师”,他尝试在一个软件中的所有流程和输入组合,搜索流程组合未按预期执行的边缘情况。

SimulatedRides 服务是一种编排服务。它管理一组客户端,并模拟真实用户在 Lyft 应用程序中可能进行的交互。我们为 SimulatedRides 提供了一个接口,这样它就可以被 Lyft 的服务所有者(其他团队)视为一个平台。这些团队可以选择用一些 Python 代码来贡献“覆盖率”。

有四个关键概念可以理解 SimulatedRides 的工作原理:模拟、客户端、动作和行为

模拟:我们如何定义测试场景

每个模拟都由 DynamoDB 表中的单个条目定义。此配置决定了模拟将如何工作,包括乘客和司机的数量、我们希望在 Lyft 系统中添加和删除这些客户的速度、我们希望他们产生的地理区域等细节. 模拟配置大致如下所示:

{ 
   “名称” : “芝加哥” ,
   “client_configurations” : { 
       “地区” : “芝加哥” ,
       “rider_close_app_after_price_check_percent” : 1 ,
       “rider_cancel_after_accepting_ride_percent” : 10 ,
       “driver_cancel_after_accepting_ride_percent” : 5 ,
   } ,
   “client_composition” : [ 
       { 
           “client_type ” " :  "rider" , 
           "number" :  50 , 
           "behaviors": { 
               “共享乘车” : 25 ,
               “standard_ride” : 65 
               “luxury_ride” : 5 ,
               “luxury_ride_suv” : 5 ,
           } 
       } ,
       { 
           “client_type” : “driver” ,
           “number” : 50 ,
           “behaviors” : { 
               “standard_ride” : 100 
           } 
       } 
   ] ,  
}

我们的模拟由三个关键的巧妙抽象组成,我们将其命名为 Clients、Actions 和 Behaviors。

Lyft如何使用负载测试来确保高峰事件期间的可靠服务,译文

客户、行动和行为如何相互作用

客户端:Lyft 服务网格中的物理设备,例如移动应用程序或电动自行车

客户端是物理设备的编程表示。我们有骑手和司机 iOS/Android 应用程序的客户端,以及自行车和踏板车的客户端,它们具有与我们的服务器通信的车载物联网。像我们的应用程序一样,客户端是有状态的,并且是我们在模拟中写入状态的唯一地方。

我们要求客户端发出的所有请求都使用公共端点,以便尽可能地模仿本机移动应用程序。通过要求我们的模拟不使用任何后门,我们确保它们是真实的,并且在我们的黄金路径上没有任何关键端点未经测试。

行为:用户与设备交互——概率决策树

行为是概率决策树,可以将其视为实际使用客户端(移动应用程序或设备)的用户的编程表示。这种抽象是排列的力量使我们能够发现 Lyft 服务中的意外用例类别,这些用例以前曾导致过分严重且难以发现的事件。例如,我们可以配置司机在与骑手匹配后取消行程的可能性为 5%,或者骑手登录仅查看行程价格并随后在 50% 的时间内关闭应用程序。每引入一种新的可能性,涵盖的可能场景的数量就会呈指数级增长。

行为检查通过操作写入客户端的状态,并概率地选择下一步做什么。例如,刚刚打开 Lyft 应用程序的乘客可以选择更改设置、查看乘车价格、叫车、注销等。此处做出的任何决定都会打开一组新的可能选择,从而揭示行为的树状结构。

将行为视为客户在模拟中执行的“场景”可能更容易。这些行为不遵循一小组有限的端到端场景,而是在流之间按概率分支。因此,我们可以通过我们的模拟配置影响特定结果发生的几率。

由于该系统的复杂性,保持这些流和网络调用的真实性依赖于服务所有者保持集成最新。这种维护既是积极的,也是消极的——它需要手动工作,但它也确保服务所有者积极主动地测试他们的服务和功能。它让 Lyft 改变了工程文化,从对事件的被动处理转变为积极主动。这种转变不是一蹴而就的,需要文档、沟通和教育方面的协调努力。

动作:用户在与设备交互时可能采取的单一动作的结果——几乎总是网络请求

Actions 是 Lyft 工程师为其服务贡献覆盖面的常见位置。客户端执行操作。大多数操作都是单个网络调用,例如“RequestRide”或“AcceptRide”。这意味着只需在 Action 中编写几行代码,服务所有者就可以立即开始向其服务中的任何端点发送合成流量。

操作也是深度可配置的。通过一个简单的配置块,工程师可以指定何时允许调用 Action。例如,我们可以定义客户不得在骑行中才能调用“RequestRide”动作,或者声明有 5% 的概率在任何时候调用“AddCouponForRide”动作时我们实际上执行了业务逻辑载于其中。这种可配置性有助于我们缓慢而有意地将合成流量推出到新端点,并微调流量模式。

动作是系统中唯一可以将状态写入客户端的抽象——例如,动作可以轮询端点以保持当前骑行状态与服务器同步,并且每次轮询时都会将新的骑行信息写入客户端客户。

额外的资源管理器服务公开了一个接口,用于在模拟中使用测试资产

随着 SimulatedRides 采用率的提高及其价值的显现,我们遇到了测试用户的问题。创建和销毁用户、自行车和踏板车等对象的时间成本很高,并且会对下游服务产生级联效应。SimulatedRides 有意不创建和销毁这些参与模拟的对象。为了确保这些测试用户被重置为一致的基线,我们编写了一个单独的服务,称为 ResourceManager 。该系统通过管理固定的“资源”池解决了一个困难的测试数据管理问题——资源是我们模拟所需的测试资产的通用名称,例如用户、自行车、踏板车等。

ResourceManager 公开一个接口来获取、租赁和恢复资源(用户、自行车、踏板车)的健康状况。任何需要测试资产的服务,例如 SimulatedRides 模拟或验收测试,都可以在给定的期限内租用测试资产。一旦不再需要该测试资产或其租约到期,ResourceManager 就会通过“健康恢复平台”运行该资源。这是一个简单的系统,它运行一组检查和修复以确保将这些资源重置为已知的良好状态。例如,这可能会将用户从孤儿游乐设施中移除,确保有效的信用卡已存档,并批准用户在特定地理区域开车。

“健康恢复平台”也被视为一个平台接口,服务所有者可以在其模拟改变底层对象状态的情况下提供新的检查和修复。

该服务可智能扩展以满足大型模拟的需求。

该系统的架构很有趣,并且与 Lyft 大部分采用的 flask/gunicorn 微服务方法完全不同,您可能熟悉更传统的微服务。相反,SimulatedRides 通过工作程序和事件循环模型广泛使用Python 的 asyncio 库,以便在代码中利用并发性。这允许服务扩展到运行大型模拟所需的大流量,同时有效利用计算能力。此外,这种设计意味着我们的模拟工作人员动态地在集群中的所有 pod 之间分配工作以实现负载平衡——运行模拟的任何一台服务器都不会比另一台服务器承担明显更多或更少的工作负载。

我们确实提供了一个兄弟产品,它使用了 SimulatedRides 中的一些相同抽象和代码——我们的确定性测试平台,称为验收测试框架,可以在我们的博客文章中详细阅读这里。

Lyft 使用 SimulatedRides 测试生产和临时环境的性能

生产中的计划负载测试将模拟流量添加到 Lyft 的实时系统,准确揭示它们在真实世界的峰值事件中将如何响应

我们定期进行生产负载测试。在高峰事件发生之前,我们将这些测试的目标转移到看起来,并且在大多数情况下,远远超过即将发生的事件期间的预期流量模式。过去,我们一直以每分钟的乘车下客量来衡量我们的负载测试目标,尽管许多其他指标也会影响我们的预测。我们的生产负载测试按计划运行并且高度自动化。在开始负载测试之前,我们的自动化 slack 机器人会发布一条 slack 消息,其中包含有关预期持续时间的信息、观察测试的链接,以及对随叫随到的负载测试指挥的“@”提示。

负载测试导体是直接负责的人,有权在发生外部事件时停止测试。虽然负载测试是高度自动化的,但我们确实在负载测试期间让人员参与循环,因为失控的负载测试很容易对业务产生重大影响,而人类从业者是复杂系统的适应性元素。

有一个界面允许在这些测试期间微调流量模式的形状和大小。这意味着我们可以运行负载测试,例如,模拟 2019 年万圣节,或为即将到来的活动(如周日超级碗)模拟预测负载。或者,我们可以在一个小区域中产生成千上万的骑手,他们都登录以查看乘车价格,模拟一个事件,比如在新年前夜的午夜钟声刚过,成千上万的人试图离开纽约市的时代广场。

我们的负载测试将模拟用户注入 Lyft 生产系统,逐渐增加到与即将到来的高峰事件的预期流量预测形状非常匹配的目标负载。一旦我们达到目标负载,我们将维持该负载一段时间,在内部称为“浸泡”,然后再次下降并最终从 Lyft 中删除所有模拟用户。

在运行负载测试时,整个组织的待命工程师将收到来自我们强大的可观察性平台的即时反馈。如果负载测试影响了他们系统的健康,这将通过业务指标或 SLO 快速看到,并且适当的团队将对服务中的任何降级或意外行为做出响应,就像他们在真正的峰值事件中所做的那样。有一个重要的区别:与真正的峰值事件不同,负载测试可以应公司中任何个人的请求立即停止。

无需详细说明,我们有严格的系统来确保测试数据不会污染我们的业务指标。

暂存中的持续加载可以发现手动难以发现的复杂场景中的错误

SimulatedRides 在生产之外也为 Lyft 提供了不可思议的价值。在暂存环境中,它为选择加入该平台的任何服务提供 24/7/365 负载。由于我们使用成功率来衡量 Lyft 的服务健康状况,因此需要流量来生成成功率基线。这意味着在暂存阶段运行模拟的服务所有者可以在将代码发布到生产环境之前,在快速反馈循环中发现预生产环境中的问题。在实践中,这意味着如果存在错误或错误配置,部署到暂存的代码将很快触发警报,而无需 任何手动测试。

拥有可定制和概率解决方案的价值大于缺点

SimulatedRides 是一个非常强大的工具,多年来对 Lyft 产生了积极影响:

  • 与其他基础设施团队一起,该产品帮助引领了多年的工程文化从被动模式转变为主动的“期望一切都会失败”的系统设计。
  • 通过 TPM 主导的高峰活动准备、棕色包、技术设计文档模板更新、文档和其他教育计划,该系统被广泛使用,并已被 Lyft 的数百名服务所有者采用。
  • 服务所有者可以快速轻松地向其系统中的特定端点添加负载。
  • 合成流量不需要与真实流量完全相似。有了足够好的现实主义,我们就可以强调我们的生产服务并识别异常和瓶颈。
  • 服务所有者会定期查看他们的服务在压力下的表现,并且会为无法通过单击按钮关闭的真实流量做好准备。
  • 在暂存中保持持续的流量使我们知道代码更改可能不会降低或破坏真实的用户体验,而无需将它们部署到生产中。
  • 无需在登台上运行负载测试,我们的登台环境不需要是生产的完美基础设施副本(具有多个可用区等),这为我们节省了大量基础设施成本。
  • 我们模拟的概率性质使我们能够测试紧急行为。这意味着我们不仅会测试有限数量的预定义场景,还会发现不常见的场景,而无需在代码中明确定义它们。
  • 当我们测试或部署新的基础设施时,我们使用模拟游乐设施来确保新的基础设施能够承受生产流量水平。

也就是说,也有一些明显的缺点:

  • 对于从事该框架工作的工程师来说,该工具的学习曲线相当陡峭。
  • 该工具是定制的这一事实意味着我们无法雇用具有使用该工具经验的人员。
  • 与服务的集成需要维护和维护。

SimulatedRides 将成长为完全自动化并提供更强大的测试机会

SimulatedRides 的一些未来计划是:

  • 更多地监控业务成果(乘车次数、收入等),而不仅仅是行动成功和服务器错误。这将使开发人员更有信心,相信他们的更改不会对业务成果产生负面影响。
  • 允许开发人员通过未合并的功能分支或本地运行的服务发送一定比例的暂存流量,以便他们可以在开发和调试新功能时将真实流量发送到他们的服务。
  • 完全消除了作为负载测试自动化一部分的人工指挥的需要。

Lyft 具有面向未来的峰值事件性能

对复杂系统进行负载测试是一个难题。我们在生产中找到了最好的保真度。虽然以这种方式进行负载测试存在一些风险,但可以通过良好的工具和护栏来减轻这种风险。SimulatedRides 平台帮助我们相信我们的系统可以处理高速增长和高峰事件,因为没有比生产更好的地方来测试您的系统在实际负载下的表现。

Authors: Remco van Bree, Ben Radler

Contributors : Alex Ilyenko, Ben Radler, Francisco Souza, Garrett Heel, Nathan Hsieh, Remco van Bree, Shu Zheng, Alex Hartwell, Brian Witt

出处:https://eng.lyft.com/simulatedrides-how-lyft-uses-load-testing-to-ensure-reliable-service-during-peak-events-644dcb654454