paint-brush
Todo lo que necesita saber sobre el banco de pruebas de carga basado en contenedores de pruebapor@avvero
190 lecturas

Todo lo que necesita saber sobre el banco de pruebas de carga basado en contenedores de prueba

por Anton Belyaev15m2024/06/07
Read on Terminal Reader

Demasiado Largo; Para Leer

El propósito de este artículo es demostrar un enfoque para crear una configuración para pruebas de carga de la misma manera que se escriben las pruebas de integración regulares: en forma de pruebas de Spock utilizando Testcontainers en un entorno de proyecto Gradle. Se utilizan utilidades de prueba de carga como Gatling, WRK y Yandex.Tank.
featured image - Todo lo que necesita saber sobre el banco de pruebas de carga basado en contenedores de prueba
Anton Belyaev HackerNoon profile picture

El uso de Testcontainers ha mejorado radicalmente el proceso de trabajo con escenarios de prueba. Gracias a esta herramienta, la creación de entornos para pruebas de integración se ha vuelto más sencilla (ver el artículo Aislamiento en Pruebas con Kafka ). Ahora podemos lanzar fácilmente contenedores con diferentes versiones de bases de datos, intermediarios de mensajes y otros servicios. Para las pruebas de integración, Testcontainers ha demostrado ser indispensable.


Aunque las pruebas de carga son menos comunes que las pruebas funcionales, pueden resultar mucho más divertidas. Estudiar gráficos y analizar el rendimiento de un servicio en particular puede brindar un verdadero placer. Estas tareas son raras, pero para mí resultan especialmente apasionantes.


El propósito de este artículo es demostrar un enfoque para crear una configuración para pruebas de carga de la misma manera que se escriben las pruebas de integración regulares: en forma de pruebas de Spock utilizando Testcontainers en un entorno de proyecto Gradle. Se utilizan utilidades de prueba de carga como Gatling, WRK y Yandex.Tank.

Crear un entorno de prueba de carga

Conjunto de herramientas: Gradle + Spock Framework + Testcontainers. La variante de implementación es un módulo Gradle separado. Las utilidades de prueba de carga utilizadas son Gatling, WRK y Yandex.Tank.


Hay dos enfoques para trabajar con el objeto de prueba:

  • Prueba de imágenes publicadas;
  • Construcción de imágenes a partir del código fuente del proyecto y pruebas.


En el primer caso, tenemos un conjunto de pruebas de carga que son independientes de la versión y cambios del proyecto. Este enfoque es más fácil de mantener en el futuro, pero se limita a probar únicamente imágenes publicadas. Por supuesto, podemos crear manualmente estas imágenes localmente, pero esto está menos automatizado y reduce la reproducibilidad. Cuando se ejecuta en CI/CD sin las imágenes necesarias, las pruebas fallarán.


En el segundo caso, las pruebas se ejecutan en la última versión del servicio. Esto permite integrar pruebas de carga en CI y obtener cambios en los datos de rendimiento entre versiones del servicio. Sin embargo, las pruebas de carga suelen tardar más que las pruebas unitarias. La decisión de incluir dichas pruebas en CI como parte del control de calidad depende de usted.


Este artículo considera el primer enfoque. Gracias a Spock, podemos realizar pruebas en múltiples versiones del servicio para realizar análisis comparativos:

 where: image | _ 'avvero/sandbox:1.0.0' | _ 'avvero/sandbox:1.1.0' | _

Es importante señalar que el objetivo de este artículo es demostrar la organización del espacio de pruebas, no pruebas de carga a gran escala.

Servicio de destino

Para el objeto de prueba, tomemos un servicio HTTP simple llamado Sandbox, que publica un punto final y utiliza datos de una fuente externa para manejar las solicitudes. El servicio cuenta con una base de datos.

El código fuente del servicio, incluido Dockerfile, está disponible en el repositorio del proyecto spring-sandbox .

Descripción general de la estructura del módulo

A medida que profundizamos en los detalles más adelante en el artículo, quiero comenzar con una breve descripción general de la estructura del módulo Gradle load-tests para comprender su composición:

 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

Repositorio de proyectos: https://github.com/avvero/testing-bench .

Ambiente

De la descripción anterior, vemos que el servicio tiene dos dependencias: el servicio https://external-weather-api.com y una base de datos. Su descripción se proporcionará a continuación, pero comencemos permitiendo que todos los componentes del esquema se comuniquen en un entorno Docker; describiremos la red:

 def network = Network.newNetwork()

y proporcionar alias de red para cada componente. Esto es extremadamente conveniente y nos permite describir estáticamente los parámetros de integración.

Dependencias como WireMock y las propias utilidades de prueba de carga requieren configuración para funcionar. Estos pueden ser parámetros que se pueden pasar al contenedor o archivos y directorios completos que deben montarse en los contenedores.


Además, necesitamos recuperar los resultados de su trabajo de los contenedores. Para resolver estas tareas, necesitamos proporcionar dos conjuntos de directorios:


  • workingDirectory : el directorio de recursos del módulo, directamente en load-tests/ .


  • reportDirectory : el directorio de los resultados del trabajo, incluidas métricas y registros. Más sobre esto estará en la sección de informes.

Base de datos

El servicio Sandbox utiliza Postgres como base de datos. Describamos esta dependencia de la siguiente manera:

 def postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withNetwork(network) .withNetworkAliases("postgres") .withUsername("sandbox") .withPassword("sandbox") .withDatabaseName("sandbox")


La declaración especifica el alias de red postgres , que el servicio Sandbox utilizará para conectarse a la base de datos. Para completar la descripción de la integración con la base de datos, es necesario dotar al servicio de los siguientes parámetros:

 'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox'


La estructura de la base de datos la gestiona la propia aplicación mediante Flyway, por lo que no se necesitan manipulaciones adicionales de la base de datos en la prueba.

Solicitudes burlonas a https://external-weather-api.com

Si no tenemos la posibilidad, la necesidad o el deseo de ejecutar el componente real en un contenedor, podemos proporcionar una simulación de su API. Para el servicio https://external-weather-api.com, se utiliza WireMock.


La declaración del contenedor WireMock se verá así:

 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 requiere una configuración simulada. La instrucción withFileSystemBind describe el enlace del sistema de archivos entre la ruta del archivo local y la ruta dentro del contenedor Docker. En este caso, el directorio "${workingDirectory}/src/test/resources/wiremock/mappings" en la máquina local se montará en /home/wiremock/mappings dentro del contenedor WireMock.


A continuación se muestra una parte adicional de la estructura del proyecto para comprender la composición de los archivos en el directorio:

 load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ | | | | | |-- health.json | | | | | |-- forecast.json


Para garantizar que WireMock cargue y acepte correctamente los archivos de configuración simulados, puede utilizar un contenedor auxiliar:

 helper.execInContainer("wget", "-O", "-", "http://wiremock:8080/health").getStdout() == "Ok"


El contenedor auxiliar se describe a continuación:

 def helper = new GenericContainer<>("alpine:3.17") .withNetwork(network) .withCommand("top")


Por cierto, la versión 2024.1 de IntelliJ IDEA introdujo soporte para WireMock y el IDE proporciona sugerencias al crear archivos de configuración simulados.

Configuración de inicio del servicio de destino

La declaración del contenedor del servicio Sandbox tiene el siguiente aspecto:

 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()

Los parámetros notables y las configuraciones de JVM incluyen:

  • Recopilación de información sobre eventos de recolección de basura.
  • Uso de Java Flight Recorder (JFR) para registrar datos de rendimiento de JVM.


Además, se configuran directorios para guardar los resultados de diagnóstico del servicio.

Inicio sesión

Si necesita ver los registros de cualquier contenedor en un archivo, lo cual probablemente sea necesario durante la etapa de configuración y redacción del escenario de prueba, puede utilizar las siguientes instrucciones al describir el contenedor:

 .withLogConsumer(new FileHeadLogConsumer("${reportDirectory}/logs/${alias}.log"))


En este caso, se utiliza la clase FileHeadLogConsumer , que permite escribir una cantidad limitada de registros en un archivo. Esto se hace porque probablemente no sea necesario el registro completo en escenarios de pruebas de carga, y un registro parcial será suficiente para evaluar si el servicio está funcionando correctamente.

Implementación de Pruebas de Carga

Existen muchas herramientas para pruebas de carga. En este artículo, propongo considerar el uso de tres de ellos: Gatling, Wrk y Yandex.Tank. Las tres herramientas se pueden utilizar de forma independiente.

gatito

Gatling es una herramienta de prueba de carga de código abierto escrita en Scala. Permite la creación de escenarios de prueba complejos y proporciona informes detallados. El archivo de simulación principal de Gatling está conectado como un recurso de Scala al módulo, lo que hace que sea conveniente trabajar con toda la gama de soporte de IntelliJ IDEA, incluido el resaltado de sintaxis y la navegación a través de métodos para referencia de documentación.


La configuración del contenedor para Gatling es la siguiente:

 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()

La configuración es casi idéntica a la de otros contenedores:

  • Monte el directorio para informes de reportDirectory .
  • Monte el directorio para los archivos de configuración workingDirectory .
  • Monte el directorio para archivos de simulación workingDirectory .


Además, los parámetros se pasan al contenedor:

  • La variable de entorno SERVICE_URL con el valor de URL para el servicio Sandbox. Aunque, como se mencionó anteriormente, el uso de alias de red permite codificar la URL directamente en el código del escenario.


  • El comando -s MainSimulation para ejecutar una simulación específica.


Aquí hay un recordatorio de la estructura del archivo fuente del proyecto para comprender qué se pasa y dónde:

 load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing

Dado que este es el contenedor final y esperamos obtener resultados una vez finalizado, establecemos la expectativa .withRegEx(".*Please open the following file: /opt/gatling/results.*") . La prueba finalizará cuando aparezca este mensaje en los registros del contenedor o después 60 * 2 segundos.


No profundizaré en el DSL de los escenarios de esta herramienta. Puede consultar el código del escenario utilizado en el repositorio del proyecto .

trabajo

Wrk es una herramienta de prueba de carga sencilla y rápida. Puede generar una carga importante con recursos mínimos. Las características clave incluyen:

  • Soporte para scripts Lua para configurar solicitudes.
  • Alto rendimiento gracias al subproceso múltiple.
  • Facilidad de uso con dependencias mínimas.


La configuración del contenedor para Wrk es la siguiente:

 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()


Para que Wrk funcione con solicitudes al servicio Sandbox, se requiere la descripción de la solicitud a través de un script Lua, por lo que montamos el directorio del script workingDirectory . Usando el comando, ejecutamos Wrk, especificando el script y la URL del método de servicio de destino. Wrk escribe un informe en el registro basado en sus resultados, que puede usarse para establecer expectativas.

Yandex.tanque

Yandex.Tank es una herramienta de prueba de carga desarrollada por Yandex. Admite varios motores de prueba de carga, como JMeter y Phantom. Para almacenar y mostrar los resultados de las pruebas de carga, puede utilizar el servicio gratuito Overload .


Aquí está la configuración del contenedor:

 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()


La configuración de prueba de carga para Sandbox está representada por dos archivos: load.yaml y ammo.txt . Como parte de la descripción del contenedor, los archivos de configuración se copian en reportDirectory , que se montará como directorio de trabajo. Aquí está la estructura de los archivos fuente del proyecto para comprender qué se pasa y dónde:

 load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- yandex-tank/ | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py

Informes

Los resultados de las pruebas, incluidas las grabaciones y registros de rendimiento de JVM, se guardan en el directorio build/${timestamp} , donde ${timestamp} representa la marca de tiempo de cada ejecución de prueba.


Los siguientes informes estarán disponibles para su revisión:

  • Registros del recolector de basura.
  • Registros de WireMock.
  • Registros de servicio de destino.
  • Registros de trabajo.
  • JFR (Grabación de vuelos Java).


Si se utilizó Gatling:

  • Informe Gatling.
  • Troncos de Gatling.


Si se utilizó Wrk:

  • Registros de trabajo.


Si se utilizó Yandex.Tank:

  • Archivos de resultados de Yandex.Tank, con una carga adicional en Overload .
  • Registros de Yandex.Tank.


La estructura del directorio para los informes es la siguiente:

 load-tests/ |-- build/ | |-- ${timestamp}/ | | |-- gatling-results/ | | |-- jfr/ | | |-- yandex-tank/ | | |-- logs/ | | | |-- sandbox.log | | | |-- gatling.log | | | |-- gc.log | | | |-- wiremock.log | | | |-- wrk.log | | | |-- yandex-tank.log | |-- ${timestamp}/ | |-- ...

Conclusión

Las pruebas de carga son una fase crucial en el ciclo de vida del desarrollo de software. Ayuda a evaluar el rendimiento y la estabilidad de una aplicación bajo diversas condiciones de carga. Este artículo presentó un enfoque para crear un entorno de prueba de carga utilizando Testcontainers, que permite una configuración fácil y eficiente del entorno de prueba.


Los Testcontainers simplifican significativamente la creación de entornos para pruebas de integración, brindando flexibilidad y aislamiento. Para las pruebas de carga, esta herramienta permite implementar los contenedores necesarios con diferentes versiones de servicios y bases de datos, lo que facilita la realización de pruebas y mejora la reproducibilidad de los resultados.


Los ejemplos de configuración proporcionados para Gatling, Wrk y Yandex.Tank, junto con la configuración del contenedor, demuestran cómo integrar de manera efectiva varias herramientas y administrar los parámetros de prueba.


Además, se describió el proceso de registro y guardado de resultados de pruebas, que es esencial para analizar y mejorar el rendimiento de las aplicaciones. Este enfoque se puede ampliar en el futuro para admitir escenarios más complejos y la integración con otras herramientas de seguimiento y análisis.


¡Gracias por su atención a este artículo y buena suerte en su esfuerzo por escribir pruebas útiles!