On connaît tous ce grand moment de solitude. Tu viens de passer des semaines à peaufiner ton architecture. En local sur ta machine de guerre à 32 Go de RAM et disque NVMe, tout tourne à la vitesse de la lumière : les requêtes SQL tombent en 0,8 ms, Redis répond avant même que tu aies fini de penser à la clé, et ton framework encaisse les requêtes comme un champion.
Et puis arrive le grand saut en production.
Un vendredi à 17 h 45, le réseau du cloud provider se met à tousser, un nœud Redis commence à lâcher des paquets, la base de données sature sous un pic de charge et… boom ! Cascade de timeouts 504, requêtes fantômes qui s’empilent dans les workers PHP, fuites de mémoire et crash généralisé.
Tester son code dans un monde idéal où le réseau est parfait et le CPU infini, c’est de la naïveté pure. Si tu veux que ton application survive aux pires tempêtes, il faut lui en faire baver dès le local. C’est tout l’art du Chaos Engineering.
Voici le guide opérationnel sans détour pour injecter de la latence vicieuse, de la perte de paquets, de la gigue aléatoire (jitter), du bridage CPU violent et des coupures franches sur ta stack Docker (PHP Octane / MySQL / Redis).
Sortez les terminaux, on casse tout.
1. Le Prérequis Vital : Débrider les Capacités Réseau de Docker
Par défaut, un conteneur Docker est un citoyen de seconde zone qui n’a pas le droit de toucher aux tables de routage du noyau Linux. Pour injecter du chaos directement sur l’interface réseau, il faut accorder la capacité NET_ADMIN à vos services dans votre docker-compose.yml :
services:
laravel.octane:
cap_add:
- NET_ADMIN
mysql:
cap_add:
- NET_ADMIN
redis:
cap_add:
- NET_ADMIN
Un petit docker compose up -d pour appliquer le tout, et on installe l’arme absolue du noyau Linux : tc (Traffic Control) issu de la suite iproute2.
- Sur Redis (
alpine) :
docker compose exec redis apk add --no-cache iproute2
- Sur MySQL (
mysql/mysql-server:8.0sous Oracle Linux) :
docker compose exec -u root mysql microdnf install -y iproute-tc
- Sur Laravel Octane (
Debian/Ubuntu) :
docker compose exec -u root laravel.octane apt-get update && docker compose exec -u root laravel.octane apt-get install -y iproute2
2. L’Enfer du Traffic Control (tc netem) : Latence, Pertes et Gigue Aléatoire
L’émulateur réseau du noyau Linux (netem) s’accroche directement à la file de sortie (eth0) de votre conteneur. Pas de proxy intermédiaire, pas de surcouche : le trafic sortant subit physiquement les dégradations.
A. Le Piège de la Latence Fixe vs la Vraie Vie (Le Jitter Aléatoire)
Une latence fixe de 100 ms, c’est bien pour tester un timeout basique, mais ça ne représente jamais la réalité d’un réseau dégradé. Dans la vraie vie, la latence oscille de façon chaotique. C’est ce qu’on appelle la gigue (jitter).
Avec tc, on peut définir un délai moyen, une variation (jitter) et un facteur de corrélation statistique :
# MySQL : 100ms de latence avec une oscillation de ±20ms (entre 80ms et 120ms) corrélée à 25%
docker compose exec -u root mysql tc qdisc add dev eth0 root netem delay 100ms 20ms 25%
# Injection d'une distribution normale (gaussienne) pour un réalisme maximal
docker compose exec -u root mysql tc qdisc change dev eth0 root netem delay 100ms 30ms distribution normal
B. Injection Chirurgicale par Service
- Sur MySQL (Ralentir ou corrompre les réponses vers l’application) :
# Combiner 100ms de latence, 20ms de gigue et 3% de paquets perdus
docker compose exec -u root mysql tc qdisc add dev eth0 root netem delay 100ms 20ms loss 3%
# Modifier la règle à chaud pour calmer le jeu
docker compose exec -u root mysql tc qdisc change dev eth0 root netem delay 30ms 5ms loss 1%
# RESET : Retour à la normale
docker compose exec -u root mysql tc qdisc del dev eth0 root
- Sur Redis (Simuler un cache qui hoquète) :
# Latence avec gigue + 5% de perte de paquets
docker compose exec redis tc qdisc add dev eth0 root netem delay 50ms 15ms loss 5%
# RESET Redis
docker compose exec redis tc qdisc del dev eth0 root
- Sur Laravel Octane (Dégrader TOUT le trafic sortant : APIs tierces, S3, SQL, Cache) :
# 150ms de lag sortant avec gigue de ±40ms et 10% de packet loss
docker compose exec -u root laravel.octane tc qdisc add dev eth0 root netem delay 150ms 40ms loss 10%
# RESET Laravel
docker compose exec -u root laravel.octane tc qdisc del dev eth0 root
3. Étouffer la Machine : Bridage CPU Dynamique à Chaud
Que se passe-t-il quand votre base de données se mange une requête d’analyse non indexée et que le CPU sature à 100 % ?
Pas besoin d’écrire des boucles infinies en bash : le moteur Docker permet de modifier les quotas de temps processeur du cgroup à la volée, sans redémarrer le moindre service.
# Réduire MySQL à un filet de voix : 5% d'un seul cœur CPU (0.05 CPU)
docker update --cpus 0.05 $(docker compose ps -q mysql)
# Passer à 20% d'un cœur (0.2 CPU) pour tester la lenteur d'un serveur sous tension
docker update --cpus 0.2 $(docker compose ps -q mysql)
# Brider les workers PHP Octane à 1 cœur strict
docker update --cpus 1.0 $(docker compose ps -q laravel.octane)
# RESET TOTAL : Rendre l'accès illimité aux cœurs de la machine
docker update --cpus 0 $(docker compose ps -q mysql)
docker update --cpus 0 $(docker compose ps -q laravel.octane)
4. Automatiser le Chaos : Shopify Toxiproxy
Si tc est parfait pour manipuler le réseau en ligne de commande, Toxiproxy est l’outil ultime pour automatiser vos tests de résilience dans vos suites de tests unitaires ou d’intégration (Pest, PHPUnit).
A. Intégration Docker Compose
On intercale le proxy entre l’application et les bases de données :
services:
toxiproxy:
image: 'ghcr.io/shopify/toxiproxy:latest'
ports:
- '8474:8474' # API REST de contrôle
- '3307:3307' # Port proxy vers MySQL
- '6380:6380' # Port proxy vers Redis
networks:
- sail
Dans le .env de votre application, pointez vers les ports exposés par Toxiproxy :
Extrait de code
DB_HOST=toxiproxy
DB_PORT=3307
REDIS_HOST=toxiproxy
REDIS_PORT=6380
B. Configuration et Injection de « Toxics » via l’API REST
On déclare d’abord les ponts réseaux :
# 1. Déclarer le tunnel MySQL
curl -X POST http://localhost:8474/proxies -d '{
"name": "mysql_proxy",
"listen": "0.0.0.0:3307",
"upstream": "mysql:3306",
"enabled": true
}'
# 2. Déclarer le tunnel Redis
curl -X POST http://localhost:8474/proxies -d '{
"name": "redis_proxy",
"listen": "0.0.0.0:6380",
"upstream": "redis:6379",
"enabled": true
}'
Puis on balance les anomalies programmées :
# Injection de 150ms de latence avec 50ms de gigue aléatoire (Jitter) sur MySQL
curl -X POST http://localhost:8474/proxies/mysql_proxy/toxics -d '{
"name": "mysql_latency",
"type": "latency",
"attributes": { "latency": 150, "jitter": 50 }
}'
# Simulation d'un crash net de Redis (Coupure immédiate de la socket)
curl -X POST http://localhost:8474/proxies/redis_proxy/toxics -d '{
"name": "redis_down",
"type": "timeout",
"attributes": { "timeout": 0 }
}'
# Suppression des perturbations (Clean Slate)
curl -X DELETE http://localhost:8474/proxies/mysql_proxy/toxics/mysql_latency
curl -X DELETE http://localhost:8474/proxies/redis_proxy/toxics/redis_down
Passez vos Systèmes au Révélateur
Une architecture robuste ne se mesure pas à sa rapidité quand tout va bien, mais à son élégance quand tout part en vrille.
En intégrant ces simulations dans vos environnements de test, vous allez immédiatement identifier :
- Les requêtes SQL bloquantes qui monopolisent les workers PHP.
- L’absence de timeouts explicites sur vos clients Redis ou HTTP.
- L’incapacité de votre frontend à afficher une erreur propre quand un service tiers s’effondre.
Bannissez le « chez moi ça marche ». Injectez du chaos, observez où ça casse, blindez vos circuits et dormez enfin sur vos deux oreilles le week-end !