Tests de performance - 07 - Soak tests : l'endurance longue duree

Lancer des tests de 2h, 12h, 24h pour détecter les fuites lentes. Mémoire, connexions et degradation progressive.

07 - Soak tests : l'endurance longue duree

Ce que tu vas apprendre

  • Pourquoi un test de 30 secondes ne suffit pas
  • Configurer un soak test avec k6
  • Quoi monitorer pendant un test de plusieurs heures
  • Identifier la degradation progressive

Prerequisites

Avoir lu les articles sur k6 (02, 03) et sur le profiling mémoire (05).


Les bugs qu'on ne voit pas en 5 minutes

La plupart des tests de charge durent entre 30 secondes et 10 minutes. Ca suffit pour vérifier que le serveur répond correctement sous charge. Mais ca ne suffit pas pour trouver les problèmes qui s'accumulent au fil du temps.

Voici des bugs réels que j'ai rencontres sur paltemps.fr et qui n'apparaissent qu'apres des heures d'exécution :

Une fuite mémoire de 2 MB par heure. Invisible en 5 minutes (170 KB de plus, noyee dans le bruit). Visible apres 6 heures (+12 MB). Fatale apres 4 jours (+192 MB, OOM kill).

Un pool de connexions PostgreSQL qui ne recyclait pas les connexions mortes. Apres 3 heures, 15 des 20 connexions du pool etaient des zombies. Les 5 restantes suffisaient a peine pour le trafic normal.

Des fichiers de log qui grossissaient de 50 MB par heure. Le disque de 20 GB du VPS etait plein au bout de 16 heures en charge. Les ecritures commencaient a échouer, les requêtes qui dependaient de l'écriture de fichiers temporaires plantaient.

Le script soak

Un soak test n'a rien de complique cote k6. C'est un test de charge normal, mais long :

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

export const options = {
  stages: [
    { duration: "5m", target: 30 },  // montee progressive
    { duration: "2h", target: 30 },  // plateau de 2 heures
    { duration: "5m", target: 0 },   // descente
  ],
  thresholds: {
    http_req_duration: ["p(95)<300"],
    http_req_failed: ["rate<0.01"],
  },
};

export default function () {
  const res = http.get("https://staging.paltemps.fr/api/feeds");
  check(res, {
    "status 200": (r) => r.status === 200,
    "response < 500ms": (r) => r.timings.duration < 500,
  });
  sleep(Math.random() * 2 + 0.5); // sleep aleatoire entre 0.5s et 2.5s
}

30 VUs pendant 2 heures. Ce n'est pas une charge énorme. C'est voulu : le soak test ne cherche pas a surcharger le serveur. Il cherche a détecter les degradations lentes sous une charge moderee et constante.

Le sleep aleatoire simule mieux le trafic réel que un sleep fixe. Les vrais utilisateurs ne cliquent pas tous exactement toutes les secondes.

Lancer en arriere-plan

Un test de 2 heures, tu ne vas pas le regarder dans ton terminal. Lance-le en arriere-plan avec export des résultats :

bashnohup k6 run --out json=soak-results.json soak-test.js > k6-output.log 2>&1 &
echo $!  # note le PID pour pouvoir le suivre

Ou avec Docker si tu veux isoler le test :

bashdocker run -d --name soak-test \
  -v $(pwd):/scripts \
  grafana/k6 run /scripts/soak-test.js

Tu peux suivre la progression avec docker logs -f soak-test.

Quoi monitorer pendant le soak

Le test k6 te donne les metriques HTTP. Mais pour un soak test, tu veux aussi surveiller l'état du serveur lui-meme. Voici un script de monitoring minimaliste a lancer en parallèle sur ton VPS :

bash#!/bin/bash
# monitor.sh - a lancer sur le serveur pendant le soak
LOG_FILE="/tmp/soak-monitor.csv"
echo "timestamp,cpu_pct,mem_rss_mb,connections,disk_used_pct" > $LOG_FILE

CONTAINER="paltemps-api"  # nom du container Docker

while true; do
  TIMESTAMP=$(date +%s)

  # CPU et memoire du container
  STATS=$(docker stats --no-stream --format "{{.CPUPerc}},{{.MemUsage}}" $CONTAINER 2>/dev/null)
  CPU=$(echo $STATS | cut -d',' -f1 | tr -d '%')
  MEM_RAW=$(echo $STATS | cut -d',' -f2 | cut -d'/' -f1 | tr -d ' ')
  # Convertir en MB
  MEM=$(echo $MEM_RAW | sed 's/GiB/*1024/;s/MiB//;s/KiB/\/1024/' | bc 2>/dev/null || echo "0")

  # Connexions TCP etablies
  CONNS=$(ss -tn state established | wc -l)

  # Disque
  DISK=$(df / --output=pcent | tail -1 | tr -d ' %')

  echo "$TIMESTAMP,$CPU,$MEM,$CONNS,$DISK" >> $LOG_FILE
  sleep 30
done

Lance-le avant le soak test et arrêté-le apres. Tu auras un CSV avec l'évolution de chaque metrique toutes les 30 secondes.

Les signaux d'alerte

Voici ce que tu cherches dans les donnees :

Mémoire qui monte lineairement. Si le RSS du process passe de 150 MB a 200 MB en 2 heures de facon régulière, c'est une fuite. Pas un comportement normal du GC (qui ferait un pattern en dents de scie). L'article 05 explique comment trouver la source.

Nombre de connexions qui augmente. Si tu passes de 20 connexions TCP a 50 sans que la charge ait change, quelque chose ne ferme pas correctement ses connexions. Verifie le pool de base de donnees, les connexions HTTP keep-alive, et les WebSockets.

bash# Nombre de connexions par etat
ss -tn | awk '{print $1}' | sort | uniq -c | sort -rn

Si tu vois beaucoup de CLOSE-WAIT, le cote serveur ne ferme pas les connexions proprement. Si tu vois beaucoup de TIME-WAIT, c'est normal (les connexions terminees restent en TIME-WAIT pendant 60 secondes par défaut).

Latence qui derive. La latence p95 au début du test est a 80ms. Apres 1 heure, elle est a 120ms. Apres 2 heures, 180ms. La degradation est lente mais régulière. Ca peut venir d'une table temporaire qui grossit, d'un cache qui se fragmente, ou d'une mémoire qui sature et force le GC a tourner plus souvent.

Espace disque qui diminue. Les logs, les fichiers temporaires, les fichiers de session. Sur une charge de 30 VUs pendant 2 heures, ca peut representer des dizaines de milliers de fichiers. Verifie que ta rotation de logs fonctionne.

Connexion pool exhaustion

C'est un classique. PostgreSQL a un max_connections de 100 par défaut. Ton pool applicatif reserve 20 connexions. Si une requête SQL est lente (2 secondes au lieu de 50ms), les 20 connexions sont occupees en meme temps. Les nouvelles requêtes attendent. La file d'attente grandit.

Pour détecter ca pendant un soak :

bash# Nombre de connexions actives vers PostgreSQL
docker exec postgres psql -U postgres -c \
  "SELECT count(*) FROM pg_stat_activity WHERE state = 'active';"

Si ce nombre atteint le maximum de ton pool de facon régulière pendant le soak, c'est un signe que tes requêtes sont trop lentes ou que ton pool est trop petit.

File descriptors

Chaque connexion réseau, chaque fichier ouvert consomme un file descriptor. La limite par défaut sous Linux est souvent 1024. Pendant un soak test :

bash# Nombre de file descriptors ouverts par le process
ls /proc/$(pgrep bun)/fd | wc -l

Si ce nombre monte régulièrement sans redescendre, tu as une fuite de file descriptors. Probablement des fichiers ou des sockets qui ne sont pas fermes.

Analyser les résultats

Apres 2 heures, tu as un fichier JSON de k6 et un CSV de monitoring. Compare les deux :

  • Est-ce que la latence p95 a la fin du test est la meme qu'au début ?
  • Est-ce que le taux d'erreur a augmente dans la dernière heure ?
  • Est-ce que la mémoire du serveur est revenue a son niveau initial apres la descente ?

Si oui, ton serveur tient l'endurance. Si la latence a double, la mémoire a augmente de 30%, ou des erreurs sont apparues dans la dernière demi-heure, il y a un problème a investiguer.

Pour mes projets, je considéré qu'un soak test passe si la latence p95 ne varie pas de plus de 20% entre le début et la fin du plateau, et que le taux d'erreur reste sous 0.1%.


Article précédent : 06 - Timeouts et resilience Article suivant : 08 - Comparatif : k6 vs Artillery vs Autocannon vs wrk

Sources

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