Ressources
Test de charge k6 avant d'ouvrir un service sur Kubernetes : seuils, générateur et verdict
k6 load testing avant d'ouvrir un service sur Kubernetes : décision écrite, modèle d'arrivée, seuils qui font échouer l'exécution, générateur vérifié, verdict.
L'équipe CTN Solutions · publié le · 21 min de lecture
Faits vérifiés sur les sources primaires le
Un test de charge qui se termine sans erreur ne dit pas forcément que le service tiendra le trafic prévu. Le script peut avoir émis moins de requêtes que la demande visée, le générateur peut avoir plafonné avant le service, et des vérifications en échec peuvent laisser le code de sortie à zéro. Avant d'ouvrir un service sur Kubernetes, l'essai doit soutenir une décision écrite à l'avance : ouvrir dans le profil essayé, refuser, ou reconnaître que l'essai ne permet pas de conclure.
Faits datés
Ce guide suit cette décision avec Grafana k6, outil de test de charge open source : k6 v2.3.0, dernière version de la branche 2.x au .
Vérifié le
Écrire la décision avant le script
Un test de charge coûte du temps de préparation, un environnement et parfois des données à reconstruire. Il n'a de valeur que s'il répond à une question qui conditionne une action. Avant une mise en production, la question prend une forme précise : le service, dans cette version et cette configuration, sert-il le parcours critique à la demande visée, en respectant les critères convenus, sur l'environnement retenu ? La réponse attendue est une décision, pas un graphique.
La fiche de décision
La fiche tient en une page et se rédige avant la première ligne de script. Elle fixe ce qui ne pourra plus bouger une fois les résultats connus.
- Le parcours critique. Les requêtes que le service doit absolument servir à l'ouverture, avec leur proportion dans le trafic attendu : lecture d'un catalogue, création d'une commande, connexion. Un parcours qui n'est pas représenté dans le script n'est pas couvert par la décision.
- La demande visée. Un taux d'arrivée (requêtes ou parcours par seconde), une montée, une durée de plateau, et l'origine de ces valeurs : journaux d'un service existant, prévision commerciale, campagne datée. Sans origine, le chiffre devient une hypothèse à nommer comme telle.
- Les critères du service. Latence par percentile sur les réponses réussies, taux d'erreurs, contenu des réponses. Si un objectif de niveau de service (SLO, Service Level Objective) existe, il fournit la base de ces critères ; sinon, ils sont convenus avec le responsable du produit et datés.
- Les critères de validité de l'essai. La part maximale de demande non émise, l'état acceptable du générateur et la fenêtre d'observation. Ces critères disent si l'essai a bien testé ce qu'il prétend tester.
- Le décideur et les trois issues. Qui tranche, et ce que déclenche chacune des issues : ouvrir, corriger puis rejouer, ou rejouer avec un générateur corrigé.
Exemple : « Lecture du catalogue, 100 requêtes par seconde au plateau pendant 50 secondes après 10 secondes de montée ; p95 (95e centile) sous 150 ms et p99 (99e centile) sous 300 ms sur les réponses réussies ; moins de 1 % d'échecs HTTP ; au plus 30 itérations abandonnées ; décision par le responsable produit. »
Le type d'essai découle de la question
La documentation k6 distingue six types d'essais. Chacun répond à une question différente, et un seul essai ne les couvre pas tous.
| Type (documentation k6) | Question à laquelle il répond | Place avant une ouverture |
|---|---|---|
| Fumée (smoke) | Le script fonctionne-t-il et le système répond-il correctement sous une charge minimale ? | Toujours en premier |
| Charge moyenne (average-load) | Le système tient-il les critères dans les conditions normales attendues ? | Cœur de la décision |
| Stress | Comment se comporte-t-il au delà de la charge moyenne ? | Si un pic est prévu |
| Endurance (soak) | Reste-t-il fiable sur une longue période ? | Si des fuites ou dérives sont redoutées |
| Pic (spike) | Survit-il à une hausse soudaine, brève et très forte ? | Lancement ou campagne datée |
| Point de rupture (breakpoint) | Où se situent ses limites de capacité ? | Planification de capacité ; avant l'ouverture seulement si la limite doit être connue |
La documentation recommande de commencer par l'essai de fumée, puis de passer à la charge moyenne avant les profils plus complexes. Un test de montée en charge progressif peut donc précéder la décision sans la remplacer : il révèle un goulot, il ne vaut pas acceptation.
Ce que l'essai ne prouve pas
Un test de charge mesure le service dans le profil essayé. Il ne prouve pas qu'une bascule de base de données préserve les données : c'est l'objet d'un plan de tests de panne, décrit dans PostgreSQL sur Kubernetes en haute disponibilité : topologies, bascule, RPO et tests de panne. Il ne prouve pas non plus que les alertes de production sonneront au bon moment ; leur conception relève d'un autre exercice, traité dans Des alertes qui sonnent pour rien : un socle d'observabilité pour une scale-up sans SRE. Profitez en revanche de l'essai pour vérifier que ces alertes réagissent pendant la charge, en le notant dans la fiche.
Choisir le modèle d'arrivée et préparer les données
Le choix le plus lourd de conséquences se fait dans la configuration du scénario : comment les itérations arrivent-elles ? La documentation k6 décrit deux modèles. Dans un modèle fermé, une nouvelle itération ne démarre qu'à la fin de la précédente : la durée d'exécution de chaque itération détermine le nombre d'itérations exécutées. Dans un modèle ouvert, les démarrages sont découplés de la durée des itérations : le service ne contrôle plus la charge qu'il reçoit.
Pourquoi le modèle fermé peut masquer le problème
Si le service ralentit, un modèle fermé attend : les itérations s'allongent et le taux d'arrivée des nouvelles itérations diminue. La documentation k6 rapproche ce phénomène de ce que la littérature appelle l'omission coordonnée (coordinated omission). Le service lent se protège lui-même, et l'essai mesure une charge plus faible que celle prévue. Pour des utilisateurs qui arrivent indépendamment les uns des autres, cette auto-limitation n'existe pas : la demande continue d'arriver pendant que le service ralentit.
Le modèle fermé reste adapté quand le nombre d'utilisateurs simultanés est la grandeur réelle, par exemple un nombre fixe de postes ou de processus clients qui attendent chacun leur réponse. Le modèle ouvert convient à une demande indépendante des temps de réponse, ce qui correspond au trafic d'un site ou d'une API publique. Dans k6, seuls les exécuteurs à taux d'arrivée, constant-arrival-rate et ramping-arrival-rate, implémentent le modèle ouvert.
Ralentissement du service selon le modèle d'arrivée
Le schéma compare ce qui arrive au taux de charge quand le service ralentit, dans un modèle fermé et dans un modèle ouvert.
Schéma défilable horizontalement ; sa version textuelle complète suit.
Lire le schéma sous forme textuelle
- Point de départ : le service ralentit.
- Modèle fermé : une nouvelle itération ne démarre qu'à la fin de la précédente. Les itérations s'allongent, puis le taux d'arrivée des nouvelles itérations diminue : l'essai mesure une charge plus faible que celle prévue.
- Modèle ouvert : les démarrages sont découplés de la durée des itérations. La demande continue d'arriver pendant que le service ralentit, et le service ne contrôle plus la charge qu'il reçoit.
Allouer les utilisateurs virtuels avant l'essai
Un modèle ouvert a besoin d'utilisateurs virtuels (VU, virtual users) libres pour démarrer chaque itération à l'heure prévue. La documentation donne une estimation de départ : preAllocatedVUs égal à la durée médiane d'une itération multipliée par le taux, plus une marge pour la variance. Elle déconseille de compter sur maxVUs : allouer des VUs en cours d'essai a un coût, peut surcharger le générateur et fausser les résultats. Dans presque tous les cas, elle recommande de préallouer les VUs nécessaires. Quand maxVUs n'est pas renseigné, il vaut preAllocatedVUs. Lorsqu'aucun VU n'est libre, k6 n'exécute pas l'itération prévue et l'ajoute au compteur dropped_iterations.
Le scénario ci-dessous est ouvert, avec montée puis plateau. À adapter à votre fiche de décision :
import http from "k6/http";
import { check } from "k6";
import { SharedArray } from "k6/data";
// Données de test lues une fois, partagées en lecture seule entre les VUs
const items = new SharedArray("items", () => JSON.parse(open("./items.json")));
export const options = {
scenarios: {
parcours_critique: {
executor: "ramping-arrival-rate",
startRate: 0,
timeUnit: "1s",
preAllocatedVUs: 50, // dans l'exemple : durée médiane d'itération x taux, plus une marge
maxVUs: 50,
stages: [
{ target: 100, duration: "10s" },
{ target: 100, duration: "50s" },
],
},
},
};
export default function () {
const item = items[Math.floor(Math.random() * items.length)];
const res = http.get(`<URL_DU_SERVICE>/items/${item.id}`, { tags: { name: "catalogue" } });
check(res, {
"statut 200": (r) => r.status === 200,
"contenu attendu": (r) => r.status === 200 && r.json("id") === item.id,
});
}Des données qui ressemblent au trafic, sans effet de bord
Les données décident autant que le profil. Un identifiant unique répété mille fois peut tester un cache plutôt que le service ; un jeu trop petit peut fausser la répartition des lectures. Le jeu de données doit reproduire la variété du trafic visé : identifiants nombreux, tailles de réponses réalistes, proportion de requêtes coûteuses. SharedArray charge le fichier une seule fois dans le contexte d'initialisation et le partage entre tous les VUs, en lecture seule ; la documentation montre une économie de mémoire importante sur de gros fichiers.
Les écritures demandent une préparation dédiée : comptes de test, commandes identifiées comme données de test, nettoyage après l'essai, aucune notification réelle envoyée à un utilisateur. Un essai qui crée des données doit indiquer comment les retirer. L'essai de fumée, à très faible charge, vérifie le contenu des réponses et l'absence d'effet persistant indésirable avant toute montée.
Des seuils qui font réellement échouer l'exécution
Dans k6, deux mécanismes se ressemblent et ne jouent pas le même rôle. Un check codifie une assertion sur une réponse : statut, contenu, en-tête. Un seuil (threshold) définit un critère de réussite ou d'échec sur une métrique agrégée. La documentation est explicite : contrairement aux seuils, les checks en échec ne changent pas le code de sortie de k6. « Tous mes checks sont verts » et « le scénario atteint ses objectifs » sont donc deux constats distincts, et seul le second peut faire échouer une chaîne d'intégration continue.
Le code de sortie 99
Quand au moins un seuil est franchi, k6 termine avec un statut d'échec. Dans le code source de la v2.3.0, cette situation correspond au code 99 (ThresholdsHaveFailed). Une chaîne de livraison qui conditionne la suite au code de sortie s'arrête donc sur un seuil violé, pas sur un check en échec. Pour qu'un check pèse sur la décision, il faut un seuil sur la métrique checks, par exemple un taux de réussite minimal.
Du seuil franchi au code de sortie
Le schéma montre pourquoi un check en échec ne change pas le code de sortie, alors qu'un seuil franchi fait terminer l'essai avec le code 99.
Schéma défilable horizontalement ; sa version textuelle complète suit.
Lire le schéma sous forme textuelle
- Un check en échec, sans seuil sur la métrique checks, laisse le code de sortie inchangé.
- Un seuil franchi sur une métrique, par exemple http_req_duration, http_req_failed, dropped_iterations ou checks, fait terminer k6 avec un statut d'échec.
- Si ce seuil porte l'option abortOnFail, l'essai est interrompu dès le franchissement. delayAbortEval retarde cette évaluation le temps d'accumuler des échantillons.
- Si le seuil ne porte pas abortOnFail, l'essai se termine normalement.
- Dans les deux cas, lorsqu'un seuil a été franchi, le code de sortie vaut 99.
Deux familles de seuils
Les critères écrits dans la fiche se traduisent en deux familles de seuils, à garder visibles et séparées.
- Validité de l'essai. Un seuil sur dropped_iterations borne la demande non émise ; la documentation recommande de définir à la conception un taux d'abandon acceptable et de le vérifier par un seuil.
- Critères du service. Des percentiles sur http_req_duration restreints aux réponses attendues, un taux maximal sur http_req_failed, un taux minimal sur checks.
Le sous-ensemble http_req_duration{expected_response:true} ne retient que les réponses jugées attendues. Par défaut, k6 considère comme attendus les statuts de 200 à 399 ; les autres sont comptés dans http_req_failed. Ce découpage évite qu'une erreur rapide améliore artificiellement un percentile de latence.
Dans l'exemple, les seuils sont ceux-ci ; à adapter avec vos propres critères :
// Valeurs reprises de la fiche de décision, écrites avant l'essai
const ABANDONS_TOLERES = 30;
const P95_MS = 150;
const P99_MS = 300;
const TAUX_ERREUR_ACCEPTE = 0.01;
const TAUX_ERREUR_ARRET = 0.05;
const TAUX_CHECKS_MINIMAL = 0.99;
export const options = {
// scenarios : voir le bloc précédent
thresholds: {
// Validité de l'essai : la demande prévue a-t-elle été émise ?
dropped_iterations: [`count<=${ABANDONS_TOLERES}`],
// Critères du service, convenus avant l'essai
"http_req_duration{expected_response:true}": [`p(95)<${P95_MS}`, `p(99)<${P99_MS}`],
http_req_failed: [
`rate<${TAUX_ERREUR_ACCEPTE}`,
// Seuil d'arrêt, plus large : protège l'environnement, ne remplace pas le critère
{ threshold: `rate<${TAUX_ERREUR_ARRET}`, abortOnFail: true, delayAbortEval: "10s" },
],
checks: [`rate>${TAUX_CHECKS_MINIMAL}`],
},
};Arrêter tôt sans décider trop tôt
L'option abortOnFail interrompt l'essai dès que le seuil concerné est franchi. delayAbortEval retarde cette évaluation le temps d'accumuler des échantillons, par exemple pendant la montée. Un seuil d'arrêt plus large que le critère d'acceptation protège un environnement partagé contre un essai devenu inutile, sans transformer chaque fluctuation de démarrage en refus. Les métriques de type Trend acceptent avg, min, max, med et p(N) ; les Rate acceptent rate ; les Counter acceptent count et rate ; les Gauge acceptent value.
Prouver une fois que l'échec fait échouer
Avant de fonder une décision sur un code de sortie, vérifiez-le sur le script réel : provoquez une violation et lisez le code. La commande ci-dessous conserve le résumé de fin d'essai en JSON et affiche le code de sortie. À adapter à votre environnement :
k6 run --summary-mode=full --summary-export=<DOSSIER_PREUVES>/resume.json \
--out csv=<DOSSIER_PREUVES>/echantillons.csv.gz <SCRIPT>.js
echo "code de sortie : $?"Dans un laboratoire du avec k6 v2.3.0, une API locale dont la dépendance accepte 20 appels simultanés, l'essai avec l'option --no-thresholds, qui désactive l'évaluation des seuils, s'est terminé avec le code 0 alors que 15,4 % des checks avaient échoué. Ce contre-exemple utilisait 150 VUs, une dépendance à 250 ms et un profil raccourci (5 secondes de montée, 15 secondes de plateau). Avec la même dépendance et les mêmes 150 VUs, mais le profil complet et les seuils actifs, l'essai s'est arrêté à 14 secondes par abortOnFail, avec le code 99. Dans l'essai à dépendance ralentie, tous les checks étaient verts et le code valait pourtant 99, parce que la latence dépassait les critères et que trop d'itérations avaient été abandonnées.
Dimensionner et isoler le générateur de charge
Le générateur est un composant de l'essai, avec ses propres limites. S'il sature, il mesure sa propre saturation. La documentation k6 sur les essais de grande taille donne des repères de surveillance : garder au moins 20 % de cycles du processeur (CPU) inactifs (k6 jusqu'à 80 %), une mémoire sous 90 %, et se méfier d'un trafic réseau constant à 1 Gbit/s, signe probable d'une carte réseau limitante. Elle rappelle aussi que k6 charge par défaut le corps des réponses en mémoire ; l'option discardResponseBodies réduit cette consommation quand le contenu n'est pas vérifié, quitte à le réactiver par requête avec responseType.
Relevez donc, pendant tout l'essai, le CPU et la mémoire du processus ou du pod k6, le nombre de VUs actifs (vus) et alloués (vus_max), et les avertissements du journal, comme celui qui signale que le plafond de VUs est atteint.
Où placer le générateur
Depuis l'extérieur du cluster, le générateur traverse le même chemin que les utilisateurs : répartiteur de charge, contrôleur d'entrée, politiques réseau. Depuis l'intérieur, il évite ce chemin et risque de partager des nœuds avec le service mesuré. Dans ce cas, réservez-lui des nœuds par sélecteur de nœuds ou tolérances, déclarez ses ressources et notez dans le dossier quel chemin réseau a été exercé. Côté service, les ressources déclarées comptent aussi : selon la documentation Kubernetes, un conteneur qui atteint sa limite CPU est ralenti (throttling) sans être arrêté, alors qu'un conteneur qui dépasse sa limite mémoire peut être arrêté, avec la raison OOMKilled.
Distribuer avec k6 Operator, et ce que deviennent les seuils
Quand un seul générateur ne suffit plus, k6 Operator (v1.7.0 au ) exécute un essai sur plusieurs pods à partir d'une ressource TestRun. Le champ parallelism fixe le nombre de runners ; l'opérateur répartit la charge par segments d'exécution. La charge totale ne se multiplie pas : 200 VUs avec un parallélisme de 4 donnent 4 runners de 50 VUs. L'option separate place les runners sur des nœuds différents par une règle d'anti-affinité, et cleanup: post supprime les ressources après l'essai.
Figez l'image des runners par version, indépendamment de la version de l'opérateur. À adapter à votre environnement :
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
name: <NOM_DE_L_ESSAI>
namespace: <ESPACE_DE_NOMS_DES_ESSAIS>
spec:
parallelism: <NOMBRE_DE_RUNNERS>
separate: true
cleanup: post
script:
configMap:
name: <CONFIGMAP_DU_SCRIPT>
file: <SCRIPT>.js
arguments: --out experimental-prometheus-rw
runner:
image: grafana/k6:<VERSION_FIGEE>
env:
- name: K6_PROMETHEUS_RW_SERVER_URL
value: <URL_DE_REMOTE_WRITE>
- name: K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM
value: "true"
nodeSelector:
<CLE_DU_POOL_DE_NOEUDS>: <VALEUR>
resources:
requests:
cpu: "<CPU>"
memory: "<MEMOIRE>"
limits:
memory: "<MEMOIRE>"Le piège principal concerne les seuils. Chaque runner est une instance k6, et la documentation est nette : chaque instance évalue ses seuils indépendamment, sans les résultats des autres, et rapporte ses métriques séparément. Un p95 tenu sur chaque runner ne garantit pas un p95 tenu sur l'ensemble, et la moyenne des p95 des runners n'est pas le p95 global. Pour décider sur un percentile global, il faut des données fusionnées. La sortie Prometheus remote write, que la documentation k6 présente comme un module expérimental, exporte par défaut chaque Trend en séries de type Counter et Gauge, avec la seule statistique p(99) sauf réglage de K6_PROMETHEUS_RW_TREND_STATS ; la documentation précise que certaines jauges, notamment les percentiles, ne peuvent pas être agrégées. L'option K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM exporte les Trend en histogrammes natifs, que le récepteur remote write doit accepter. La page k6 les décrit encore comme une fonctionnalité expérimentale de Prometheus v2.40 ; le journal des versions de Prometheus en fait une fonctionnalité stable mais optionnelle depuis la v3.8, et rend le drapeau de fonctionnalité correspondant sans effet depuis la v3.9. Vérifiez ce qu'acceptent la version et la configuration de votre récepteur. Tant que cette agrégation n'est pas en place et vérifiée, préférez un seul générateur suffisamment dimensionné : la décision est plus simple à défendre.
Lire ensemble latence, erreurs, abandons et saturation, puis conclure
Aucune métrique isolée ne porte le verdict. La latence dit comment le service a répondu, les erreurs ce qu'il a refusé, les abandons quelle part de la demande n'a pas été émise, et la saturation, côté service comme côté générateur, pourquoi. La lecture se fait sur une chronologie commune : mêmes horodatages pour le résumé k6, les métriques du service et les événements du cluster (redémarrages, arrêts pour dépassement mémoire, limitation CPU, changements du nombre de réplicas).
Trois états, un seul changement à la fois
L'API de test appelle une dépendance : 20 appels simultanés au plus, délai moyen configurable, réponse 503 après 500 ms d'attente d'un emplacement libre. Le script suit le modèle ouvert du bloc précédent : montée de 0 à 100 itérations par seconde en 10 secondes, puis plateau de 50 secondes, soit environ 5 500 itérations prévues. Les états 2 et 3 ne diffèrent chacun de l'état sain que par un paramètre, et l'essai de contrôle ne diffère de l'état 2 que par le nombre de VUs.
Une seconde série d'essais donne des valeurs voisines, par exemple 991 à 996 itérations abandonnées pour la dépendance ralentie et 1 583 à 1 620 pour le générateur réduit.
| Essai | Changement | Code | Itérations | Abandons | p95 réussies | Échecs HTTP | Checks | Durée d'itération médiane | CPU max de k6 |
|---|---|---|---|---|---|---|---|---|---|
| 1, sain | Dépendance à 20 ms, 50 VUs | 0 | 5 499 | 0 | 21,9 ms | 0 % | 100 % | 20,3 ms | 5,1 % d'un cœur |
| 2, dépendance ralentie | Dépendance à 250 ms | 99 | 4 508 | 991 | 648,4 ms | 0 % | 100 % | 604,9 ms | 5,3 % |
| 3, générateur réduit | Dépendance à 20 ms, 2 VUs | 99 | 3 917 | 1 583 | 22,4 ms | 0 % | 100 % | 20,4 ms | 4,4 % |
| Contrôle de l'état 2 | 250 ms, 150 VUs | 99, arrêt à 14 s | 829 | 0 | 757,3 ms | 6,6 % | 93,4 % | 345,7 ms | 4,2 % |
| Répétition de l'état 1 | Générateur rétabli, 50 VUs | 0 | 5 499 | 0 | 21,8 ms | 0 % | 100 % | 20,4 ms | 6,7 % |
Attribuer les abandons
Les essais 2 et 3 affichent tous deux des abandons et le même avertissement de k6 : plafond de VUs atteint. La documentation prévient que les abandons ont deux causes possibles, une configuration insuffisante ou un service qui ne suit plus, et que des abandons dès le début de l'essai indiquent plutôt un manque de VUs. Trois indices les séparent.
- La durée d'itération. Dans l'essai 3, elle reste autour de 20 ms du début à la fin, avec des médianes de 20,7 ms pendant la montée et 20,4 ms au plateau ; dans l'essai 2, elle passe d'environ 260 ms pendant la montée à environ 610 ms au plateau.
- L'estimation de VUs nécessaires. Durée médiane multipliée par le taux : 0,020 s × 100 donne environ 2 VUs pour l'essai 3, sans aucune marge, alors que 2 étaient alloués ; 0,605 s × 100 donne environ 60 VUs pour l'essai 2, quand 50 étaient alloués. Dans le premier cas, la configuration manque ; dans le second, c'est le ralentissement qui a consommé les VUs.
- La télémétrie du service. Pendant l'essai 2, le pool de la dépendance est plein (20 sur 20) et sa file d'attente monte à 30. Pendant l'essai 3, deux emplacements au plus sont occupés et la file reste vide.
Dans ces essais, une durée d'itération stable avec des abandons désignait le générateur ; une durée qui s'allonge désignait le service.
Le CPU du processus k6 est resté sous 9 % d'un cœur dans tous les essais : aucun générateur n'était saturé en ressources. Le défaut de l'essai 3 tient à l'allocation de VUs, pas à la capacité de calcul du générateur.
L'absence d'erreurs peut venir du générateur
L'essai 2 ne montre aucune erreur HTTP. Le plafond de 50 VUs limitait pourtant les requêtes simultanées : 20 dans le pool, 30 en file, et une attente restée sous le délai de 500 ms. Le générateur se comportait alors comme un modèle fermé à son plafond. L'essai de contrôle, avec 150 VUs, émet la demande sans abandon ; la file monte à 51, des réponses 503 apparaissent, 6,6 % des requêtes échouent et le seuil d'arrêt interrompt l'essai. Ce moment d'arrêt dépend de delayAbortEval : avec 10 s, l'essai s'arrête à 14 s ; sur les essais de sensibilité, 15 s donnent un arrêt à 16 s et 20 s un arrêt à 22 s. Le refus de l'essai 2 est confirmé, mais son taux d'erreur nul tenait au générateur.
Le verdict en trois états
Un code 99 dit qu'au moins un seuil a été franchi ; il ne dit pas lequel ni pourquoi. Le verdict se construit dans un ordre fixe.
Du résultat de l'essai au verdict
Le schéma montre l'ordre des trois questions qui transforment un essai terminé en verdict de mise en production.
Schéma défilable horizontalement ; sa version textuelle complète suit.
Lire le schéma sous forme textuelle
- L'essai se termine et son code de sortie est relevé avec le résumé exporté.
- Première question : le générateur est-il resté sain en ressources (CPU, mémoire, réseau, erreurs du générateur) ? Si non, l'essai est non concluant et doit être rejoué avec un générateur corrigé.
- Deuxième question, si le générateur est sain : les critères du service sont-ils tenus (latence, erreurs, checks) ? Si non, le service est refusé, même si la demande prévue n'a pas été entièrement émise, puisqu'il a échoué sous une charge au plus égale à la cible.
- Troisième question, si les critères du service sont tenus : la demande prévue a-t-elle été émise (abandons sous le budget, nombre d'itérations conforme) ? L'avertissement de plafond de VUs relève de cette question : il signale une demande non émise, à attribuer avec la durée d'itération. Si non, l'essai est non concluant pour la capacité.
- Si les trois réponses sont positives, le service est accepté dans le profil essayé, et seulement dans celui-ci.
Dans ces essais, les essais 1 et 5 (répétition) sont acceptés dans le profil essayé ; les essais 2 et 4 sont refusés sur la latence, et l'essai 4 aussi sur les erreurs ; l'essai 3 est non concluant pour la capacité, alors que ses métriques HTTP sont sous les seuils. Rétablir le générateur, dans la répétition, a rendu un résultat équivalent au premier essai : le défaut venait bien de l'allocation de VUs. Un essai non représentatif n'efface jamais une panne observée ; il limite seulement ce que l'on peut affirmer sur la capacité.
Le dossier de preuves
La décision se défend avec des pièces, pas avec une capture d'écran du résumé. Le dossier conserve :
- la fiche de décision datée, avec les critères écrits avant l'essai et jamais réécrits après ;
- la version du script (empreinte ou commit), la version de k6 et l'empreinte du binaire ou de l'image ;
- le profil de charge tel que configuré et tel qu'émis : itérations, abandons, vus_max ;
- l'environnement : version du service, nombre de réplicas, ressources déclarées, nœuds, chemin réseau exercé ;
- le code de sortie, le résumé exporté en JSON et les échantillons bruts ;
- les métriques du générateur et celles du service sur la même chronologie, avec les événements du cluster ;
- le verdict, son auteur, sa date et ses limites : ce qui n'a pas été essayé et ce qui reste à rejouer.
Pour inscrire cet essai dans une ouverture progressive, avec répétition du déploiement, chemin de retour exécuté et critères d'arrêt mesurables, la page Mise en production Kubernetes : ouvrir par étapes décrit les critères de passage que nous vérifions avant d'exposer le trafic.
Sources officielles et limites de lecture
Les comportements de k6 décrits ici correspondent, au , à la documentation officielle de la version v2.3.x et au code source de la v2.3.0 ; ceux de k6 Operator, à la v1.7.0 et à sa documentation. Les options, valeurs par défaut et formats de résumé évoluent d'une version majeure à l'autre : la v2.0.0 a par exemple retiré le mode de résumé « legacy » et l'option --no-summary. Avant d'appliquer une commande, vérifiez-la dans la documentation propre à la version figée.
Générateur et service partagent une seule machine : les résultats montrent comment distinguer trois états, pas une capacité.
Sources
- Thresholds, Grafana k6 documentation : https://grafana.com/docs/k6/latest/using-k6/thresholds/
- Open and closed models, Grafana k6 documentation : https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/open-vs-closed/
- Dropped iterations, Grafana k6 documentation : https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/dropped-iterations/
- Arrival-rate VU allocation, Grafana k6 documentation : https://grafana.com/docs/k6/latest/using-k6/scenarios/concepts/arrival-rate-vu-allocation/
- Ramping arrival rate, Grafana k6 documentation : https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ramping-arrival-rate/
- Built-in metrics, Grafana k6 documentation : https://grafana.com/docs/k6/latest/using-k6/metrics/reference/
- setResponseCallback( callback ), Grafana k6 documentation : https://grafana.com/docs/k6/latest/javascript-api/k6-http/set-response-callback/
- SharedArray, Grafana k6 documentation : https://grafana.com/docs/k6/latest/javascript-api/k6-data/sharedarray/
- Load test types, Grafana k6 documentation : https://grafana.com/docs/k6/latest/testing-guides/test-types/
- Running large tests, Grafana k6 documentation : https://grafana.com/docs/k6/latest/testing-guides/running-large-tests/
- Running distributed tests, Grafana k6 documentation : https://grafana.com/docs/k6/latest/testing-guides/running-distributed-tests/
- Configure the TestRun CRD, Grafana k6 documentation : https://grafana.com/docs/k6/latest/set-up/set-up-distributed-k6/usage/configure-testrun-crd/
- Prometheus remote write, Grafana k6 documentation : https://grafana.com/docs/k6/latest/results-output/real-time/prometheus-remote-write/
- End of test, Grafana k6 documentation : https://grafana.com/docs/k6/latest/results-output/end-of-test/
- errext/exitcodes/codes.go (tag v2.3.0), dépôt officiel grafana/k6 sur GitHub : https://github.com/grafana/k6/blob/v2.3.0/errext/exitcodes/codes.go
- Release v2.3.0, dépôt officiel grafana/k6 sur GitHub : https://github.com/grafana/k6/releases/tag/v2.3.0
- Release v1.7.0, dépôt officiel grafana/k6-operator sur GitHub : https://github.com/grafana/k6-operator/releases/tag/v1.7.0
- api/v1alpha1/testrun_types.go (tag v1.7.0), dépôt officiel grafana/k6-operator sur GitHub : https://github.com/grafana/k6-operator/blob/v1.7.0/api/v1alpha1/testrun_types.go
- Resource Management for Pods and Containers, Kubernetes documentation : https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
- CHANGELOG.md (tag v3.16.0, entrées 3.8.0 et 3.9.0), dépôt officiel prometheus/prometheus sur GitHub : https://github.com/prometheus/prometheus/blob/v3.16.0/CHANGELOG.md