使用 Testcontainers 彻底改善了处理测试场景的过程。借助此工具,创建集成测试环境变得更加简单(请参阅文章使用 Kafka 进行测试中的隔离)。现在,我们可以轻松启动具有不同版本数据库、消息代理和其他服务的容器。对于集成测试,Testcontainers 已被证明是不可或缺的。
尽管负载测试不如功能测试常见,但它却更有趣。研究图表并分析特定服务的性能可以带来真正的乐趣。这样的任务很少见,但对我来说尤其令人兴奋。
本文的目的是演示一种创建负载测试设置的方法,其方式与编写常规集成测试的方式相同:在 Gradle 项目环境中使用 Testcontainers 以 Spock 测试的形式进行。使用 Gatling、WRK 和 Yandex.Tank 等负载测试实用程序。
工具集:Gradle + Spock Framework + Testcontainers。实现版本是一个单独的 Gradle 模块。使用的负载测试实用程序是 Gatling、WRK 和 Yandex.Tank。
处理测试对象有两种方法:
在第一种情况下,我们有一组与项目版本和更改无关的负载测试。这种方法将来更容易维护,但仅限于测试已发布的镜像。当然,我们可以在本地手动构建这些镜像,但这种方式自动化程度较低,并且可重复性较低。在没有必要镜像的情况下在 CI/CD 中运行时,测试将失败。
在第二种情况下,测试在服务的最新版本上运行。这样可以将负载测试集成到 CI 中,并获取服务版本之间的性能数据变化。但是,负载测试通常比单元测试花费的时间更长。是否将此类测试作为质量门的一部分纳入 CI 取决于您。
本文考虑的是第一种方法。借助 Spock,我们可以对服务的多个版本运行测试以进行比较分析:
where: image | _ 'avvero/sandbox:1.0.0' | _ 'avvero/sandbox:1.1.0' | _
值得注意的是,本文的目标是展示测试空间的组织,而不是全面的负载测试。
对于测试对象,我们采用一个名为 Sandbox 的简单 HTTP 服务,该服务发布一个端点并使用来自外部源的数据来处理请求。该服务有一个数据库。
该服务的源代码(包括 Dockerfile)可在项目存储库spring-sandbox中找到。
当我们在文章后面深入探讨细节时,我想首先简要概述一下load-tests
Gradle 模块的结构,以便了解其组成:
load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing | |-- test/ | | |-- groovy/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- GatlingTests.groovy # Gatling load test file | | | | |-- WrkTests.groovy # Wrk load test file | | | | |-- YandexTankTests.groovy # Yandex.Tank load test file | | |-- java/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- FileHeadLogConsumer.java # Helper class for logging to a file | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ # WireMock setup for mocking external services | | | | | |-- health.json | | | | | |-- forecast.json | | | |-- yandex-tank/ # Yandex.Tank load testing configuration | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py | | | |-- wrk/ # LuaJIT scripts for Wrk | | | | |-- scripts/ | | | | | |-- getForecast.lua |-- build.gradle
项目存储库 - https://github.com/avvero/testing-bench 。
从上面的描述中,我们可以看到该服务有两个依赖项:服务 https://external-weather-api.com 和数据库。它们的描述将在下面提供,但让我们首先让方案的所有组件能够在 Docker 环境中进行通信——我们将描述网络:
def network = Network.newNetwork()
并为每个组件提供网络别名。这非常方便,并允许我们静态描述集成参数。
依赖项(例如 WireMock)和负载测试实用程序本身需要配置才能工作。这些可以是传递给容器的参数,也可以是需要挂载到容器的整个文件和目录。
此外,我们还需要从容器中检索它们的工作结果。为了解决这些任务,我们需要提供两组目录:
workingDirectory
— 模块的资源目录,直接位于load-tests/
。
reportDirectory
— 工作结果的目录,包括指标和日志。有关此内容的更多信息将在报告部分中介绍。Sandbox 服务使用 Postgres 作为其数据库。我们来描述一下这种依赖关系:
def postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withNetwork(network) .withNetworkAliases("postgres") .withUsername("sandbox") .withPassword("sandbox") .withDatabaseName("sandbox")
声明指定了网络别名postgres
,Sandbox 服务将使用该别名连接数据库。为了完成与数据库的集成描述,需要为服务提供以下参数:
'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox'
数据库结构由应用程序本身使用 Flyway 管理,因此测试中不需要额外的数据库操作。
如果我们没有可能、没有必要或不想在容器中运行实际组件,我们可以为其 API 提供模拟。对于服务 https://external-weather-api.com,使用 WireMock。
WireMock 容器的声明将如下所示:
def wiremock = new GenericContainer<>("wiremock/wiremock:3.5.4") .withNetwork(network) .withNetworkAliases("wiremock") .withFileSystemBind("${workingDirectory}/src/test/resources/wiremock/mappings", "/home/wiremock/mappings", READ_WRITE) .withCommand("--no-request-journal") .waitingFor(new LogMessageWaitStrategy().withRegEx(".*https://wiremock.io/cloud.*")) wiremock.start()
WireMock 需要 mock 配置, withFileSystemBind
指令描述了本地文件路径和 Docker 容器内路径之间的文件系统绑定,本例中本地机器上的目录"${workingDirectory}/src/test/resources/wiremock/mappings"
会被挂载到 WireMock 容器内的/home/wiremock/mappings
中。
下面是项目结构的附加部分,以了解目录中的文件组成:
load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ | | | | | |-- health.json | | | | | |-- forecast.json
为了确保模拟配置文件被 WireMock 正确加载和接受,您可以使用辅助容器:
helper.execInContainer("wget", "-O", "-", "http://wiremock:8080/health").getStdout() == "Ok"
辅助容器描述如下:
def helper = new GenericContainer<>("alpine:3.17") .withNetwork(network) .withCommand("top")
顺便说一句,IntelliJ IDEA 2024.1 版本引入了对 WireMock 的支持,IDE 在形成模拟配置文件时会提供建议。
Sandbox服务容器的声明如下:
def javaOpts = ' -Xloggc:/tmp/gc/gc.log -XX:+PrintGCDetails' + ' -XX:+UnlockDiagnosticVMOptions' + ' -XX:+FlightRecorder' + ' -XX:StartFlightRecording:settings=default,dumponexit=true,disk=true,duration=60s,filename=/tmp/jfr/flight.jfr' def sandbox = new GenericContainer<>(image) .withNetwork(network) .withNetworkAliases("sandbox") .withFileSystemBind("${reportDirectory}/logs", "/tmp/gc", READ_WRITE) .withFileSystemBind("${reportDirectory}/jfr", "/tmp/jfr", READ_WRITE) .withEnv([ 'JAVA_OPTS' : javaOpts, 'app.weather.url' : 'http://wiremock:8080', 'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox' ]) .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Started SandboxApplication.*")) .withStartupTimeout(Duration.ofSeconds(10)) sandbox.start()
值得注意的参数和 JVM 设置包括:
此外,还配置了用于保存服务诊断结果的目录。
如果需要将任何容器的日志查看到文件中,这在编写和配置测试场景阶段可能是必要的,您可以在描述容器时使用以下指令:
.withLogConsumer(new FileHeadLogConsumer("${reportDirectory}/logs/${alias}.log"))
在这种情况下,使用FileHeadLogConsumer
类,它允许将有限数量的日志写入文件。这样做是因为在负载测试场景中可能不需要整个日志,部分日志就足以评估服务是否正常运行。
负载测试工具有很多。在本文中,我建议考虑使用其中三种:Gatling、Wrk 和 Yandex.Tank。这三种工具都可以独立使用。
Gatling 是一款用 Scala 编写的开源负载测试工具。它允许创建复杂的测试场景并提供详细的报告。Gatling 的主要模拟文件作为 Scala 资源连接到模块,因此可以方便地使用 IntelliJ IDEA 的全方位支持,包括语法突出显示和通过方法导航以供文档参考。
Gatling 的容器配置如下:
def gatling = new GenericContainer<>("denvazh/gatling:3.2.1") .withNetwork(network) .withFileSystemBind("${reportDirectory}/gatling-results", "/opt/gatling/results", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/scala", "/opt/gatling/user-files/simulations", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/resources", "/opt/gatling/conf", READ_WRITE) .withEnv("SERVICE_URL", "http://sandbox:8080") .withCommand("-s", "MainSimulation") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Please open the following file: /opt/gatling/results.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ); gatling.start()
该设置与其他容器几乎相同:
reportDirectory
挂载报告目录。workingDirectory
挂载配置文件的目录。workingDirectory
挂载模拟文件的目录。
此外,参数被传递给容器:
SERVICE_URL
环境变量,其中包含 Sandbox 服务的 URL 值。尽管如前所述,使用网络别名允许直接在场景代码中对 URL 进行硬编码。
-s MainSimulation
命令运行特定的模拟。
以下是项目源文件结构的提醒,以便了解正在传递什么以及传递到哪里:
load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing
由于这是最终的容器,并且我们期望在完成后获得结果,因此我们设置了期望.withRegEx(".*Please open the following file: /opt/gatling/results.*")
。当此消息出现在容器日志中或60 * 2
秒后,测试将结束。
我不会深入研究该工具场景的 DSL。您可以在项目存储库中查看所用场景的代码。
Wrk是一款简单快速的负载测试工具。它能够以最少的资源生成大量负载。主要功能包括:
Wrk 的容器配置如下:
def wrk = new GenericContainer<>("ruslanys/wrk") .withNetwork(network) .withFileSystemBind("${workingDirectory}/src/test/resources/wrk/scripts", "/tmp/scripts", READ_WRITE) .withCommand("-t10", "-c10", "-d60s", "--latency", "-s", "/tmp/scripts/getForecast.lua", "http://sandbox:8080/weather/getForecast") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Transfer/sec.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) wrk.start()
为了使 Wrk 能够处理对 Sandbox 服务的请求,需要通过 Lua 脚本描述请求,因此我们从workingDirectory
挂载脚本目录。使用命令,我们运行 Wrk,指定脚本和目标服务方法的 URL。Wrk 根据其结果将报告写入日志,可用于设定预期。
Yandex.Tank 是 Yandex 开发的负载测试工具。它支持各种负载测试引擎,例如 JMeter 和 Phantom。要存储和显示负载测试结果,您可以使用免费服务Overload 。
这是容器配置:
copyFiles("${workingDirectory}/src/test/resources/yandex-tank", "${reportDirectory}/yandex-tank") def tank = new GenericContainer<>("yandex/yandex-tank") .withNetwork(network) .withFileSystemBind("${reportDirectory}/yandex-tank", "/var/loadtest", READ_WRITE) .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Phantom done its work.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) tank.start()
Sandbox 的负载测试配置由两个文件表示: load.yaml
和ammo.txt
。作为容器描述的一部分,配置文件被复制到reportDirectory
,它将被挂载为工作目录。以下是项目源文件的结构,以了解正在传递的内容和传递到哪里:
load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- yandex-tank/ | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py
测试结果(包括 JVM 性能记录和日志)保存在目录build/${timestamp}
中,其中${timestamp}
代表每次测试运行的时间戳。
以下报告可供审阅:
如果使用 Gatling:
如果使用 Wrk:
如果使用 Yandex.Tank:
报告的目录结构如下:
load-tests/ |-- build/ | |-- ${timestamp}/ | | |-- gatling-results/ | | |-- jfr/ | | |-- yandex-tank/ | | |-- logs/ | | | |-- sandbox.log | | | |-- gatling.log | | | |-- gc.log | | | |-- wiremock.log | | | |-- wrk.log | | | |-- yandex-tank.log | |-- ${timestamp}/ | |-- ...
负载测试是软件开发生命周期中的关键阶段。它有助于评估应用程序在各种负载条件下的性能和稳定性。本文介绍了一种使用 Testcontainers 创建负载测试环境的方法,该方法可以轻松高效地设置测试环境。
Testcontainers 大大简化了集成测试环境的创建,提供了灵活性和隔离性。对于负载测试,此工具可以部署具有不同版本服务和数据库的必要容器,从而更轻松地进行测试并提高结果的可重复性。
提供的 Gatling、Wrk 和 Yandex.Tank 配置示例以及容器设置展示了如何有效地集成各种工具并管理测试参数。
此外,还介绍了记录和保存测试结果的过程,这对于分析和改进应用程序性能至关重要。这种方法将来可以扩展,以支持更复杂的场景并与其他监控和分析工具集成。
感谢您对本文的关注,祝您在编写有用的测试时好运!