Testez la robustesse de votre API avec Locust
Notre API fonctionne. Bien ! Mais tient-elle la charge ? C'est parti pour du load testing avec Locust !
Lorsque nous développons des services destinés à être utilisés avec un certain volume, il est crucial de tester notre code.
Les tests unitaires, fonctionnels, d'intégration et end-to-end, sont souvent mis en place dans les projets. C’est très bien, mais nous pouvons aller encore plus loin afin d’être plus serein.
Le load testing (test de charge en français).
L'idée est d'envoyer un grand nombre de requêtes à notre service pour observer sa réaction face à la charge.
Cela inclut l'évaluation de la capacité d'absorption de la charge, du changement des temps de réponse, des éventuelles erreurs dans les réponses, et des comportements inattendus.
Cela permet aussi de déterminer le nombre maximal d'utilisateurs simultanés, afin d'ajuster le sizing ou le nombre de nos machines (vertical scaling vs horizontal scaling).
De plus, ces tests fournissent des informations sur la latency (temps nécessaire pour compléter une action) versus le throughput (nombre d'opérations par unité de temps).
Voici ce magnifique GIF que j’ai réalisé avec la librairie Motion Canvas. Il y a encore un peu de boulot mais voici une illustration de la différence entre Latency et Throughput.
Je vais vous parler de la librairie Locust, une bibliothèque Python permettant de faire du load testing en Python. Locust peut être utilisé via la ligne de commande ou une interface web.
❓Petit moment culture : Locust veut dire Sauterelle / Criquet pour rappeler les 10 plaies d’Egypte. L’idée sous-jacente est que Locust envahit notre API comme les sauterelles ont envahi l’Egypte tel que décrit dans le livre de l’Exode.
Avec Locust, on définit le nombre de connexions simultanées souhaitées (par exemple, 1 000 pour simuler mille utilisateurs), et également un "ramp-up" pour augmenter progressivement la charge. Si nous choisissons de tester 100 connexions simultannées avec un ramp-up de 10 utilisateurs, cela signifie que chaque seconde, nous ajoutons 10 utilisateurs pour monter progressivement la charge.
Nous pouvons choisir les endpoints à cibler et leur attribuer des poids relatifs. Locust permet également de définir un "wait_time" pour simuler de manière plus réaliste le comportement d'un utilisateur, en introduisant des pauses entre les requêtes.
Tester notre première API
Sans plus attendre, voici un exemple. Nous avons une application FastAPI simple avec une seule route sur "/". Nous voulons évaluer combien de requêtes peuvent être gérées par cette application, sur mon ordinateur.
Nous utilisons la commande suivante pour démarrer notre API :
uvicorn app:app --workers 32
J'ai choisi 32 workers car mon ordinateur a 32 cœurs.
Apparemment, la formule idéale serait le nombre de CPU * 2 + 1, donc 65 dans mon cas. Mais n’ayant pas constaté de gain de performance, j’ai gardé 32 workers.
Si vous appréciez la newsletter StuffAndCode, vous pouvez vous inscrire afin de ne manquer aucun article !
🐍 Et recevez en PLUS, un article exclusif sur comment profiler n’importe quel type de code Python.
Ensuite, écrivons notre premier test de charge avec Locust.
Nous créons une classe qui hérite de HttpUser
, instanciée pour chaque utilisateur.
Par exemple, avec 1000 utilisateurs, cette classe sera instanciée 1000 fois.
Nous définissons une seule tâche avec le décorateur @task
.
Si plusieurs tâches sont définies, Locust les choisit aléatoirement. Dans le test test_home_page
, on utilise self.client.get("/")
.
Pour spécifier l’host, nous pouvons utiliser une variable d'environnement ou l'interface web de Locust.
Pour ce tutoriel, j'utilise l'interface web, parce que : why not ?
Après avoir écrit notre test, démarrons Locust avec la commande :
locust -f locustfile_part1.py --processes -1 --modern-ui
L'option -f précise le fichier à exécuter.
--processes -1 permet d'utiliser tous les processeurs disponibles sur notre machine pour envoyer le plus de requêtes possible
L'option --modern-ui utilise une interface web, plus sympa, qui est encore en beta
Sur la page Locust, nous définissons :
le nombre d'utilisateurs à simuler.
le ramp-up par seconde.
la durée du test.
Par exemple, nous pouvons monter à 1000 utilisateurs, envoyer des requêtes pendant 30 secondes, puis arrêter, en spécifiant ces paramètres dans les champs appropriés.
Voici à quoi ressemble l’écran de configuration de notre test de charge.
J'ai configuré le test pour évaluer notre route avec 1 000 utilisateurs, sans spécifier de délai entre les requêtes. Ci-dessous, les résultats.
Les résultats ne montrent aucune erreur sur nos routes, des temps de réponse constants, et pour 1 000 utilisateurs simultanés, nous atteignons un peu plus de 20 000 requêtes par seconde.
Vous pouvez consulter ces résultats dans les tableaux ou graphiques, télécharger les données, consulter les logs, et voir le nombre de requêtes traitées par chaque worker.
Maintenant, passons à quelque chose de plus concrêt.
Une API protégée par du “JWT”
Je vais simuler une API REST utilisant un JWT. Pour simplifier, je considérerai qu'un header avec un Bearer token est valide pour l'authentification.
Voici le code de notre “API REST” :
Nous disposons de trois routes :
Le login qui renvoie un token.
Deux routes identiques simulant une ressource protégée.
Dans notre test Locust :
La méthode on_start
est utilisée pour définir une action spécifique au moment de la création d'un nouvel utilisateur. Par exemple, chaque nouvel utilisateur envoie une requête sur /login
pour récupérer un token, nécessaire pour s'authentifier sur les autres routes.
Nous avons ensuite deux autres tâches envoyant des requêtes sur les routes protégées. Nous avons décoré notre deuxième route avec @task(3)
. Cela nous sert à donner un poids, c’est à dire que cette route a trois fois plus de chances d'être appelée que la première. Car pour rappel, Locust regarde toutes les méthodes décorées de @task
puis choisit de manière random.
L'attribut wait_time
de la classe définit un intervalle aléatoire entre 1 et 5 secondes entre chaque tâche, afin de simuler un comportement utilisateur plus réaliste.
Conclusion
En conclusion, je voudrai souligner que Locust offre des possibilités plus avancées :
envoyer des requêtes depuis plusieurs machines pour simuler des millions d’users.
Bien que nous ayons testé une API REST dans cet article, Locust permet de tester plusieurs protocoles en utilisant différents clients. Plus d'informations sur ces clients sont disponibles ici.
Finalement, il est important de noter que j'ai exécuté le test de charge et l'API sur la même machine par simplicité, mais cela a clairement influencé les résultats.
Cela permet tout de même de détecter des bugs potentiels et d’avoir une vue globale. Pour pousser au maximum l’expérience, il faudrait exécuter le load testing sur une machine distincte.
Si vous appréciez la newsletter StuffAndCode, vous pouvez vous inscrire afin de ne manquer aucun article !
🐍 Et recevez en PLUS, un article exclusif sur comment profiler n’importe quel type de code Python.