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