Architecture logicielle

Méthodologie des tests de charge : modèles de charge, types de tests et analyse

Intermédiaire12 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

Un test de charge est une expérience, et la plupart échouent en tant qu'expériences bien avant de faire tomber le logiciel. Ce guide présente les différents types de tests et la question à laquelle chacun répond, la construction d'un modèle de charge à partir du trafic réel, le degré de fidélité d'environnement dont vous avez vraiment besoin, les métriques à collecter des deux côtés, et la lecture d'un résultat pour trouver le point de saturation plutôt qu'un chiffre à mettre dans une slide.

Ce que vous allez apprendre

  • Choisir le bon type de test — charge, stress, endurance, pic, capacité — selon la question posée
  • Construire un modèle de charge à partir de données de production plutôt que d'hypothèses
  • Concevoir la montée en charge, le temps de réflexion et les données de test pour que le test ressemble au trafic réel
  • Collecter les métriques côté client et côté serveur nécessaires pour interpréter un tir
  • Identifier le point de saturation et distinguer un vrai goulot d'étranglement d'un artefact du test

La plupart des tests de charge produisent un chiffre plutôt qu'une réponse. Une équipe lance un script, note que le système a encaissé 1 200 requêtes par seconde, le met dans un rapport, et n'apprend rien qui change une décision. Le problème vient rarement de l'outil ; c'est que le test n'a jamais été formulé comme une question. « Tiendrons-nous le jour des soldes vendredi ? » et « pourquoi le p99 se dégrade-t-il au bout de deux heures ? » appellent des expériences complètement différentes, et aucune des deux ne se résout avec un simple chiffre de débit.

Traitez un test de performance comme une expérience : une hypothèse, des conditions maîtrisées, des mesures des deux côtés du réseau et une analyse qui confirme ou réfute l'hypothèse. Ce guide explique comment construire cette expérience. Diagnostiquer ce que révèlent les résultats — profilage, GC, accès à la base de données — fait l'objet du guide performance applicative.

Les types de tests et les questions auxquelles ils répondent

Les types se distinguent par la forme de la charge et par la durée, et se tromper de type produit une réponse fausse, mais assurée.

TypeForme de la chargeQuestion à laquelle il répond
SmokeTrès faible, quelques minutesLe script fonctionne-t-il et l'environnement répond-il seulement ?
ChargePic attendu, stableTenons-nous nos objectifs au trafic prévu ?
StressCroissante jusqu'à la ruptureOù le système casse-t-il, et comment ?
CapacitéPaliers successifsQuel est le débit maximal soutenable, et qu'est-ce qui le limite ?
Endurance (soak)Modérée, des heures à des joursQuelque chose se dégrade-t-il avec le temps — fuites, fragmentation, croissance des logs ?
Pic (spike)Saut brutal puis chuteLe système survit-il à un afflux soudain, et s'en remet-il ?
ScalabilitéCharge par unité de ressourceDoubler les instances double-t-il le débit ?

Deux d'entre eux sont systématiquement sous-estimés. Les tests d'endurance trouvent les défaillances qui n'apparaissent qu'avec le temps : une collection qui grossit lentement, des fuites de connexions, un cache qui n'évince jamais rien, des disques qui se remplissent de logs. Ce sont ces défaillances qui font tomber la production à 3 heures du matin le quatrième jour, et aucun test court ne peut les révéler. Les tests de stress valent moins pour le chiffre de rupture que pour le comportement à la rupture : un système qui rejette l'excédent de charge avec des réponses 429 rapides et se rétablit quand la charge retombe vaut infiniment mieux qu'un système qui met en file d'attente jusqu'à épuiser sa mémoire, même si les deux cassent au même débit.

Construire le modèle de charge

Le facteur qui détermine le plus si un test prédit quoi que ce soit, c'est la ressemblance entre la charge modélisée et la charge réelle. Un trafic uniforme sur un seul endpoint est facile à scripter et ne vous apprend rien.

Partez des faits. Les logs d'accès, les statistiques de la gateway et les traces donnent la distribution réelle : quelles opérations, dans quelles proportions, à quel rythme, avec quelle variété de paramètres. Prenez une heure de pointe représentative plutôt qu'une moyenne journalière, car ce sont les pics qui dimensionnent la capacité.

Modélisez des processus métier, pas des endpoints. Les vrais utilisateurs suivent des parcours — naviguer, chercher, consulter un produit, ajouter au panier, payer — avec un taux d'abandon réaliste entre les étapes. Un modèle où 100 % des utilisateurs virtuels vont jusqu'au paiement surestime énormément la charge en écriture et sous-estime la recherche.

Scenario mix for a retail peak hour (from 30 days of access logs):

  Browse-only ........ 62%   home → category → 3 × product
  Search-driven ...... 24%   search → 2 × product → add to basket (40% of these)
  Checkout ............ 9%   basket → address → payment → confirmation
  Account admin ....... 4%   login → orders → order detail
  API partners ........ 1%   steady 25 req/s, no think time

Peak measured arrival rate: 340 sessions/minute
Read:write ratio: 94:6

Le temps de réflexion n'est pas facultatif. Les vrais utilisateurs marquent des pauses entre leurs actions, ce qui modifie la concurrence pour un taux d'arrivée donné — et, d'après la loi de Little, le nombre de sessions nécessaires pour produire un débit donné. Supprimer le temps de réflexion ramène 5 000 utilisateurs réalistes à la charge d'environ 200, et produit un test qui martèle un seul chemin de code avec une localité de cache irréaliste. Utilisez une distribution, pas une constante : une pause aléatoire entre 3 et 12 secondes vaut bien mieux qu'une pause fixe de 5.

Variez les données. Si chaque utilisateur virtuel demande le même identifiant produit, vous mesurez votre cache, pas votre système. Utilisez une distribution de clés réaliste — généralement une longue traîne avec quelques éléments vraiment très sollicités — et des données uniques là où l'unicité compte, pour que le test ne se heurte pas par accident à des contraintes d'unicité ou ne remplisse pas une seule partition.

Modélisez les arrivées, pas seulement la concurrence. Les outils en modèle fermé maintiennent N utilisateurs virtuels qui attendent chacun une réponse avant de continuer : la charge offerte baisse donc automatiquement quand le système ralentit — c'est le problème de la coordinated omission, et il vous flatte. Les outils en modèle ouvert injectent à un taux d'arrivée fixé quel que soit le temps de réponse, ce qui correspond au comportement réel du trafic Internet et à la manière dont les files d'attente se forment vraiment. Privilégiez l'injection par taux d'arrivée pour tout ce qui est exposé au public.

Environnement et données de test

Une fidélité parfaite est hors de prix ; la question utile est de savoir quelles différences faussent le résultat.

FacteurImportanceRéductible en général
Volume et distribution des donnéesCritique — les plans d'exécution changent avec la cardinalitéRarement ; utilisez un volume de production
Topologie applicativeOui — nombre de sauts, comportement du load balancingMême forme, moins d'instances
Dimensionnement de la baseOui ; c'est généralement le goulot d'étranglementSeulement avec une interprétation prudente
Chemin réseau et latenceOui pour les architectures bavardesSimulez une latence supplémentaire
Dépendances tiercesOuiStub à latence réaliste, queue de distribution comprise
Génération de CPU, type de disqueOui pour les valeurs absoluesComparez uniquement des résultats relatifs

Le volume de données mérite qu'on insiste. Un plan d'exécution qui utilise un index sur 10 000 lignes peut basculer vers un parcours séquentiel à 10 millions : un test sur une petite base valide le fonctionnel et rien du tout en matière de performance. Si vous ne pouvez pas copier les données de production, générez un jeu de même volume, même cardinalité et même asymétrie — y compris le client aux 40 000 commandes qui fait tomber toutes les hypothèses de pagination.

Les stubs de services tiers doivent reproduire la distribution de latence de la dépendance, pas sa moyenne. Une API partenaire qui répond en 80 ms avec, de temps en temps, une valeur aberrante à 3 secondes mettra à l'épreuve vos timeouts et votre logique de retry ; un stub qui répond toujours en 80 ms, non. Quand c'est possible, incluez un mode de panne dans certains tirs.

Enfin, réduisez l'échelle en toute honnêteté. Les résultats d'un environnement au quart de la taille ne prédisent la production que si le système passe à l'échelle linéairement et que la base est réduite dans la même proportion — exactement ce que vérifie un test de scalabilité. Énoncez l'hypothèse de mise à l'échelle dans le rapport au lieu de laisser le lecteur supposer une fidélité que vous n'aviez pas.

Écrire le test

Trois outils dominent, et le choix compte moins que le modèle qui se trouve derrière.

JMeter est mature, piloté par une interface graphique avec un plan de test en XML, dispose d'un immense écosystème de protocoles et de plugins, et les équipes de test le connaissent bien. Ses threads correspondent aux utilisateurs virtuels : une très forte concurrence exige donc des injecteurs distribués, et versionner le plan XML est pénible, mais faisable.

Gatling exprime les scénarios dans un DSL Scala ou Java, s'appuie sur un moteur asynchrone qui permet à un seul injecteur de soutenir une forte concurrence, et produit de bons rapports HTML. Il convient aux équipes à l'aise avec un langage JVM qui veulent relire leurs tests comme du code.

k6 utilise JavaScript, se scripte facilement et produit des diffs lisibles, prend en charge nativement les executors à taux d'arrivée et les seuils qui font échouer un tir automatiquement, et s'intègre bien aux pipelines.

// k6: open model — inject a fixed arrival rate regardless of response time.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';

const checkoutLatency = new Trend('checkout_latency', true);

export const options = {
  scenarios: {
    browse: {
      executor: 'ramping-arrival-rate',
      startRate: 20, timeUnit: '1s',
      preAllocatedVUs: 400, maxVUs: 2000,
      stages: [
        { target: 120, duration: '5m' },   // ramp
        { target: 120, duration: '20m' },  // steady state — this is what we measure
        { target: 0,   duration: '2m' },   // ramp down
      ],
    },
  },
  thresholds: {
    'http_req_failed': ['rate<0.005'],                 // fail the run above 0.5% errors
    'checkout_latency': ['p(95)<800', 'p(99)<2000'],
  },
};

const products = JSON.parse(open('./product-ids.json'));   // realistic key distribution

export default function () {
  const id = products[Math.floor(Math.random() * products.length)];

  const home = http.get(`${__ENV.BASE}/`, { tags: { name: 'home' } });
  check(home, { 'home 200': (r) => r.status === 200 });
  sleep(Math.random() * 9 + 3);                             // think time 3–12 s

  const product = http.get(`${__ENV.BASE}/products/${id}`, { tags: { name: 'product' } });
  check(product, { 'product 200': (r) => r.status === 200 });
  sleep(Math.random() * 6 + 2);

  if (Math.random() < 0.09) {
    const res = http.post(`${__ENV.BASE}/orders`,
      JSON.stringify({ sku: id, quantity: 1 }),
      { headers: { 'Content-Type': 'application/json', 'X-Synthetic': 'loadtest' },
        tags: { name: 'checkout' } });
    checkoutLatency.add(res.timings.duration);
    check(res, { 'order created': (r) => r.status === 201 });
  }
}

Remarquez les tags sur chaque requête : sans étiquetage par opération, un p95 agrégé mélange un asset statique à 20 ms et un paiement à 900 ms, et ne décrit ni l'un ni l'autre. Remarquez aussi l'en-tête X-Synthetic, qui permet à la plateforme d'exclure le trafic de test des métriques métier.

Quel que soit l'outil, gardez les scripts de test dans le dépôt, à côté de l'application, faites-les relire et paramétrez l'environnement. Un test de charge qu'une seule personne sait lancer depuis son portable cesse d'être lancé.

Les métriques à collecter

Les chiffres côté client vous disent ce que vivraient les utilisateurs ; les chiffres côté serveur vous disent pourquoi. Il vous faut les deux, sur la même horloge.

Côté injecteur : le taux d'arrivée obtenu par rapport au taux demandé (un écart signifie que l'injecteur est saturé et que tous les autres chiffres sont suspects), les percentiles de temps de réponse par opération, le taux d'erreur ventilé par code de statut et par cause, les erreurs de connexion et les timeouts séparément des erreurs HTTP, et le volume de données transférées.

Côté système : les signaux exposés par Actuator et l'observabilité — débit de requêtes, taux d'erreur et percentiles de latence par service et par endpoint ; CPU, mémoire, disque et réseau par instance ; nombre de threads actifs et profondeur des files des pools de threads ; utilisation du pool de connexions et temps d'attente d'acquisition ; durée et fréquence des pauses GC ; transactions par seconde, attentes de verrous, nombre de requêtes lentes et taux de succès des caches côté base ; et profondeur des files sur tout broker.

Surveillez aussi l'injecteur lui-même. Un injecteur à 95 % de CPU, à court de ports éphémères ou subissant ses propres pauses GC rapporte une latence qui ne dépend que de lui. Surveillez-le avec autant de soin que le système testé, et distribuez l'injection dès qu'une seule machine n'est plus à l'aise.

Lire les résultats

Tracez le débit, le temps de réponse et le taux d'erreur en fonction de la charge offerte, sur le même axe temporel : la forme des courbes raconte l'histoire.

Quand la charge augmente, le débit croît à peu près linéairement tandis que le temps de réponse reste plat : le système a de la marge. Puis la croissance du débit ralentit et le temps de réponse commence à grimper — c'est le coude, où une ressource approche de la saturation et où des files d'attente commencent à se former. Poussez plus loin et le débit plafonne ou baisse tandis que le temps de réponse s'envole et que des erreurs apparaissent : c'est la saturation, et au-delà l'effondrement, où le débit chute parce que le système consacre ses efforts aux timeouts, aux retries et aux changements de contexte.

Le chiffre qui mérite d'être rapporté est la charge offerte la plus élevée à laquelle le système tient encore ses objectifs — pas le débit maximal observé, qui survient généralement à une latence que personne n'accepterait. Répondez ensuite à la question suivante : qu'est-ce qui l'a limité ? Un CPU saturé, un pool de connexions plein, un verrou, un service en aval, ou l'injecteur. Si vous ne pouvez pas nommer la ressource limitante, le test a produit un chiffre plutôt qu'une réponse.

Quelques motifs récurrents et leur signification habituelle :

  • La latence augmente alors que le CPU reste bas — le système attend quelque chose : un pool, un verrou, un appel en aval.
  • Le débit plafonne bien en dessous des attentes alors que le système semble sain — un plafond de concurrence artificiel : taille du pool de threads, pool de connexions, sémaphore, ou limite propre à l'injecteur.
  • Les erreurs n'apparaissent qu'au-delà d'un seuil — une file qui déborde ou un timeout qui expire ; identifiez lequel et vérifiez si c'est le comportement attendu.
  • Les temps de réponse se dégradent progressivement sur plusieurs heures à charge constante — une fuite, une croissance non bornée ou un gonflement d'index. Prolongez en test d'endurance.
  • Un changement brutal au bout de quelques minutes — un cache qui se remplit, un batch qui démarre ou la compilation JIT qui se termine ; vérifiez ce qui d'autre se déclenche à ce rythme.

Comparez à une référence issue du même environnement plutôt qu'à un objectif absolu venant d'un autre système. Et répétez : trois tirs d'une même configuration permettent de dire si un écart de 7 % est une régression ou du bruit.

Les erreurs courantes

Tester un seul endpoint. On mesure un seul chemin de code avec une localité de cache parfaite, ce qui ne prédit rien du trafic mixte de production.

Pas de temps de réflexion. La concurrence comme le comportement des caches sont faux, et dans le sens qui flatte le système.

Rapporter la moyenne. Comme dans tout travail sur la latence, la moyenne masque précisément le comportement qui compte.

Un jeu de données minuscule ou irréaliste. Les plans d'exécution diffèrent, les caches contiennent tout et l'asymétrie disparaît.

Ignorer la phase de chauffe. Mesurer pendant la compilation JIT, avec des pools froids et des caches vides, donne des chiffres pessimistes qui masquent le problème en régime stable.

Ne pas réinitialiser entre les tirs. Les données grossissent, les caches restent chauds, et chaque tir mesure un système différent.

Ignorer le client. Un injecteur saturé fait paraître sain un système qui ne l'est pas.

Ne tester que le chemin nominal. Pas de dépendance lente, pas de panne partielle, pas de tempête de retries : le comportement de résilience, celui qui décide si un incident tourne à la panne générale, n'est jamais éprouvé.

Le lancer une seule fois, avant la mise en production. Les performances régressent à chaque release. Un test automatisé court dans le pipeline de livraison, complété par un test complet planifié : voilà ce qui maintient le résultat valable.

Points clés à retenir

  • Formulez chaque test comme une question et choisissez le type — charge, stress, capacité, endurance, pic — qui y répond ; l'endurance et le stress révèlent les défaillances qui font le plus mal.
  • Le modèle de charge fait la valeur du résultat : répartition réelle des opérations, scénarios métier avec abandons, temps de réflexion aléatoire, données variées et injection par taux d'arrivée.
  • Le volume et la distribution des données comptent plus que la plupart des autres écarts d'environnement ; remplacez les services tiers par des stubs dont la distribution de latence est réaliste, et non constante.
  • Étiquetez chaque requête par opération, gardez les scripts sous contrôle de version, et enregistrez version, configuration et jeu de données à chaque tir.
  • Collectez ensemble les métriques côté client et côté serveur, et vérifiez toujours que l'injecteur n'est pas lui-même le goulot d'étranglement.
  • Rapportez la charge la plus élevée qui respecte encore les objectifs et nommez la ressource limitante ; un chiffre de débit sans cause n'est pas une conclusion.

Sources et lectures complémentaires

Questions fréquentes

Peut-on faire des tests de charge en production ?
Beaucoup d'organisations le font, et c'est le seul environnement d'une fidélité réelle. Cela exige des garde-fous : tirer pendant les périodes de faible trafic, marquer les requêtes synthétiques avec un en-tête pour les exclure des métriques métier, plafonner le débit injecté, disposer d'un arrêt d'urgence immédiat et s'assurer que le trafic de test ne peut ni créer de vraies commandes ni envoyer de vrais e-mails. Commencez avec une petite fraction de la capacité de production et augmentez progressivement.
Combien de temps doit durer un test de charge ?
Assez longtemps pour dépasser la phase de chauffe et atteindre le régime stable, ce qui, pour un service JVM, signifie au moins dix à quinze minutes de mesure après la montée en charge, le temps que la compilation JIT, les pools de connexions et les caches se stabilisent. En dessous, vous mesurez le démarrage. Les tests d'endurance répondent à une tout autre question et demandent des heures, voire des jours, pour révéler fuites et fragmentation.
Pourquoi mes résultats varient-ils d'un tir à l'autre ?
Généralement parce qu'un paramètre n'est pas maîtrisé. Les suspects habituels : un environnement partagé avec d'autres trafics, une base de données dont le volume grossit d'un tir à l'autre, des caches restés chauds après un test précédent, des voisins bruyants en environnement virtualisé et un injecteur lui-même saturé. Réinitialisez les données entre les tirs, surveillez le CPU et le réseau de l'injecteur, et répétez chaque configuration au moins trois fois avant de croire à un écart.
Les tests de charge doivent-ils tourner dans le pipeline de CI ?
Un court test de performance de type smoke à chaque build est précieux — quelques minutes sous une charge modeste, en échec au-delà d'un seuil de régression net, ce qui attrape la requête N+1 accidentelle avant le merge. Les tests complets de capacité et d'endurance relèvent d'une exécution planifiée sur un environnement dédié, car ils demandent du temps, une infrastructure stable et un humain pour interpréter le résultat.