paint-brush
Tout ce que vous devez savoir sur le banc de test de charge basé sur des conteneurs de testpar@avvero
223 lectures

Tout ce que vous devez savoir sur le banc de test de charge basé sur des conteneurs de test

par Anton Belyaev15m2024/06/07
Read on Terminal Reader

Trop long; Pour lire

Le but de cet article est de démontrer une approche pour créer une configuration pour les tests de charge de la même manière que les tests d'intégration classiques sont écrits : sous la forme de tests Spock utilisant Testcontainers dans un environnement de projet Gradle. Des utilitaires de test de charge tels que Gatling, WRK et Yandex.Tank sont utilisés.
featured image - Tout ce que vous devez savoir sur le banc de test de charge basé sur des conteneurs de test
Anton Belyaev HackerNoon profile picture

L'utilisation de Testcontainers a radicalement amélioré le processus de travail avec des scénarios de test. Grâce à cet outil, la création d'environnements pour les tests d'intégration est devenue plus simple (voir l'article Isolation dans les tests avec Kafka ). Désormais, nous pouvons facilement lancer des conteneurs avec différentes versions de bases de données, de courtiers de messages et d'autres services. Pour les tests d'intégration, Testcontainers s'est révélé indispensable.


Bien que les tests de charge soient moins courants que les tests fonctionnels, ils peuvent être beaucoup plus agréables. Étudier des graphiques et analyser les performances d'un service particulier peut apporter un réel plaisir. De telles tâches sont rares, mais elles sont particulièrement passionnantes pour moi.


Le but de cet article est de démontrer une approche pour créer une configuration pour les tests de charge de la même manière que les tests d'intégration classiques sont écrits : sous la forme de tests Spock utilisant Testcontainers dans un environnement de projet Gradle. Des utilitaires de test de charge tels que Gatling, WRK et Yandex.Tank sont utilisés.

Création d'un environnement de test de charge

Ensemble d'outils : Gradle + Spock Framework + Testcontainers. La variante d'implémentation est un module Gradle distinct. Les utilitaires de test de charge utilisés sont Gatling, WRK et Yandex.Tank.


Il existe deux approches pour travailler avec l'objet de test :

  • Tester les images publiées ;
  • Création d'images à partir du code source du projet et tests.


Dans le premier cas, nous disposons d'un ensemble de tests de charge indépendants de la version et des modifications du projet. Cette approche est plus facile à maintenir à l’avenir, mais elle se limite à tester uniquement les images publiées. On peut bien sûr construire manuellement ces images localement, mais cela est moins automatisé et réduit la reproductibilité. Lors d'une exécution dans CI/CD sans les images nécessaires, les tests échoueront.


Dans le second cas, les tests sont exécutés sur la dernière version du service. Cela permet d'intégrer des tests de charge dans CI et d'obtenir des modifications des données de performances entre les versions de service. Cependant, les tests de charge prennent généralement plus de temps que les tests unitaires. La décision d’inclure de tels tests dans CI dans le cadre du contrôle qualité vous appartient.


Cet article considère la première approche. Grâce à Spock, nous pouvons effectuer des tests sur plusieurs versions du service pour une analyse comparative :

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

Il est important de noter que l’objectif de cet article est de démontrer l’organisation de l’espace de test, et non les tests de charge à grande échelle.

Service cible

Pour l'objet de test, prenons un simple service HTTP nommé Sandbox, qui publie un point de terminaison et utilise les données d'une source externe pour gérer les requêtes. Le service dispose d'une base de données.

Le code source du service, y compris le Dockerfile, est disponible dans le référentiel du projet spring-sandbox .

Présentation de la structure des modules

Alors que nous approfondissons les détails plus loin dans l'article, je souhaite commencer par un bref aperçu de la structure du module Gradle load-tests pour comprendre sa composition :

 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

Dépôt de projet - https://github.com/avvero/testing-bench .

Environnement

D'après la description ci-dessus, nous voyons que le service a deux dépendances : le service https://external-weather-api.com et une base de données. Leur description sera fournie ci-dessous, mais commençons par permettre à tous les composants du schéma de communiquer dans un environnement Docker — nous décrirons le réseau :

 def network = Network.newNetwork()

et fournissez des alias de réseau pour chaque composant. Ceci est extrêmement pratique et nous permet de décrire statiquement les paramètres d’intégration.

Les dépendances telles que WireMock et les utilitaires de test de charge eux-mêmes nécessitent une configuration pour fonctionner. Il peut s'agir de paramètres pouvant être transmis au conteneur ou de fichiers et répertoires entiers devant être montés sur les conteneurs.


De plus, nous devons récupérer les résultats de leur travail dans les conteneurs. Pour résoudre ces tâches, nous devons fournir deux ensembles de répertoires :


  • workingDirectory — le répertoire des ressources du module, directement dans load-tests/ .


  • reportDirectory — le répertoire des résultats du travail, y compris les métriques et les journaux. Vous en saurez plus à ce sujet dans la section sur les rapports.

Base de données

Le service Sandbox utilise Postgres comme base de données. Décrivons cette dépendance comme suit :

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


La déclaration spécifie l'alias réseau postgres , que le service Sandbox utilisera pour se connecter à la base de données. Pour compléter la description de l'intégration avec la base de données, le service doit être fourni avec les paramètres suivants :

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


La structure de la base de données est gérée par l'application elle-même à l'aide de Flyway, aucune manipulation supplémentaire de la base de données n'est donc nécessaire lors du test.

Demandes moqueuses à https://external-weather-api.com

Si nous n'avons pas la possibilité, la nécessité ou le désir d'exécuter le composant réel dans un conteneur, nous pouvons fournir une simulation de son API. Pour le service https://external-weather-api.com, WireMock est utilisé.


La déclaration du conteneur WireMock ressemblera à ceci :

 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 nécessite une configuration fictive. L'instruction withFileSystemBind décrit la liaison du système de fichiers entre le chemin du fichier local et le chemin à l'intérieur du conteneur Docker. Dans ce cas, le répertoire "${workingDirectory}/src/test/resources/wiremock/mappings" sur la machine locale sera monté sur /home/wiremock/mappings à l'intérieur du conteneur WireMock.


Vous trouverez ci-dessous une partie supplémentaire de la structure du projet pour comprendre la composition des fichiers dans le répertoire :

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


Pour vous assurer que les fichiers de configuration fictifs sont correctement chargés et acceptés par WireMock, vous pouvez utiliser un conteneur d'assistance :

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


Le conteneur d'assistance est décrit comme suit :

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


À propos, IntelliJ IDEA version 2024.1 a introduit la prise en charge de WireMock et l'EDI fournit des suggestions lors de la création de fichiers de configuration fictifs.

Configuration de lancement du service cible

La déclaration du conteneur de service Sandbox se présente comme suit :

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

Les paramètres notables et les paramètres JVM incluent :

  • Collecte d’informations sur les événements de garbage collection.
  • Utilisation de Java Flight Recorder (JFR) pour enregistrer les données de performances JVM.


De plus, des répertoires sont configurés pour enregistrer les résultats de diagnostic du service.

Enregistrement

Si vous avez besoin de consulter les journaux d'un conteneur dans un fichier, ce qui est probablement nécessaire lors de l'étape d'écriture et de configuration du scénario de test, vous pouvez utiliser les instructions suivantes pour décrire le conteneur :

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


Dans ce cas, la classe FileHeadLogConsumer est utilisée, ce qui permet d'écrire une quantité limitée de journaux dans un fichier. Cela est dû au fait que l'intégralité du journal n'est probablement pas nécessaire dans les scénarios de tests de charge, et qu'un journal partiel sera suffisant pour évaluer si le service fonctionne correctement.

Mise en œuvre de tests de charge

Il existe de nombreux outils pour tester la charge. Dans cet article, je propose d'envisager d'en utiliser trois : Gatling, Wrk et Yandex.Tank. Les trois outils peuvent être utilisés indépendamment les uns des autres.

Gatling

Gatling est un outil de test de charge open source écrit en Scala. Il permet la création de scénarios de tests complexes et fournit des rapports détaillés. Le fichier de simulation principal de Gatling est connecté en tant que ressource Scala au module, ce qui facilite l'utilisation de toute la gamme de prise en charge d'IntelliJ IDEA, y compris la coloration syntaxique et la navigation à travers les méthodes de référence dans la documentation.


La configuration du conteneur pour Gatling est la suivante :

 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 configuration est presque identique à celle des autres conteneurs :

  • Montez le répertoire des rapports à partir de reportDirectory .
  • Montez le répertoire des fichiers de configuration à partir de workingDirectory .
  • Montez le répertoire des fichiers de simulation à partir de workingDirectory .


De plus, les paramètres sont transmis au conteneur :

  • La variable d'environnement SERVICE_URL avec la valeur URL du service Sandbox. Cependant, comme mentionné précédemment, l'utilisation d'alias réseau permet de coder en dur l'URL directement dans le code du scénario.


  • La commande -s MainSimulation pour exécuter une simulation spécifique.


Voici un rappel de la structure du fichier source du projet pour comprendre ce qui est transmis et où :

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

Puisqu'il s'agit du conteneur final et que nous espérons obtenir des résultats une fois terminé, nous définissons l'attente .withRegEx(".*Please open the following file: /opt/gatling/results.*") . Le test se terminera lorsque ce message apparaîtra dans les journaux du conteneur ou après 60 * 2 secondes.


Je n'entrerai pas dans le détail des scénarios de cet outil. Vous pouvez consulter le code du scénario utilisé dans le référentiel du projet .

Travail

Wrk est un outil de test de charge simple et rapide. Cela peut générer une charge importante avec un minimum de ressources. Les principales fonctionnalités incluent :

  • Prise en charge des scripts Lua pour configurer les requêtes.
  • Hautes performances grâce au multithreading.
  • Facilité d'utilisation avec un minimum de dépendances.


La configuration du conteneur pour Wrk est la suivante :

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


Pour que Wrk fonctionne avec les requêtes adressées au service Sandbox, la description de la requête via un script Lua est requise, nous montons donc le répertoire de script depuis workingDirectory . À l'aide de la commande, nous exécutons Wrk, en spécifiant le script et l'URL de la méthode du service cible. Wrk écrit un rapport dans le journal en fonction de ses résultats, qui peut être utilisé pour définir les attentes.

Yandex.Tank

Yandex.Tank est un outil de test de charge développé par Yandex. Il prend en charge divers moteurs de tests de charge, tels que JMeter et Phantom. Pour stocker et afficher les résultats des tests de charge, vous pouvez utiliser le service gratuit Overload .


Voici la configuration du conteneur :

 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 configuration des tests de charge pour Sandbox est représentée par deux fichiers : load.yaml et ammo.txt . Dans le cadre de la description du conteneur, les fichiers de configuration sont copiés dans le reportDirectory , qui sera monté en tant que répertoire de travail. Voici la structure des fichiers sources du projet pour comprendre ce qui est transmis et où :

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

Rapports

Les résultats des tests, y compris les enregistrements et les journaux de performances JVM, sont enregistrés dans le répertoire build/${timestamp} , où ${timestamp} représente l'horodatage de chaque exécution de test.


Les rapports suivants seront disponibles pour examen :

  • Journaux du Garbage Collector.
  • Journaux WireMock.
  • Journaux de service cibles.
  • Journaux de travail.
  • JFR (enregistrement de vol Java).


Si Gatling a été utilisé :

  • Rapport Gatling.
  • Bûches Gatling.


Si Wrk a été utilisé :

  • Journaux de travail.


Si Yandex.Tank a été utilisé :

  • Fichiers de résultats Yandex.Tank, avec un téléchargement supplémentaire vers Overload .
  • Journaux Yandex.Tank.


La structure des répertoires des rapports est la suivante :

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

Conclusion

Les tests de charge sont une phase cruciale dans le cycle de vie du développement logiciel. Il permet d'évaluer les performances et la stabilité d'une application dans diverses conditions de charge. Cet article présente une approche pour créer un environnement de test de charge à l'aide de Testcontainers, qui permet une configuration simple et efficace de l'environnement de test.


Les conteneurs de tests simplifient considérablement la création d'environnements pour les tests d'intégration, offrant flexibilité et isolation. Pour les tests de charge, cet outil permet le déploiement des conteneurs nécessaires avec différentes versions de services et de bases de données, facilitant ainsi la réalisation des tests et améliorant la reproductibilité des résultats.


Les exemples de configuration fournis pour Gatling, Wrk et Yandex.Tank, ainsi que la configuration des conteneurs, montrent comment intégrer efficacement divers outils et gérer les paramètres de test.


De plus, le processus de journalisation et de sauvegarde des résultats des tests a été décrit, ce qui est essentiel pour analyser et améliorer les performances des applications. Cette approche peut être étendue à l'avenir pour prendre en charge des scénarios plus complexes et l'intégration avec d'autres outils de surveillance et d'analyse.


Merci de l'attention que vous portez à cet article et bonne chance dans vos efforts pour rédiger des tests utiles !