Tests de charge avec locust et uv
Cette compétence fait passer un projet de « combien peut-il encaisser ? » à un rapport avec des chiffres exploitables par l'équipe. Suivez les étapes dans l'ordre, car chacune alimente la suivante. L'utilisateur prend deux décisions — le niveau de test et le scénario de trafic — et il les prend après avoir vu ce que vous avez trouvé dans le code.
$SKILL est le dossier de cette compétence, le répertoire depuis lequel vous lisez ce SKILL.md. Définissez-le une fois dans le shell (SKILL=/chemin/vers/locust-load-test). Le test de charge lui-même réside dans le dépôt cible, dans loadtest/ ou là où l'utilisateur le choisit.
1 Découvrir -> loadtest/endpoints.json chaque point d'accès, avec ses appelants et indices de coût
2 Classifier -> loadtest/PLAN.md tableau cœur / support / périphérique ; montrer à l'utilisateur
3 Demander -> niveaux + scénario + environnement + contraintes
4 Construire -> loadtest/ (un projet uv) harness.py, payloads/, scripts d'exécution
5 Exécuter -> loadtest/results/<run>/ d'abord le test de fumée, puis les niveaux choisis, le plus risqué en dernier
6 Collecter -> results/<run>/metrics, profiles vue côté serveur, si accessible
7 Rapporter -> loadtest/report.html + constats en langage clair
1. Découvrir les points d'accès
uv run $SKILL/scripts/discover_endpoints.py . -o loadtest/endpoints.json --markdown loadtest/endpoints.md
Le scanner lit le code et n'écrit rien d'autre. Il trouve les déclarations de routes dans les piles courantes : Python, Node, JVM, Go, Ruby, .NET et les frameworks PHP, ainsi que les fichiers OpenAPI, les schémas GraphQL, les protos gRPC, les websockets et SSE. Il résout les préfixes de routeur et les montages quand c'est possible. Il compte aussi les sites d'appel client (fetch, axios, httpx, requests, clients générés, tests, et les liens, formulaires et attributs htmx des templates rendus côté serveur) pour chaque point d'accès, afin de voir ce que l'interface et les services du produit utilisent réellement. Chaque entrée comporte la méthode, le chemin, file:ligne, le gestionnaire, les indices d'authentification, les indices de corps, un indicateur de streaming, les indices de coût, les appels que le gestionnaire effectue, et qui l'appelle.
La liste des routes d'un framework prévaut sur toute analyse statique, car le framework a déjà résolu tous les préfixes, montages et routes générées. Lorsque l'application peut en produire une, utilisez-la comme source de vérité. Cela signifie un document OpenAPI provenant d'un serveur de développement en cours (/openapi.json), flask routes, bin/rails routes, php artisan route:list --json, ou /actuator/mappings de Spring :
uv run $SKILL/scripts/discover_endpoints.py . --route-dump http://localhost:8000/openapi.json -o loadtest/endpoints.json
Ensuite, lisez la sortie de manière critique avant de lui faire confiance :
unmatched_client_callssont soit des routes que l'analyse a manquées, soit des appels vers d'autres services.prefix_resolved: falseetnotesmarquent les suppositions.shared_handler: truesignifie qu'une fonction dessert plusieurs méthodes et que l'analyse n'a pas pu déterminer à quelle méthode appartiennent ses indices de coût ; lisez ce gestionnaire.existing_load_testsliste les fichiers locust ou les scripts k6 déjà présents dans le dépôt. Réutilisez leurs charges utiles.- Vérifiez cinq entrées de manière ponctuelle par rapport aux fichiers sources.
Lisez references/discovery.md lorsque l'inventaire semble maigre, lorsqu'une pile technologique n'est pas couverte, ou lorsque vous avez besoin de la commande de dump de routes pour un framework.
2. Classifier : cœur, support, périphérique
Classez les points d'accès par jugement. Ne les classez pas par mots-clés : le même /search est un point d'accès cœur dans une boutique et un point d'accès support dans une console d'administration.
| Niveau | Ce que c'est | Exemples typiques |
|---|---|---|
| cœur | Ce pour quoi les utilisateurs viennent, et généralement là où se produit le travail lourd : calculs, appels de modèles, traitement de données, chemin principal d'écriture | passage en caisse dans une boutique ; convertir ou extraire dans un service de documents ; complétions dans une API de chat |
| support | Ce qui rend le cœur utilisable | connexion, sessions, paramètres, listes et recherche sur les propres données de l'utilisateur, téléversements qui alimentent le cœur, CRUD d'administration |
| périphérique | Ce pour quoi le produit n'existe pas | santé, métriques, documentation, fichiers statiques, routes de débogage, webhooks provenant de services qu'il ne possède pas |
Éléments à évaluer :
- ce que le README et la documentation disent que le produit est ;
- ce que l'interface utilisateur appelle le plus (
client_calls) et ce que les tests exercent (test_calls) ; - le coût du gestionnaire : suivez les
callsdans la couche de service, carcost_hintsne voit que le corps du gestionnaire. Les appels de modèles, les écritures en base de données, les appels externes, le travail CPU (hachage de mots de passe, compression, images :cpu), le traitement de fichiers et les publications de files d'attente sont ce qui rend un point d'accès coûteux ; - le nommage ;
- ce que la surveillance ou les logs de l'équipe disent être le plus occupé, si vous pouvez les voir.
Écrivez loadtest/PLAN.md avec ce tableau, une ligne par point d'accès que vous avez l'intention de charger, et une courte liste des points d'accès que vous avez omis et pourquoi :
| point d'accès | niveau | pourquoi | indices de coût | source de la charge utile |
|---|---|---|---|---|
| POST /api/orders/{id}/checkout | cœur | le chemin d'achat ; l'interface l'appelle depuis 3 endroits | écritures en base de données, appel API de paiement | tests/fixtures/order.json |
Montrez ce tableau à l'utilisateur avant de poser les questions, et invitez les corrections. Un mauvais niveau fait que chaque nombre ultérieur répond à la mauvaise question, et l'utilisateur connaît son produit mieux que toute analyse automatisée.
3. Demander à l'utilisateur
Utilisez l'outil AskUserQuestion. S'il n'est pas disponible, posez les mêmes questions sous forme d'une liste numérotée simple et attendez les réponses. Ne demandez qu'après que l'utilisateur ait vu le tableau du plan.
- Q1 Niveau cœur. Niveau 1, 2 ou 3.
- Q2 Niveau support. Niveau 1, 2 ou 3, ou laissez le support de côté.
- Q3 Scénario de charge utilisateur. Deux à quatre options dérivées de ce projet (voir ci-dessous), avec celle que vous recommandez en premier, marquée « (Recommandé) », et une ligne expliquant pourquoi elle convient.
- Q4 Environnement cible. Quel déploiement charger (staging, une pile locale, une copie de test de charge). Ne chargez jamais un système de production à moins que l'utilisateur ne le confirme explicitement. La charge se répand sur les bases de données partagées, les locataires et les factures.
| Niveau | Exécutions | Réponses | Coûts |
|---|---|---|---|
| 1 Fumée / référence | charge faible et constante (1 à quelques utilisateurs) pendant quelques minutes | est-ce que chaque chemin fonctionne, et quelle est la latence sans contention ? | minutes ; risque négligeable |
| 2 Charge attendue | montée en charge par paliers jusqu'au pic attendu, maintien, puis une courte période de trempage à ce niveau | atteint-il l'objectif de latence à son point le plus chargé, avec quelle marge ? | environ une heure ; la cible fonctionne à son véritable pic |
| 3 Stress / capacité | montée en charge par paliers jusqu'à ce qu'une règle d'arrêt se déclenche (le point de rupture et le point de défaillance), plus un pic | où casse-t-il, comment, et récupère-t-il ? | une à deux heures ; la cible va saturer et peut tomber, elle a donc besoin d'une fenêtre et de quelqu'un qui surveille |
Dérivez les options de Q3 de ce que le projet dit de son trafic : README et docs (qui l'utilise, nombre de sièges, locataires), configuration (limites de débit, nombres de workers et de répliques, limites de l'autoscaler, tailles de files d'attente, pools de connexions), tâches cron et par lots, et les propres timeouts et tentatives des appelants. Les formes courantes sont une montée en charge par paliers aux heures de bureau, un pic matinal, un trempage constant, une rafale d'intégration provenant d'un autre service, une importation par lots, et un trafic mixte interactif et de fond. Chaque option indique ses chiffres : utilisateurs, taux d'apparition, temps de réflexion, durée et répartition des requêtes. Lorsque le projet ne dit rien de son trafic, dites-le, nommez l'hypothèse sur laquelle chaque nombre repose, et demandez le pic attendu dans Q3.
references/tiers-and-scenarios.md montre comment transformer la configuration en ces nombres. Lisez-la avant de demander.
Après les réponses, réglez deux choses en texte clair :
- Identifiants. Ils proviennent d'un fichier ou d'une variable d'environnement que le harnais lit. Ils ne sont jamais collés dans le chat.
- Contraintes. Listez celles que vous avez trouvées et demandez le reste : limites de débit, fenêtres de licence ou d'activation, ce que coûte un redémarrage, locataires partagés, et le coût par requête (tokens de modèle, API payantes). Un redémarrage dans une fenêtre de licence peut verrouiller un service pendant une journée, et quelques tâches lourdes parallèles avec une seule énorme entrée peuvent faire tomber une cible. Demandez avant la charge, pas après.
- État des données. Combien de lignes les listes et les recherches contiennent dans l'utilisation réelle, si les données peuvent être réinitialisées entre les exécutions, et quelles lignes un utilisateur de test peut modifier ou supprimer. Une table vide rend une page de liste gratuite, et les lignes ajoutées par une exécution font que chaque étape ultérieure lit plus que la précédente.
4. Construire le harnais
mkdir -p loadtest && cd loadtest
cp $SKILL/templates/pyproject.toml . && uv sync # son propre .venv et uv.lock ; locust épinglé, Python 3.11+
cp $SKILL/templates/{harness.py,run_local.sh,run_plan.sh,collect.sh,seed.py,loadtest.env.example} .
cp $SKILL/templates/run_docker.sh . # lorsque le générateur s'exécute sur le réseau Docker de la cible
cp -r $SKILL/templates/{run_k8s.sh,k8s} . # lorsqu'il s'exécute dans le cluster de la cible
git -C .. status --short # seul loadtest/ peut apparaître
N'exécutez pas uv init ici. Dans un dépôt qui est lui-même un projet uv, cela ajoute loadtest à l'espace de travail [tool.uv.workspace] de l'application, installe locust dans le .venv de l'application et écrit un uv.lock au niveau de l'application. Si cela s'est produit, annulez-le : git checkout -- pyproject.toml dans l'application, supprimez le nouveau uv.lock, et exécutez uv sync là-bas. Ensuite, remplissez le modèle :
REQUESTS. Remplissez le tableau dansharness.pyà partir de PLAN.md : niveau, nom, poids, répartition, corps et statut attendu. Prenez les charges utiles dans les tests, les fixtures et les schémas du projet, et placez-les dansloadtest/payloads/. N'utilisez jamais de données de production. Une application rendue côté serveur répond 200 pour sa page de connexion et aussi pour ses pages d'erreur, vérifiez donc ce que seule la bonne page a :expect_text, etexpect_locationlà où une redirection est la réponse. Une ligne qui est redirigée sans le dire échoue.REQ_TIMEOUT. Définissez-le sur le timeout de l'appelant réel : la passerelle, l'annulation du frontend, le SDK client, ou le timeout de lecture du proxy. Le harnais tient chaque requête comme un délai pour la réponse entière, donc une réponse ultérieure échoue en tant que « délai dépassé » au lieu d'être comptée comme un succès que le produit aurait déjà signalé comme un échec.MIX. Donnez à chaque mélange de trafic avec un coût différent sa propre balise, et exécutez chaque mélange comme son propre scénario (entrées courtes de type chat, entrées de type document, une option qui coûte plus cher). Leurs capacités peuvent différer de plusieurs fois, et une exécution mélangée cache la plus coûteuse.- Auth. Configurez
AUTHpour que chaque utilisateur simulé se connecte une fois, pas une fois par requête. Une connexion par formulaire avec un cookie de session a également besoin deLOGIN_EXPECT_LOCATIONouLOGIN_EXPECT_TEXT, car un mauvais mot de passe répond aussi 200 (voirloadtest.env.example). Les connexions obtiennent le stageloginet restent en dehors des numéros de palier et des règles d'arrêt : chacune atterrit au début du palier qui a ajouté son utilisateur, et un hachage de mot de passe lent mettrait fin à l'exécution sur un coût que la charge stable ne paie jamais. Pour charger la connexion elle-même, faites-en une ligne. - Données. Remplissez
seed.pypour l'application et exécutezpython3 seed.py up --users N --rows M: via l'API de l'application, chaque compte de test reçoit ses propres lignes, queSEED_MANIFESTremet à son utilisateur ; les clés de lignecaptureetconsumesuivent ce qu'un utilisateur crée et supprime. Associez les créations avec les suppressions, ou réensemencez entre les exécutions. - Parcours. Les flux en plusieurs étapes (téléverser puis interroger, créer puis lire) sont une méthode sur une classe d'utilisateur ; le modèle a un exemple.
references/harness.md couvre chaque champ, les formes, les modes d'authentification, le streaming, les protocoles non-HTTP et les trois emplacements. Lisez-le pendant la construction.
5. Exécuter en toute sécurité
- D'abord le test de fumée. Exécutez
RUN_NAME=smoke SHAPE=smoke ./run_local.sh: un utilisateur par classe d'utilisateur envoie chaque ligne une fois, dans l'ordre du tableau (mettez la ligne qui crée un identifiant avant les lignes qui l'utilisent), puis reste inactif. Le log se termine parNOTE smoke: every row was sent, ou nomme les lignes qui ne l'ont pas été. Vérifiez zéro échec, latence plausible et corps corrects. Cela prouve le câblage avant que quoi que ce soit ne coûte de la charge. - Vérifiez que les machines sont calmes. Avant une exécution qui mesure la capacité, exécutez
uptimeetvmstat 1 5sur le générateur et sur l'hôte de la cible. D'autres travaux sur les mêmes cœurs transforment chaque nombre en une mesure de la contention. Le harnais échantillonne son propre hôte avant chaque exécution dansrun.json(host_checket une note) ; mettez les chiffres de l'hôte cible dansNOTESlorsqu'il s'agit d'une autre machine. Le rapport met une bannière sur une exécution dont l'hôte ou le CPU du générateur a atteint 90 % pendant ses paliers et met en garde sur son point de rupture. Si une machine est occupée, attendez, épinglez-la sur des cœurs inactifs (taskset), ou déplacez-vous, et dites-le. - Ensuite, les niveaux choisis, du plus sûr au plus risqué. Exécutez la charge attendue, puis le trempage, puis le stress, puis le pic. Les entrées risquées (les plus grandes charges utiles, les options les plus coûteuses) vont en dernier, de sorte que si elles endommagent la cible, rien avant elles n'est perdu.
run_plan.shexécute la liste dans l'ordre et ne démarre chaque exécution que lorsque la vérification de santé de la cible réussit. - Placez le générateur à côté des appelants réels lorsque le chemin réseau est important : le même cluster, VNet ou réseau de composition (
run_k8s.sh,run_docker.sh). Une exécution sur un ordinateur portable mesure autant Internet que le service. Chaque exécution enregistreGENERATOR, et le propre CPU du générateur va àmetrics/client.csv; au-dessus d'environ 90 % de CPU, les nombres décrivent le générateur. - Respectez les règles d'arrêt. Le harnais met fin à une exécution lorsque le taux d'échec dépasse
STOP_FAIL_RATIO, lorsque le p95 dépasseSTOP_P95_MS, lorsque plus de la moitié des 20 dernières secondes ont échoué, ou lorsque la plupart des connexions ont échoué. De nombreux services mettent en file d'attente au lieu de se débarrasser de la charge : zéro erreur, et une latence qui ne fait qu'augmenter. Définissez donc le plafond p95 au timeout de l'appelant ou au SLO. Ne dépassez pas une règle d'arrêt « pour voir ». - Surveillez la santé entre les exécutions et laissez les files d'attente se vider (
COOLDOWNdansrun_plan.sh). - Nettoyez tout ce que vous avez créé :
./run_k8s.sh down(pod, ConfigMaps, Secret), conteneurs arrêtés, utilisateurs de test et données de test. Listez pour l'utilisateur tout ce qui ne peut pas être supprimé, comme les lignes avec suppression logicielle ou les entrées d'audit.
Lisez references/safety.md avant la première exécution au-delà du test de fumée, et avant toute exécution contre un système partagé.
6. Collecter le côté serveur
Les nombres côté client disent ce qui s'est passé. Les nombres côté serveur disent pourquoi. Démarrez les collecteurs avant locust, arrêtez-les après, et écrivez dans le même répertoire d'exécution :
uv run $SKILL/scripts/collect_server_metrics.py . -o results/<run>/metrics --host target_host
Le script interroge les métriques de la cible via les points d'accès de surveillance courants (/metrics Prometheus, /actuator/health, /healthz) lorsqu'ils sont accessibles. En complément, pour une vue plus granulaire des ressources système (CPU, mémoire, E/S disque, réseau), le script host_top.sh peut être exécuté sur l'hôte de la cible pour capturer un échantillonnage (top, iostat, netstat) toutes les 30 secondes et l'enregistrer dans results/<run>/host_top.log. Le rapport combine ces données avec les métriques de locust pour identifier les goulots d'étranglement. Si la cible est un conteneur, utilisez les outils d'inspection du conteneur (docker stats, kubectl top pod) plutôt que les métriques de l'hôte.
7. Rapporter
uv run $SKILL/scripts/generate_report.py results/<run>/ -o loadtest/report.html
Le rapport HTML résume l'exécution : la configuration, les métriques de locust (latence, débit, taux d'erreur par point d'accès et par palier), les métriques du serveur (CPU, mémoire, etc.), les profils CPU (si collectés via py-spy ou async-profiler) et les conclusions en langage clair. Il met en évidence les paliers où les règles d'arrêt ont été déclenchées, le point de rupture (knee) sur le graphique de latence et la façon dont le service échoue (erreurs, timeouts, saturation). Il compare les résultats aux objectifs définis par l'utilisateur (latence cible, débit attendu). Le rapport est conçu pour être partagé avec l'équipe et sert de base aux discussions sur la capacité et les améliorations.
Références utiles dans ce dossier de compétence : references/discovery.md, references/tiers-and-scenarios.md, references/harness.md, references/safety.md. Lisez-les selon les indications dans chaque section.