大厂实践:Airbnb 使用 Impulse 进行负载测试

Airbnb 内部负载测试框架 Impulse 的实践解析:如何通过上下文感知负载生成、依赖项模拟、生产流量采集与回放、测试 API 生成,在 CI/CD 中完成自助式负载测试和容量验证。

作者 翻译

导读:本文分享 Airbnb 内部负载测试框架 Impulse 的实践。负载测试不是只在故障后补做的性能动作,而是研发、测试和运维都应该提前关注的稳定性工程能力。上线前做负载测试,后续才能做容量管理(Capacity Management 是 SRE 重点方向之一);在大促、流量峰值或关键版本发布之前,团队必须尽早知道系统容量上限、瓶颈位置和回归风险。

核心要点摘要

  • Impulse 是 Airbnb 内部的 load-testing-as-a-service 框架之一,面向服务团队提供自助式负载测试能力。
  • Impulse 的核心能力包括合成负载生成、依赖项模拟、生产流量数据收集与回放,以及面向异步工作流的测试 API 生成。
  • 它强调去中心化、容器化和 CI/CD 集成,让服务所有者可以按需启动隔离的压测环境,并在发布流程中自动执行容量验证。
  • 对复杂系统而言,负载测试不只验证 QPS/TPS,还要验证上下文感知请求、下游依赖延迟、异常响应、流量分布和异步链路行为。
  • Airbnb 的实践说明,系统级负载测试可以帮助团队提前识别线程池压力、API 超时、高内存使用等潜在问题。

系统级负载测试对于可靠性和效率至关重要。它可以识别瓶颈、评估峰值流量下的容量、建立性能基准并检测错误。在像 Airbnb 这样规模和复杂度都很高的公司中,负载测试需要稳健、灵活和分散执行。这要求工程团队拥有合适的工具,能够执行与 CI/CD 无缝集成的自助式负载测试。

Impulse 是 Airbnb 内部的负载测试即服务框架之一。它提供的工具可以生成合成负载、模拟依赖项,并从生产环境收集流量数据。本文将介绍 Impulse 的架构如何减少手动工作、集成可观测性堆栈,并帮助团队主动发现潜在稳定性问题。

Impulse 能力总览

能力模块 解决的问题 典型用途 对负载测试的价值
Load generator 负载生成器 如何生成真实、有上下文的测试请求 使用 Java 或 Kotlin 编写测试逻辑,按阶段配置 TPS 和持续时间 支持合成流量、上下文感知请求和 CI/CD 自动执行
Dependency mocker 依赖项模拟器 如何在不影响下游服务的情况下压测 SUT 模拟下游响应、延迟和异常,或回放生产记录响应 隔离受测服务,降低对供应商服务和内部依赖的影响
Traffic collector 流量收集器 如何复用真实生产流量形态 收集上游、下游流量及其关系 支持高保真流量回放,减少测试环境与生产行为差异
Testing API generator 测试 API 生成器 如何测试事件驱动的异步工作流 根据事件或作业架构生成 HTTP API 让异步逻辑可以像同步 API 一样被直接压测

架构:四个独立组件组成的负载测试框架

Impulse 是一个面向系统级负载测试的框架,允许服务所有者执行上下文感知负载测试、模拟依赖项,并收集流量数据,以验证系统在不同条件下的性能表现。它包含四个主要组件:

  • Load generator:动态生成上下文感知请求,用于使用合成流量或收集到的流量测试不同场景。
  • Dependency mocker:模拟带延迟的下游响应,使受测服务(SUT)在压测时不必真正调用某些依赖服务。当依赖项是无法支持负载测试的供应商服务,或者团队希望在日常部署期间进行回归负载测试且不影响下游服务时,这一点尤其重要。
  • Traffic collector:从生产环境收集上游和下游流量,然后将生成的数据应用于测试环境。
  • Testing API generator:将异步工作流包装为同步 API 调用,以便进行负载测试。

这四个工具彼此独立。服务所有者可以根据自己的负载测试需求选择其中一个或多个组件,而不是被迫接入完整套件。

Load generator:上下文感知的负载生成器

为什么需要上下文感知请求

在进行负载测试时,发给 SUT 的请求通常需要使用上一个响应中的信息,或者必须按特定顺序发送。例如,如果更新 API 需要提供要更新的 entity_id,测试逻辑就必须先确保该实体已经存在于测试环境上下文中。

Impulse 的负载生成器允许用户用 Java 或 Kotlin 编写任意测试逻辑,并启动容器针对 SUT 大规模运行这些测试。Airbnb 选择代码而不是 DSL 或配置逻辑,主要基于四点考虑:

  • 灵活性:编程语言比 DSL 更具表达力,更适合复杂的上下文场景。
  • 可重用性:同一份测试代码可用于其他测试,例如集成测试。
  • 开发者熟悉度:开发者无需额外学习测试逻辑 DSL,入门成本更低。
  • 开发体验:可以使用 IDE、单元测试、调试等常规工程能力。

下面是一个合成上下文感知测试用例示例:

class HelloWorldLoadGenerator : LoadGenerator {
   override suspend fun run() {
       val createdEntity = sutApiClient.create(CreateRequest(name="foo", ...)).data

       // request with id from previous response (context)
       val updateResponse = sutApiClient.update(UpdateRequest(id=createdEntity.id, name="bar"))
       
       // ... other operations
       
       // clean up
       sutApiClient.delete(DeleteRequest(id=createdEntity.id))
   }
}

为什么采用去中心化和容器化设计

Impulse 的负载生成器是去中心化和容器化的。每次触发负载测试时,系统都会创建一组新容器来运行测试。这种设计带来三个直接收益:

  • 隔离:不同服务之间的负载测试运行彼此隔离,避免相互干扰。
  • 可扩展性:容器数量可以根据流量需求扩容或缩容。
  • 成本效益:容器生命周期较短,只在负载测试运行期间存在。

Airbnb 的服务基于云环境运行,因此还有一个细节很关键:Impulse 会在所有数据中心之间平均分配工作线程,并让负载从这些工作线程均匀发出。Impulse 的负载生成器会确保整体每秒触发数(TPS)与配置一致。基于这种设计,团队可以更好地利用负载均衡器中的 locality 设置,从而更接近生产环境中的真实流量分布。

如何在 CI/CD 中执行负载测试

Impulse 的负载生成器设计为在 CI/CD 管道中执行,因此可以自动触发负载测试。开发人员可以将测试规范配置为多个阶段,例如预热阶段、稳态阶段、峰值阶段等。每个阶段都可以配置:

  • 要运行的测试用例
  • 每个测试用例的 TPS(每秒触发数)
  • 测试持续时间

这让负载测试不再只是一项临时手工操作,而可以成为发布流程、回归验证和容量验证的一部分。

Dependency mocker:隔离下游影响的依赖项模拟器

Impulse 是一个去中心化框架,每个服务都有自己的依赖项模拟器。这样可以消除服务之间的干扰,并降低通信成本。每个依赖项模拟器都是进程外服务,因此 SUT 的行为与生产环境中的行为保持一致。

Airbnb 将模拟器运行在单独实例中,以避免对 SUT 的性能造成影响。模拟服务器都是短暂存在的:它们只在测试运行之前启动,并在测试运行之后关闭,从而节省成本和维护工作。响应延迟和异常是可配置的,mocker 实例数量也可以按需调整,以支持大量流量。

依赖项模拟器支持哪些场景

依赖项模拟器有两个值得注意的能力:

  • 可以有选择地存根某些依赖项。目前,HTTP JSON、Airbnb Thrift 和 Airbnb GraphQL 依赖项支持存根。
  • 除了负载测试,它也可以支持集成测试。集成测试经常依赖其他服务或第三方 API 调用,而这些外部依赖不一定能提供稳定测试环境,或者只支持理想场景。依赖项模拟器可以通过预定义响应或异常,让团队更完整地测试这些流程。

mock 响应的两种生成方式

Impulse 支持两种生成 mock 响应的选项:

  1. Synthetic response:响应由用户逻辑生成,与集成测试中的方式类似;区别在于响应来自带模拟延迟的远程进程外服务器。
  2. 重放响应:响应从生产下游记录中重放,由 Traffic collector 组件支持。

对于 Synthetic response,相关逻辑同样使用 Java 或 Kotlin 编写,包含请求匹配和响应生成,并且可以使用 p95/p99 指标模拟延迟。

以下是 Kotlin 中带延迟的合成响应示例:

downstreamsMocking.every(
      thriftRequest<FooRequest>().having { it.message == "hello" }
    ).returns { request ->
      ThriftDownstream.Response.thriftEncoded(
        HttpStatus.OK,
        FooResponse.builder.reply("${request.message} world").build()
      )
    }.with {
      delay = latencyFromP95(p95=500.miliseconds, min=200.miliseconds, max=2000.miliseconds)
    }

Traffic collector:生产流量采集与高保真回放

Traffic collector 组件用于捕获上游和下游流量,以及它们之间的关系。这样做的目的是让 Impulse 在负载测试期间可以更准确地重放生产流量,避免因为下游数据或行为不一致导致测试失真。

通过依赖项模拟器复制下游响应,包括接近生产环境的延迟和错误,系统可以实现更高保真的负载测试。在这种模式下,测试环境中的服务行为更接近生产环境,从而为性能评估、容量验证和稳定性回归提供更可靠的输入。

Testing API generator:让异步工作流也能被压测

Airbnb 严重依赖事件驱动的异步工作流,这些工作流对业务运营非常关键。相关任务包括处理来自消息队列(MQ)的事件,以及执行延迟作业。

大多数 MQ 事件或作业都来自同步流,例如 API 调用,因此理论上可以被 API 负载测试覆盖。但现实情况更复杂:这些异步流往往涉及来自多种来源的长链路事件和作业派发,仅使用基于 API 的方法很难准确复制和测试。

为了解决这个问题,Testing API generator 会在 CI 阶段根据事件或作业架构创建 HTTP API。这些 API 充当底层异步流的包装器,并以独占方式注册在测试环境中。这样,负载测试工具(例如 Load generator)就可以将流量发送到这些合成 API,使异步流像同步 API 一样执行。

Testing API generator 的目标,是帮助开发人员识别异步流实现中的性能瓶颈,以及高流量条件下的潜在问题。它通过允许团队直接对异步流做负载测试来实现这一点,而不涉及 MQ 等中间件组件。

这种设计背后的理由是:开发人员通常想评估的是自己业务逻辑的行为,而不是中间件的行为。中间件通常已经经过充分测试。绕过这些组件,可以简化负载测试过程,并让开发人员独立管理和执行自己的测试。

与其他测试框架集成

Airbnb 强调产品质量,并使用多功能测试框架,覆盖开发、暂存和生产环境中的集成测试和 API 测试,同时顺利集成到 CI/CD 管道中。Impulse 的模块化设计有助于它与这些框架集成,从而提供更系统的服务测试能力。

从 Impulse 实践看负载测试的关键设计

从 Airbnb 的 Impulse 实践可以看到,面向复杂业务系统的负载测试至少需要回答四类问题:

  1. 请求是否真实:测试请求是否包含上下文、顺序和业务状态,而不是只压测孤立接口。
  2. 依赖是否可控:下游服务、第三方 API、延迟和异常是否能够被模拟,避免压测影响真实依赖。
  3. 流量是否可信:测试流量是否能反映生产中的上游、下游关系和流量分布。
  4. 执行是否自动化:负载测试是否能进入 CI/CD,而不是依赖上线前临时手工操作。

这些问题共同指向一个目标:负载测试要服务于容量验证、稳定性回归和发布风险控制,而不仅是一次性的性能数据采集。

总结

Impulse 通过 Load generator、Dependency mocker、Traffic collector 和 Testing API generator 四个组件,帮助 Airbnb 的开发者执行自助式负载测试。它覆盖了合成负载、依赖隔离、生产流量回放、异步工作流压测和 CI/CD 自动执行等关键场景。

在原文发布时,Impulse 已经在多个客户支持后端服务中实现,并正在与公司内部计划使用 Impulse 进行负载测试的不同团队展开探讨。

在使用过程中,团队收到了不少正向反馈。例如,Impulse 帮助识别和解决服务中的潜在问题:测试期间检测到线程池压力导致的 ApiClientThreadToolExhaustionException;它还提醒团队注意服务部署期间客户端 API 调用中偶尔出现的超时错误;同时,Impulse 帮助团队发现主服务容器中的高内存使用率,使团队能够微调内存分配并优化服务资源使用。因此,原反馈也强烈建议将 Impulse 用作开发和测试过程中不可或缺的一部分。

FAQ

Impulse 主要解决什么问题?

Impulse 主要解决服务团队如何自助完成系统级负载测试的问题。它通过负载生成、依赖项模拟、流量采集与回放、测试 API 生成,让团队可以在 CI/CD 中验证服务容量、性能瓶颈和稳定性风险。

Impulse 为什么不只使用简单的配置或 DSL 生成请求?

Airbnb 的负载生成器允许开发者使用 Java 或 Kotlin 编写测试逻辑,因为复杂负载测试经常需要上下文感知。例如,后续请求要使用前一个响应返回的 ID,或者请求必须按业务顺序执行。使用通用编程语言可以获得更高灵活性,也能复用集成测试中的代码。

Dependency mocker 在负载测试中有什么价值?

Dependency mocker 可以模拟下游响应、延迟和异常,让 SUT 在接近真实生产行为的条件下被测试,同时避免压测影响供应商服务或内部下游服务。它也可以支持集成测试,用预定义响应或异常覆盖不稳定或难以构造的依赖场景。

Traffic collector 和普通合成流量有什么区别?

普通合成流量主要由测试逻辑构造;Traffic collector 则从生产环境收集上游、下游流量及其关系。结合依赖项模拟器后,Impulse 可以在测试环境中更高保真地重放生产流量,减少测试环境行为与生产环境行为之间的差异。

Testing API generator 为什么适合异步工作流?

事件驱动的异步工作流通常经过 MQ、延迟作业和长链路事件派发,直接压测比较困难。Testing API generator 会在 CI 阶段把事件或作业架构包装为 HTTP API,让负载测试工具可以像调用同步 API 一样触发异步逻辑,从而更直接地验证业务逻辑在高流量下的表现。

本文由 快猫团队 翻译,原文来自 Airbnb Tech Blog

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

标签 大厂实践
快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云