Tests de performance - 04 - Stress test : trouver le point de rupture

Comment trouver les limites de ton serveur avec un stress test. Le point de rupture, les cascades et ce qui casse en premier.

04 - Stress test : trouver le point de rupture

Ce que tu vas apprendre

  • La différence entre test de charge et stress test
  • Trouver le point de rupture de ton serveur
  • Ce qui casse en premier (et pourquoi c'est rarement ce qu'on croit)
  • Comment lire les courbes de degradation

Prerequisites

Avoir lu les articles sur k6 (02 et 03). Un serveur a tester (staging, pas la prod, s'il te plait).


Le test de charge vs le stress test

Le test de charge vérifié que ton système tient le trafic attendu. Si tu attends 100 utilisateurs simultanes, tu en simules 100 et tu verifies que tout va bien. C'est un test de conformité.

Le stress test, c'est l'inverse. Tu ne cherches pas a vérifier que ca tient. Tu cherches a casser. Tu montes la charge jusqu'a ce que le système s'ecroule, et tu notes a quel moment. C'est un test exploratoire.

Pourquoi c'est utile ? Parce que connaître ta limite te donne une marge de sécurité. Si ton serveur casse a 200 req/s et que tu en recois 50 en moyenne, tu as un facteur 4x de marge. C'est confortable. Si ton serveur casse a 60 req/s et que tu en recois 50 en moyenne, un petit pic suffit a tout faire tomber.

Trouver le point de rupture avec k6

Le ramping-arrival-rate est l'executeur qu'il te faut. Contrairement au ramping classique (qui augmente les VUs), celui-ci augmente le nombre de requêtes par seconde indépendamment du temps de réponse :

javascriptimport http from "k6/http";
import { check } from "k6";

export const options = {
  scenarios: {
    breakpoint: {
      executor: "ramping-arrival-rate",
      startRate: 10,
      timeUnit: "1s",
      preAllocatedVUs: 500,
      maxVUs: 1000,
      stages: [
        { duration: "2m", target: 10 },  // warmup a 10 req/s
        { duration: "5m", target: 50 },  // montee a 50 req/s
        { duration: "5m", target: 100 }, // montee a 100 req/s
        { duration: "5m", target: 200 }, // montee a 200 req/s
        { duration: "5m", target: 500 }, // montee a 500 req/s
        { duration: "2m", target: 500 }, // plateau
      ],
    },
  },
  thresholds: {
    http_req_failed: ["rate<0.10"], // on tolere jusqu'a 10% d'erreurs
  },
};

export default function () {
  const res = http.get("https://staging.paltemps.fr/api/health");
  check(res, {
    "status 200": (r) => r.status === 200,
    "response < 1s": (r) => r.timings.duration < 1000,
  });
}

Pas de sleep() ici. On veut que k6 envoie exactement le nombre de requêtes par seconde défini, sans delai supplementaire. preAllocatedVUs: 500 prepare 500 VUs d'avance pour absorber les paliers eleves.

Lance le test et observe les résultats en temps réel. Tu vas voir les metriques évoluer au fur et a mesure que la charge monte.

Ce qui casse en premier

En 2 ans de tests de charge sur paltemps.fr et les projets de mes clients, voici les goulots d'etranglement que je rencontre, du plus frequent au plus rare :

Pool de connexions base de donnees : PostgreSQL a un max_connections (défaut : 100). Quand le pool est sature, les nouvelles requêtes attendent. La latence explose, puis les timeouts arrivent. C'est le problème numero 1 sur les APIs qui touchent une base.

CPU sature : si ton serveur fait du JSON.parse() sur des gros payloads, du chiffrement, ou des calculs lourds, le CPU atteint 100% et tout ralentit. Sur un VPS 2 vCPU, ca arrive vite.

RAM : chaque requête en cours consomme de la mémoire (buffers, objets en cours de traitement). A 500 requêtes simultanees, ca peut representer plusieurs centaines de MB. Si la RAM est pleine, le kernel commence a swapper, et les performances s'effondrent.

File descriptors : chaque connexion TCP ouverte consomme un file descriptor. Linux a une limite par défaut de 1024. Si tu ne l'as pas augmentee (ulimit -n), tu seras bloque bien avant d'atteindre les limites CPU ou RAM.

Bande passante : rarement le problème sur une API JSON (les réponses font quelques KB), mais sur un serveur de fichiers ou d'images, 1 Gbps se remplit vite.

La cascade : comment tout s'effondre

Le pattern est toujours le meme. Disons que ton pool PostgreSQL gere 20 connexions et que chaque requête prend 10ms en base. A 20 req/s concurrentes, ca passe. A 30, les 10 requêtes en trop attendent une connexion libre. Elles prennent 20ms au lieu de 10ms.

Mais pendant ces 20ms, 10 nouvelles requêtes arrivent. Elles attendent aussi. La file grandit. 50ms. 100ms. 500ms. Les clients commencent a timeout. Ils reessayent. Les retries doublent la charge. Le pool ne se vide jamais. Tout le serveur est bloque en attente de la base de donnees.

En 30 secondes, tu passes de "tout va bien" a "rien ne répond". C'est ce qu'on appelle une cascade failure.

Lire les courbes

Quand tu lances un stress test, exporte les résultats pour pouvoir les visualiser :

bashk6 run --out json=results.json stress-test.js

Tu peux aussi envoyer les metriques vers InfluxDB ou Prometheus pour les visualiser dans Grafana. Mais un simple fichier JSON suffit pour commencer.

Ce que tu dois chercher dans les résultats :

Phase 1 - Lineaire : la charge monte, la latence reste stable. Le throughput augmente proportionnellement aux requêtes. Tout va bien.

Phase 2 - Saturation : la latence commence a monter. Le throughput plafonne. Le serveur est au maximum de sa capacité. C'est le "knee" de la courbe.

Phase 3 - Degradation : la latence explose (facteur 10x ou plus). Les erreurs apparaissent. Le throughput diminue malgre l'augmentation de la charge. Le système s'ecroule.

Le point entre la phase 1 et la phase 2, c'est ta capacité nominale. Le point entre la phase 2 et la phase 3, c'est ton point de rupture. L'ecart entre les deux, c'est ta zone de buffer.

Que faire quand tu connais la limite

Tu as plusieurs options, dans l'ordre du plus simple au plus complexe :

Optimiser le goulot : si c'est la base de donnees, ajoute des index, optimise les requêtes, augmente le pool. J'ai deja gagne un facteur 8x sur un endpoint juste en ajoutant un index sur une colonne utilisee dans un WHERE.

Ajouter du cache : un cache Redis devant une requête SQL lourde peut absorber 95% des lectures. Sur paltemps.fr, le cache des articles réduit la charge sur PostgreSQL de facon drastique.

Rate limiting : si tu ne peux pas augmenter la capacité, protégé le système en refusant poliment les requêtes en exces. Un 429 Too Many Requests vaut mieux qu'un 502 Bad Gateway.

Scaler : plus de CPU, plus de RAM, plus d'instances. C'est la solution la plus chere et souvent la moins efficace si le vrai problème est une requête SQL qui fait un full scan.

Le stress test te dit ou investir ton temps. Sans lui, tu optimises a l'aveugle.


Article précédent : 03 - Scénarios k6 : ramping, spike et soak Article suivant : 05 - Profiling mémoire : détecter les fuites

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.