Développeurs, Faites Exploser Vos APIs 🧨
Cet article fait suite à une présentation faite aux Human Talks en septembre 2022. Les slides sont disponibles ici.
Pendant ma mission chez RCA en 2021/2022, j’ai intĂ©grĂ© une Ă©quipe travaillant sur des API publiques mises Ă disposition derrière un API Manager, Gravitee. Cette plateforme permet, entre autres, de contrĂ´ler et de sĂ©curiser nos APIs. Par exemple, le “rate limit” consiste Ă limiter le nombre d’appels Ă une API dans un intervalle de temps donnĂ©.
Malgré la mise en place de cette protection, il était important, pour nous, de pouvoir estimer la limite d’utilisation de notre API et de notre plateforme. Avec l’arrivée de nouveaux clients, l’utilisation de l’API, et donc les montées en charge, vont être progressives.
Le terme “tir de charge” peut être défini de manières différentes en fonction des profils des personnes ou des entreprises.
Pour nous, l’exécution de tirs de charge consiste à simuler un nombre important d’appels à notre API sur un temps très court, l’objectif étant d’arriver à “faire exploser” notre API.
Gatling : notre premier test de charge
Pour trouver ces limites, nous nous sommes orientés vers la solution Gatling. Pourquoi ne pas utiliser d’autres outils plus “rapides” comme wrk, un outil en ligne de commande n’impactant pas les développements ?
Chez RCA, l’éco-système des tests est construit avec l’API Karaté, un outil de test automatisé qui se veut simple d’utilisation, que l’on soit développeur·se ou personne moins technique.
Avec le langage Gherkin, nous allons pouvoir rédiger nos scénarios de tests. Par exemple :
Feature: Guess the word
# The first example has two steps
Scenario: Maker starts a game
When the Maker starts a game
Then the Maker waits for a Breaker to join
Gatling peut réutiliser ces scénarios Karaté pour exécuter des tirs de charge. Son intégration dans une API est très simple. Il suffit de récupérer la dépendance karate-gatling :
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-gatling</artifactId>
<scope>test</scope>
</dependency>
Lorsque nous avons initié ces travaux autour des tests de charge, seul le DSL Scala était disponible. Un DSL Java a depuis été créé.
class GatlingSimulation extends Simulation{
val test = scenario("tirs-charge").exec(karateFeature("classpath:scenarios/charge/tirs-charge.feature"))
Après avoir créé un profile Maven dédié à ces tests de charge, la commande suivante permet d’exécuter notre classe GatlingSimulation : mvn clean test-compile gatling:test
Gatling génère par la suite un rapport HTML contenant des informations complètes sur le nombre de requêtes envoyées, les temps de réponse moyens, les statuts des requêtes, telles que le montrent les rapports suivants :
Rapidement, nous pouvons constater sur le premier graphique que les indicateurs semblent à première vue corrects. L’API a répondu aux 3 000 requêtes effectuées en 800 millisecondes maximum. Pour une API et au vu de notre expérience, c’est une valeur tout à fait raisonnable.
Cette tendance est confirmée dans le diagramme en barres. Notre API a répondu sous les 200 ms.
Les fourchettes de temps de réponse, visibles sur les graphiques (800ms, 1200ms) sont des valeurs de base définies par Gatling.
L’intégration | automatisation dans CI/CD
Une fois le scénario Gatling écrit, le tester en l’exécutant sur son propre poste est tout à fait possible. Un tir de charge sur une API déployée sur son propre ordinateur l’est aussi. Mais à quoi bon faire ce test en local ou bien à partir de son poste ? Chaque tir dépendra de la puissance et de la capacité de votre ordinateur à cet instant. Si notre objectif est de trouver des limites à notre API, cela n’aboutira pas à un bilan pertinent.
Pour avoir des résultats interprétables, nous avons déployé notre API sur une infrastructure “testing”, quasiment équivalente à celle de production. Elle est légèrement sous-dimensionnée.
Comme dit précédemment, notre objectif était de nous donner une limite de sollicitation pour que notre API ait des temps de réponse acceptables. Il fallait donc que l’infrastructure exécutant les scénarios Karaté soit assez puissante, ce qui n’est pas le cas de nos ordinateurs.
A quoi bon devoir installer des outils comme Maven pour pouvoir exécuter un tir de charge ?
L’interface utilisateur de GitLab, assez simple d’utilisation, permet à tout profil de déclencher rapidement des pipelines. Chez RCA, le mécanisme exécutant les pipelines, les GitLab Runner, est installé sur une infrastructure puissante.
Nous avons créé un nouveau job “🧯tirs de charge sur testing 🔥” dans notre pipeline GitLab existant pour rendre possible l’exécution un tir de charge sur notre API déployée sur l’environnement iso production.
Les problèmes rencontrés (et solutions)
Infrastructure as code
L’appel Ă notre API est rĂ©alisĂ© après avoir rĂ©cupĂ©rĂ© un jeton d’authentification via Gravitee. Nous avions dĂ©fini un “rate limit” Ă 1000 appels pour une pĂ©riode de 10 minutes. Lors de notre tir de charge nous Ă©tions sur plusieurs dizaines de milliers d’appels en 1 minute. Le “rate limit” est logiquement atteint !
Malgré une modification de l’infrastructure de test pour désactiver ce “rate limit”, un autre point de blocage est intervenu.
Call Single
Dans notre scénario Karaté, 1 appel à l’API provoque 1 appel à Gravitee. Pour plusieurs dizaines de milliers d’appels, nous avons donc le même volume de jetons créés. Cependant notre tir de charge n’a pas pour objectif de tester la robustesse de Gravitee. D’autant plus que l’équipe ayant mis en place cette infrastructure s’était assurée de sa performance.
Et d’un autre point de vue, générer autant de jetons n’a pas de sens, autant en créer un seul pour nos tests.
Pour faire cela, l’API Karaté met à disposition une méthode callSingle pour n’appeler qu’une seule fois une ressource, dans notre cas la génération du jeton.
var result = karate.callSingle('classpath:scenarios/api/authentification.feature', config);
Le résultat de l’exécution de cette commande peut se vérifier dans le tableau généré suivant, au niveau de la ligne entourée en rouge : 1 seule exécution sur la ressource POST /bearer/token a été produite.
Cela a pour conséquence de devoir exécuter notre tir de charge pendant la validité du jeton, sous peine de générer un nombre important d’erreurs 401 - Unauthorized.
Analyse des résultats
Pour déterminer une limite à notre API, nous avons réalisé plusieurs séries de tirs de charge. Démarrant à quelques centaines d’appels en une minute, nous avons progressivement augmenté le nombre d’appels, réduit le temps d’exécution, jusqu’à avoir des temps de réponse dégradés pour une mise à disposition auprès de nos clients.
Dans les graphiques suivants générés par Gatling, deux informations sont disponibles : le nombre de requêtes et le nombre d’utilisateurs à un instant t.
Ces données sont stables et montrent qu’aucun point de blocage n’est rencontré pendant le tir. Le nombre de requêtes est stable.
En plus des schémas, Gatling offre un rapport textuel avec le nombre de requêtes pendant le tir, le temps de réponse maximum ainsi que des statistiques sur la répartition des temps de réponse.
Une donnée importante et représentative de la santé de notre API est la consommation de CPU. Cette information est disponible dans les tableaux de bord Grafana existant au sein de RCA qui vont nous permettre de valider notre interprétation des résultats issus de Gatling.
Nous sommes arrivés à identifier une limite pour notre API. Un tir simulant 50 000 utilisateurs sur 30 secondes nous donne des temps de réponse acceptables, c’est-à -dire sous la barre des 800 ms.
La plateforme collaborative créée par RCA a une volumétrie en production de 12 000 à 16 000 utilisateurs par heure. Sachant qu’en plus, l’environnement est sous dimensionné par rapport à la production, ces 50 000 utilisateurs en 30 secondes pourront largement solliciter notre API.
Les apports de cette expérimentation
L’intégration des tirs de charge dans notre équipe était une découverte. Après avoir pris en main l’API Karaté et l’extension Gatling, nous avons rapidement pu exploiter nos premiers résultats et nous donner un ordre de grandeur de la capacité d’appels de notre API.
Heureusement que ces tests ont été réalisés, ils nous ont permis de détecter et corriger un problème de performance. Cela est rassurant pour la suite des évolutions et nous protégera d’autres problèmes du même genre.
L’intégration des tests de charge dans toute l’équipe est une bonne chose. Avec la sensibilisation et l’automatisation des tirs de charge dans GitLab CI, tout profil de personne peut exécuter des tirs de charge, récupérer le graphique et en faire une première interprétation.
Sylvain Naël (RCA) / Jean-Philippe Baconnais (Zenika)
Retrouvez cet article sur le compte Medium de Sylvain et d’ici quelques jours sur le blog de RCA.