IT加油站

API性能测试实战:如何设计真实场景的测试方案

31浏览 3天前 软件教程 MA123742

原文:https://dev.to/gramli/api-performance-testing-how-to-design-realistic-tests-59gn(作者 @gramli)

我们大多数人都听过性能测试这个词,无论是在发布重要新功能前、推出新应用时,还是简单地检查应用能否承受特定负载时。

性能测试帮助我们理解应用在负载下的行为,以及它能否应对预期的压力。然而,测试需要被正确设计。否则,结果可能会误导我们,导致错误的结论。

另一方面,正确设计的性能测试能帮助我们识别瓶颈,理解应用的极限,并在发布前提供更强的信心

在本文中,我们将首先探讨性能测试的基本概念和不同类型。然后进入更实际的部分:如何设计真实的测试场景、如何确定应用应承受的负载、如何处理外部服务,以及测试环境应多大程度上匹配生产环境。

目标不仅仅是生成大量请求,而是创建能真正告诉我们应用在真实世界中表现的有用信息的性能测试。

目录

- 测试类型

- 定义预期负载

那么,让我们从性能测试的基本定义和不同类型开始。

基本定义

我们可以将性能测试总结为一种非功能性的软件测试方法,用于评估应用在特定工作负载下的速度、稳定性、可扩展性和响应能力。

简化来说,以 API 为例。我们准备测试用例,在负载下调用想要测试的端点,通常按照代表应用实际使用方式的特定顺序进行。性能测试背后的核心理念是避免发布一个没有为预期负载做好准备的应用。

性能不佳可能导致生产环境宕机、响应缓慢,或应用无法在规定时间内处理任务的情况。所有这些问题最终都可能导致客户流失、收入损失或产品声誉受损。

根据配置、目的和我们想要观察的指标,性能测试可以分为不同类型。

测试类型

📌 测试类型(图,点击查看)

我认为图片已经说明了一切,但还是简要描述一下每种类型:

负载测试验证应用在预期或正常流量水平下的行为。目标通常是确认系统能在保持可接受的响应时间、错误率和资源使用情况的同时,处理所需数量的用户或请求。

压力测试将应用推到超出其预期极限的范围,以发现它开始降级或失败的位置。它们帮助我们识别系统的最大容量,并观察当 CPU、内存、数据库连接或线程等资源耗尽时系统的行为。

耐力(浸泡)测试在较长时间内让应用持续承受负载。目的是发现可能在较短测试中不会出现的问题,如内存泄漏、连接泄漏、资源耗尽或随时间推移的性能下降。

尖峰(峰值)测试模拟突然且显著的流量增加。它们帮助我们验证应用对负载快速变化的反应,以及在流量恢复正常后是否能恢复。

容量测试关注应用在需要处理或操作大量数据时的行为。例如,我们可能测试当数据量远大于平常时,数据库查询、导入、导出或批处理操作的表现。

可扩展性测试验证当工作负载增加并添加更多资源时,应用性能如何变化。目标是理解系统是否能高效扩展,例如通过添加更多应用实例、CPU、内存或数据库容量。

你不一定需要为每种类型实现完全不同的测试。在许多情况下,你可以重用相同的性能测试场景,并根据想要测量的内容(例如并发用户数量、持续时间、请求速率或工作负载模式)更改其配置。然后根据测试目标观察不同的指标。

也不是每个应用都需要每种类型的性能测试。有些应用可能主要需要负载和压力测试,而其他应用可能从负载和尖峰测试中受益更多。这实际上取决于应用的需求,更重要的是,取决于用户实际如何使用它。

如何正确设计性能测试

正确设计的性能测试非常重要,因为大多数时候,我们不想简单地向随机端点乱发请求。这通常无法告诉我们太多关于应用程序真实行为或瓶颈所在的信息。

对我而言,长期行之有效的方法是识别典型的用户行为,并在性能测试中模拟它。例如,想象一个带有订购系统的电子商务应用。

一个典型用户可能会:

  1. 搜索几种产品。
  2. 将产品添加到购物车。
  3. 完成结账流程。
  4. 完成支付。

我不会孤立地测试每个端点,而是会设计一个模拟整个流程的性能测试,调用与前端通常调用相同的端点。

另一个例子可能是具有多种用户类型的企业应用,每种用户类型有不同的权限,使用应用的方式也不同。在这种情况下,我们可以为每种用户类型设计一个典型的工作流,并按照与前端相同的顺序调用 API 端点。这种方法的好处在于我们模拟了真实的用户行为。然后我们可以更改测试配置:例如,增加并发用户数,同时保持相同的真实工作流。

还有一个例子可以是 API 到 API 的通信。想象一下,我们有一个 POST 端点,后面跟着一个 GET 端点。在这种情况下,我们可以在特定负载下模拟不同的输入参数和请求模式,并观察系统的行为。

假设我们已经设计好了用户路径。在构建实际的测试流程时,我们仍然需要考虑一些重要因素。下图总结了一些关键点:

📌 用户路径(图,点击查看)

  • 真实用户点击的速度不可能像性能测试发送请求那么快,因此我们应在操作之间引入真实的思考时间。
  • 并非所有用户都遵循相同的路径。在电子商务应用中,有些用户会完成订单,而有些用户只浏览产品或将商品加入购物车后稍后返回。因此,我们应模拟具有不同行为的多条用户路径。
  • 用户通常不会全部在完全相同的时刻连接,因此对于常规负载测试,我们应逐步增加负载。

一个真实的用户旅程告诉我们该测试什么。下一个问题是系统需要处理多少负载。

定义预期负载

设计真实的工作负载只是工作的一部分。在运行测试之前,我们还需要明确定义一个成功的结果到底是什么样子。并非每个应用都需要每秒处理 1,000 个请求。每个系统都有自己的预期工作负载和性能要求。

例如:

预期负载:150 RPS

p95 < 400 ms
p99 < 1 s
错误率 < 0.5%
维持所需吞吐量
没有持续增长的队列/连接数


那么,我们如何确定这些数字?

让我们回到电子商务的例子。许多应用在某些时段的流量会显著高于平常。对于电子商务应用,这可能是圣诞节、黑色星期五或其他主要销售活动。如果公司已经有良好的可观测性,历史生产环境指标可以为我们提供一个有用的起点。我们可以检查 Grafana 等工具,确定峰值请求率、并发用户数、订单量、CPU 使用率、内存消耗和其他相关指标。然后我们可以估算未来的负载。例如,如果我们预计明年流量将增长 10%,我们可能会决定在测试系统时,在预期负载之上再增加一个安全裕量。

另一种估算所需负载的方法是从业务需求出发。例如,假设业务期望我们的电子商务应用在两小时的峰值期间处理 10,000 个订单。我们可以查看典型的订单流程,估算用户在搜索产品、添加或删除购物车商品、完成结账和完成订单的过程中产生了多少 HTTP 请求。如果一个完成的订单平均产生,比如 20 个请求,那么 10,000 个订单就意味着在这两小时内大约有 200,000 个请求。

由此,我们可以计算出平均请求率:

200,000 个请求 / 7,200 秒 ≈ 每秒 28 个请求

这给出了大约 28 RPS 的平均值。这并不意味着 28 RPS 应该自动成为我们的测试目标。此计算仅估算完成订单流程产生的流量。实际的应用流量通常会更高,因为浏览、放弃的购物车、后台请求和其他用户旅程也对总负载有贡献。真实的流量也极少均匀分布,因此我们应该考虑更短的流量高峰并增加适当的安全裕量。

这种方法为我们提供了另一种在没有可靠生产环境指标时估算所需负载的方法,表明我们可以从技术数据和业务需求两方面推导出性能目标。

性能测试与外部服务

在性能测试中,外部服务需要特别关注。

在某些情况下,我们可以模拟外部服务,因为出于多种原因,我们不需要或不想测试它。一个很好的例子是我们端点后的付费 API,比如 LLM API 或其他付费第三方服务。成本是一个合理的顾虑,因为在性能测试期间,我们的应用可能在相对较短的时间内产生大量请求。

另一种情况是我们完全不需要调用外部 API。例如,它可能是一个简单的参考数据 API 或支付提供商,其性能不在我们的测试范围内。相反,我们可以使用像 WireMock 这样的工具将其替换为受控依赖,并在测试期间返回预期的响应。这使我们能够专注于自身应用的性能,而不会让外部服务的性能、速率限制或可用性影响结果,从而更难识别实际的瓶颈。

另一方面,如果与外部服务的通信是系统实际性能的重要组成部分,我们可能需要单独测试它或将其包含在性能测试中。

因此,是否模拟外部服务应取决于我们想要模拟的行为以及我们具体想测量的内容。

环境配置

环境配置是性能测试的另一个重要部分,因为理想情况下,我们希望性能测试环境尽可能接近生产环境。如果环境与生产环境差异显著,结果可能会产生误导,特别是当我们想估计应用在真实生产负载下的行为时。

那么,我们应该配置什么?

首先,应用本身应使用与生产相同或非常相似的配置。运行 API 的服务器或集群也应具有可比的 CPU、内存、扩展规则和其他资源限制。

数据库也是如此。不过,这不仅仅是使用类似的数据库资源。我们还应该避免针对几乎为空的数据库进行测试。数据的数量和分布会对性能产生显著影响。在高负载下,当表包含数百万行与仅几条测试记录时,CRUD 操作、连接、过滤和排序的行为可能非常不同。索引、查询执行计划、统计信息和缓存都可能根据数据的大小和结构而表现不同。

因此,测试数据库应尽可能包含真实数量的代表性数据。

我们还应该注意缓存配置,例如 Redis 或内存缓存。不同的缓存配置或使用永久预热的缓存进行测试,可能产生不代表真实生产行为的结果。

网络条件是另一个重要因素。例如,如果外部服务在本地被模拟,请求可能几乎立即完成,而生产中的真实服务可能增加几十或几百毫秒的网络延迟。根据我们想要测量的内容,我们可能需要模拟这种延迟以获得更真实的结果。

最后,是负载生成器本身。这一点很容易忘记。生成负载的机器必须有足够的 CPU、内存、网络容量和可用连接来生成所需的工作负载。否则,负载生成器可能成为瓶颈,而不是我们实际测试的应用。

指标与结果

运行性能测试后,我们需要评估结果并通常创建某种形式的报告。我们关注的指标取决于测试类型,因为不同的测试类型回答不同的问题。

回到我们的电子商务应用示例,假设我们决定同时进行负载测试和压力测试。

对于负载测试,我们已经知道预期的工作负载,例如特定的每秒请求数或并发用户数。现在我们想验证应用程序是否能在保持可接受性能的同时处理此负载。

一些最重要的指标包括:

响应时间*,特别是百分位数,如 p95 或 p99。

错误率*——请求失败的百分比。

吞吐量*——应用程序是否实际处理了预期的请求数量。

资源使用率*——CPU、内存、数据库连接、连接池和其他相关资源。

例如,如果某个端点的 p95 响应时间远高于预期,我们可以开始调查瓶颈所在。同样,如果错误率超过可接受限制,我们需要找出哪些请求失败以及失败原因。

对于压力测试,我们的目标略有不同。我们故意将负载增加到超过预期的水平,并试图找到应用程序开始性能下降的临界点。在这里,我们观察随着负载增加,响应时间和错误率如何变化,同时监控 CPU、内存、数据库连接、队列和其他有限资源。我们寻找的是系统达到饱和、开始产生过多错误或无法再维持所需吞吐量的那个临界点。

观察负载降低后应用程序的行为也很有用。一个在极端负载下变慢但之后能恢复的系统,与一个保持卡死或需要重启的系统,其行为表现非常不同。

下图展示了我们在示例中监控的指标,并强调了为什么我们需要结合查看多个图表来识别特定的瓶颈或饱和点。

📌 Performance test metrics(图,点击查看)

因此,最终报告不应只包含单一的数字,如平均响应时间。它应该将生成的工作负载与延迟、吞吐量、错误和资源利用率联系起来,这样我们才能不仅理解应用程序是否未达预期,还能理解为什么未达预期。

总结

在本文中,我们介绍了一些性能测试的基础知识,然后重点讲解了如何为现实场景设计测试。

关键在于理解应用程序的实际使用方式、准备真实的测试环境、生成代表性的工作负载并监控正确的指标。当这些部分设计得当时,性能测试可以为我们提供有关瓶颈、系统限制以及应用程序在负载下的整体行为的有用信息。

已经有很多优秀的文章解释了性能测试理论和各种测试类型。我的目标不是重复所有这些内容,而是从更实用的角度来看待性能测试,并展示我如何设计真实的测试。

最终,只有当测试的工作负载、环境和指标足够真实,使得结果有意义时,性能测试才真正有用。

原文:https://dev.to/gramli/api-performance-testing-how-to-design-realistic-tests-59gn(作者 @gramli)

#API性能测试 #性能测试 #负载测试 #压力测试 #测试类型