Déboguer des conteneurs Docker lents : throttling CPU, goulots d’étranglement I/O et limites mal configurées
Un conteneur « lent » n’est presque jamais un mystère : il est généralement bridé par le CPU (throttling CFS), bloqué en attente d’entrées/sorties (I/O disque ou réseau), confiné par une limite mémoire (avec swap, pression mémoire, OOM), ou victime d’une configuration de limites incohérente (CPU set, quotas trop bas, cgroups v1/v2, limites implicites). Ce tutoriel propose une méthode pratique et reproductible pour diagnostiquer et corriger ces situations, avec commandes réelles et interprétation des résultats.
1) Pré-requis et principes de diagnostic
Objectif
Répondre à trois questions :
- Le conteneur est-il réellement lent, ou est-ce un problème d’observabilité (latence réseau, DNS, timeouts, contention côté base de données, etc.) ?
- Quelle ressource est le goulot : CPU, mémoire, disque, réseau, verrous applicatifs ?
- Quelle limite ou contention provoque le goulot : quotas CPU, cpuset, throttling, I/O saturées, limites de fichiers, pression mémoire, etc.
Outils utiles (hôte Linux)
Installez (selon distro) :
sudo apt-get update
sudo apt-get install -y \
sysstat iotop htop jq curl \
linux-tools-common linux-tools-generic \
strace lsof procps
Optionnel (très utile) :
perf(profiling CPU)bpftrace/bcc-tools(observabilité kernel)pidstat(sysstat) pour CPU/I/O par PID
2) Étape 0 : établir une baseline et isoler le problème
2.1 Identifier le conteneur et son PID « init »
docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
Récupérer le PID du conteneur (processus 1 dans le namespace du conteneur) :
CID=<id_ou_nom>
docker inspect --format '{{.State.Pid}}' "$CID"
Gardez ce PID, il servira pour nsenter, strace, perf, etc.
2.2 Vérifier rapidement l’état global
Sur l’hôte :
uptime
free -h
vmstat 1 5
iostat -xz 1 5
Interprétation rapide :
vmstat: colonner(run queue),wa(I/O wait),si/so(swap in/out),cs(context switches).iostat -xz:await,svctm(selon version),utilproche de 100% => disque saturé.
2.3 Vérifier si la lenteur est interne au conteneur
Exécutez une commande simple dans le conteneur :
docker exec -it "$CID" sh -lc 'date; uname -a; ps aux | head'
Si même ps ou ls est lent, suspectez I/O ou pression mémoire. Si l’application seule est lente, suspectez CPU throttling, verrous, GC, latence réseau, etc.
3) Comprendre les limites CPU Docker (et pourquoi ça throttle)
Docker s’appuie sur les cgroups. Deux mécanismes principaux :
- CPU shares (
--cpu-shares) : priorité relative quand il y a contention (pas une limite stricte). - CPU quota/period (
--cpus,--cpu-quota,--cpu-period) : limite stricte via le scheduler CFS. C’est la cause classique du throttling.
Exemple :
--cpus=1.0équivaut généralement àcpu.cfs_quota_us=100000etcpu.cfs_period_us=100000.--cpus=0.5=> quota 50000 sur période 100000 : le conteneur peut exécuter 50 ms puis sera throttlé jusqu’à la fin de la période.
Le symptôme : l’application « veut » du CPU, mais le kernel la bloque car elle a consommé son quota.
4) Détecter le throttling CPU (preuve chiffrée)
4.1 docker stats (indicateur, pas preuve)
docker stats "$CID"
Si vous voyez un CPU à ~100% mais une latence élevée, ce n’est pas suffisant. Le throttling peut se produire même si le pourcentage affiché semble « plafonner » bas.
4.2 Lire les stats cgroup CPU (cgroups v1)
Sur beaucoup de systèmes, Docker utilise encore des chemins cgroup v1 (ou un mode hybride). Pour trouver le chemin exact :
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
cat /proc/$PID/cgroup
Vous verrez des lignes du type cpu,cpuacct:/docker/<id>.
Ensuite, lisez cpu.stat :
CGPATH=$(awk -F: '$2 ~ /cpu/ {print $3}' /proc/$PID/cgroup | head -n1)
cat /sys/fs/cgroup/cpu"$CGPATH"/cpu.stat
Sortie typique :
nr_periods: nombre de périodes écouléesnr_throttled: nombre de périodes où le conteneur a été throttléthrottled_time: temps total throttlé (en ns)
Interprétation :
- Si
nr_throttledaugmente rapidement etthrottled_timegrimpe, vous avez un throttling réel. - Comparez sur 10 secondes :
watch -n 1 "cat /sys/fs/cgroup/cpu$CGPATH/cpu.stat"
4.3 Lire les stats cgroup CPU (cgroups v2)
En v2, les fichiers diffèrent (cpu.stat, cpu.max). Même approche pour trouver le chemin, puis :
# Exemple : chemin unifié
CGPATH=$(awk -F: '$1 == 0 {print $3}' /proc/$PID/cgroup)
cat /sys/fs/cgroup"$CGPATH"/cpu.stat
cat /sys/fs/cgroup"$CGPATH"/cpu.max
cpu.max:quota periodoumax periodsi illimité.cpu.statinclut souventnr_throttledetthrottled_usec(selon kernel).
4.4 Vérifier la configuration CPU du conteneur
docker inspect "$CID" | jq '.[0].HostConfig | {NanoCpus, CpuQuota, CpuPeriod, CpuShares, CpusetCpus, CpusetMems}'
Points d’attention :
NanoCpus(si--cpus) peut être défini sans que vous le réalisiez (compose, orchestrateur).CpusetCpuspeut contraindre à un cœur déjà saturé.CpuQuotatrès bas (ex. 20000) => throttling quasi permanent.
5) Corriger le throttling CPU (solutions concrètes)
5.1 Augmenter le quota CPU
En recréant le conteneur avec plus de CPU :
docker run --cpus="2.0" ...
Ou en mode explicite :
docker run --cpu-period=100000 --cpu-quota=200000 ...
Remarque :
docker updatepermet parfois de modifier à chaud (selon options et version).
docker update --cpus="2.0" "$CID"
Vérifiez ensuite cpu.max/cpu.cfs_quota_us.
5.2 Éviter un cpuset trop restrictif
Si CpusetCpus="0" et que le CPU0 est déjà chargé, vous créez une contention inutile. Ajustez :
docker update --cpuset-cpus="2-5" "$CID"
5.3 Comprendre cpu-shares (priorité, pas plafond)
Si vous n’avez pas de quota mais des shares faibles, le conteneur peut être « affamé » quand d’autres consomment.
docker update --cpu-shares 1024 "$CID"
5.4 Vérifier le nombre de threads applicatifs
Un conteneur limité à 1 CPU mais une JVM/Node/Go configurée pour utiliser 8 threads peut empirer la latence (context switches). Ajustez la configuration applicative :
- Java :
-XX:ActiveProcessorCount=1 - Go :
GOMAXPROCS=1 - Node : limiter le pool (selon workload)
6) Diagnostiquer les goulots d’étranglement I/O (disque)
Une application peut être « CPU faible » mais lente car elle attend le disque. Symptômes :
waélevé dansvmstatiostatmontreawaitélevé etutilproche de 100%- Beaucoup de processus en état
D(uninterruptible sleep)
6.1 Observer I/O au niveau hôte
iostat -xz 1
À surveiller :
r/s,w/s: opérationsrkB/s,wkB/s: débitawait: latence moyenne (ms) — si ça grimpe (ex. > 20-50 ms sur SSD, > 50-100 ms sur HDD), problème probable%util: saturation
6.2 Trouver quels processus font de l’I/O
sudo iotop -oPa
-o: n’affiche que les actifs-P: par processus-a: cumulé
6.3 Relier au conteneur (PID namespace)
Récupérez le PID du conteneur et listez ses processus côté hôte :
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
sudo ps -o pid,ppid,cmd --forest -g $(ps -o sid= -p "$PID")
Alternative simple : entrer dans le namespace du conteneur :
sudo nsenter -t "$PID" -p -m -u -i -n sh -lc 'ps aux'
6.4 OverlayFS et coûts cachés
Docker utilise souvent overlay2. Certaines opérations (beaucoup de petits fichiers, fsync fréquents, logs volumineux) peuvent devenir coûteuses.
Vérifiez le driver de stockage :
docker info | grep -i 'Storage Driver'
docker info | sed -n '/Storage Driver/,$p' | head -n 30
Si vous écrivez énormément, privilégiez des volumes plutôt que la couche writable du conteneur :
docker run -v /data/app:/var/lib/app ...
6.5 Identifier les fichiers chauds (logs, DB, temp)
Dans le conteneur :
docker exec -it "$CID" sh -lc 'ls -lah /var/log || true; du -sh /var/log/* 2>/dev/null | sort -h | tail'
docker exec -it "$CID" sh -lc 'lsof -nP | head'
Sur l’hôte (si vous connaissez le PID du process), vous pouvez inspecter ses fichiers ouverts :
sudo lsof -p <PID_DU_PROCESS> | head -n 50
6.6 Tracer les appels système I/O (preuve fine)
Sur un PID spécifique (dans le conteneur, mais PID hôte) :
sudo strace -f -tt -T -p <PID> -e trace=read,write,openat,fsync,fdatasync
Cherchez :
- des
fsync()longs - des
openat()répétitifs - des
read()bloquants
7) Limites I/O via cgroups (blkio / io) : quand Docker bride le disque
Docker peut limiter l’I/O avec :
- v1 :
--blkio-weight,--device-read-bps,--device-write-bps, etc. - v2 : contrôleur
io(fichiersio.max,io.weight)
7.1 Vérifier si une limite I/O est configurée
Inspectez la config :
docker inspect "$CID" | jq '.[0].HostConfig | {BlkioWeight, BlkioDeviceReadBps, BlkioDeviceWriteBps, BlkioDeviceReadIOps, BlkioDeviceWriteIOps}'
Si vous voyez des valeurs non nulles, c’est une piste forte.
7.2 Lire les fichiers cgroup I/O (selon v1/v2)
Toujours avec CGPATH :
- v1 (blkio) :
cat /sys/fs/cgroup/blkio"$CGPATH"/blkio.throttle.io_service_bytes
cat /sys/fs/cgroup/blkio"$CGPATH"/blkio.throttle.io_serviced
- v2 (io) :
cat /sys/fs/cgroup"$CGPATH"/io.stat
cat /sys/fs/cgroup"$CGPATH"/io.max
Si io.max impose des plafonds, vous verrez des limites par device (major:minor).
8) Mémoire : pression, swap, OOM et « lenteur » qui ressemble à du CPU
Une application peut être lente car :
- elle swap (latence énorme),
- elle subit une pression mémoire (reclaim, compaction),
- elle se fait OOM-kill puis redémarre,
- elle est limitée par
--memorytrop bas et passe son temps en GC (JVM) ou en allocations/évictions.
8.1 Vérifier la mémoire du conteneur
docker stats "$CID"
Mais allez plus loin :
docker inspect "$CID" | jq '.[0].HostConfig | {Memory, MemorySwap, MemoryReservation, OomKillDisable}'
Memory: limite RAMMemorySwap: RAM+swap autorisé (selon config). Une valeur égale àMemorysignifie souvent « pas de swap » (selon version).- Si swap autorisé, un conteneur peut devenir très lent au lieu de crash.
8.2 Lire les stats mémoire cgroup
- v1 :
cat /sys/fs/cgroup/memory"$CGPATH"/memory.stat | egrep 'rss|cache|swap|pgfault|pgmajfault' || true
cat /sys/fs/cgroup/memory"$CGPATH"/memory.failcnt
cat /sys/fs/cgroup/memory"$CGPATH"/memory.limit_in_bytes
- v2 :
cat /sys/fs/cgroup"$CGPATH"/memory.current
cat /sys/fs/cgroup"$CGPATH"/memory.max
cat /sys/fs/cgroup"$CGPATH"/memory.stat | egrep 'anon|file|swap|pgfault|pgmajfault' || true
Indicateurs :
pgmajfaultqui augmente : accès disque dus à défauts de page majeurs (souvent swap ou fichiers non en cache).swapnon nul (v2) : le conteneur swap.failcnt(v1) : tentatives d’allocation refusées => pression forte.
8.3 Détecter l’OOM killer
Sur l’hôte :
dmesg -T | egrep -i 'killed process|oom|out of memory' | tail -n 50
journalctl -k --since "1 hour ago" | egrep -i 'oom|killed process' | tail -n 50
Dans Docker :
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}} {{.State.FinishedAt}}' "$CID"
8.4 Corriger
- Augmenter
--memorysi réellement insuffisant. - Désactiver le swap pour éviter la « lenteur infinie » (selon politique) :
- Ajuster
--memory-swappour limiter.
- Ajuster
- Ajuster l’application (caches, batch size, index DB).
- Pour JVM, calibrer le heap (trop grand => pression, trop petit => GC).
9) Réseau : latence, DNS, saturation et files d’attente
La lenteur peut venir d’appels externes (API, DB, S3), DNS, ou congestion.
9.1 Vérifier rapidement depuis le conteneur
docker exec -it "$CID" sh -lc 'ip a; ip r; cat /etc/resolv.conf'
docker exec -it "$CID" sh -lc 'getent hosts example.com'
docker exec -it "$CID" sh -lc 'time curl -sS -o /dev/null -w "dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://example.com'
Si time_namelookup est élevé : DNS.
Si time_connect élevé : réseau/pare-feu/route.
Si ttfb élevé : serveur distant lent.
9.2 Observer les sockets et connexions
Dans le conteneur :
docker exec -it "$CID" sh -lc 'ss -tanp | head -n 50'
Sur l’hôte, en entrant dans le namespace réseau :
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
sudo nsenter -t "$PID" -n ss -s
sudo nsenter -t "$PID" -n ss -tan state established '( sport = :http or dport = :http )' | head
9.3 Capturer un pcap (si nécessaire)
Sur l’hôte, dans le namespace réseau du conteneur :
sudo nsenter -t "$PID" -n tcpdump -i any -w /tmp/container.pcap port 443
Puis analysez avec Wireshark.
10) Limites mal configurées : les pièges fréquents
10.1 Confondre « CPU % » et « CPU disponible »
Un conteneur limité à 0.5 CPU peut afficher ~50% sur un système à 1 CPU, ou un chiffre trompeur sur un hôte multi-cœurs. La preuve du throttling reste cpu.stat.
10.2 ulimit trop bas (fichiers, processus)
Dans le conteneur :
docker exec -it "$CID" sh -lc 'ulimit -a'
Problèmes typiques :
open filestrop bas => erreurs, retries, lenteur.max user processestrop bas => thread creation bloquée.
Configurer au lancement :
docker run --ulimit nofile=1048576:1048576 ...
10.3 Logs Docker (json-file) qui saturent le disque
Si vous loggez énormément sur stdout/stderr, le driver json-file peut créer une I/O énorme.
Vérifiez la taille des logs :
docker inspect --format '{{.LogPath}}' "$CID"
sudo ls -lh "$(docker inspect --format '{{.LogPath}}' "$CID")"
Solutions :
- Activer la rotation :
docker run --log-opt max-size=10m --log-opt max-file=3 ...
- Utiliser un autre driver (selon infra) :
local,fluentd, etc.
10.4 CPU pinning et NUMA
Si CpusetMems contraint à une zone mémoire NUMA défavorable, ou si vous épinglez des CPU non optimaux, vous pouvez créer de la latence. Vérifiez :
docker inspect "$CID" | jq '.[0].HostConfig | {CpusetCpus, CpusetMems}'
10.5 Cgroups v1/v2 et métriques incohérentes
Selon la distribution, Docker peut être en v2 (unified) ou v1. Les chemins et fichiers changent. Toujours partir de :
cat /proc/$(docker inspect --format '{{.State.Pid}}' "$CID")/cgroup
mount | grep cgroup
11) Méthode pas-à-pas (checklist opérationnelle)
Étape A — Confirmer le symptôme
- Mesurez la latence applicative (endpoint, job, requête).
- Vérifiez si la lenteur est constante ou en pics (corrélation avec charge).
Étape B — CPU
docker stats- Lire
cpu.statet vérifiernr_throttled/throttled_time - Vérifier
CpuQuota/CpuPeriod/NanoCpusetCpusetCpus - Corriger : augmenter
--cpus, ajustercpuset, réduire threads
Commandes clés :
docker inspect "$CID" | jq '.[0].HostConfig | {NanoCpus, CpuQuota, CpuPeriod, CpuShares, CpusetCpus}'
watch -n 1 "cat /sys/fs/cgroup/cpu$CGPATH/cpu.stat 2>/dev/null || cat /sys/fs/cgroup$CGPATH/cpu.stat"
Étape C — I/O disque
iostat -xz 1iotop -oPastracesur le PID suspect- Vérifier limites blkio/io
- Corriger : volumes, réduire fsync/logs, augmenter IOPS, changer storage, enlever throttling
Étape D — Mémoire
free -h,vmstat 1- cgroup memory stats (
pgmajfault,swap) dmesgOOM- Corriger : augmenter mémoire, ajuster heap, limiter swap, optimiser caches
Étape E — Réseau
curl -wavec timingsss -s,ss -tanptcpdumpsi besoin- Corriger : DNS, MTU, timeouts, pool connexions, latence externe
12) Cas pratiques (symptômes → diagnostic → correction)
Cas 1 : API lente sous charge, CPU à 30% seulement
Symptôme : latence p95 explose, docker stats montre CPU ~30% alors que l’hôte a plein de CPU libre.
Diagnostic :
- Lire
cpu.stat:nr_throttledaugmente fortement. - Inspect :
--cpus=0.3configuré par erreur dans Compose.
Commandes :
docker inspect "$CID" | jq '.[0].HostConfig | {NanoCpus, CpuQuota, CpuPeriod}'
cat /sys/fs/cgroup/cpu"$CGPATH"/cpu.stat 2>/dev/null || cat /sys/fs/cgroup"$CGPATH"/cpu.stat
Correction :
- Monter à
--cpus=2.0et redéployer. - Ajuster le nombre de workers applicatifs au nouveau budget CPU.
Cas 2 : Job de traitement « bloqué », CPU faible, wa élevé
Symptôme : CPU conteneur faible, mais le job prend 10× plus longtemps, l’hôte montre wa > 30%.
Diagnostic :
iostat -xz 1:%util100% sur le disque,awaitélevé.iotop: processus du conteneur écrit énormément.strace: beaucoup defsync()longs (ex. base SQLite, journaling, logs).
Correction :
- Déplacer les écritures sur un volume dédié plus performant.
- Réduire
fsync(si acceptable), batcher les écritures. - Activer la rotation des logs Docker.
Cas 3 : Lenteur aléatoire + pics de latence, mémoire « presque pleine »
Symptôme : parfois tout va bien, parfois latence énorme. Pas d’OOM, mais « ça rame ».
Diagnostic :
- cgroup memory :
swapaugmente,pgmajfaultgrimpe. vmstat:si/sonon nuls (swap in/out).
Correction :
- Réduire ou interdire le swap du conteneur (selon politique).
- Augmenter la mémoire ou réduire la consommation (cache, heap).
- Sur JVM : calibrer heap et GC.
13) Conseils de prévention (éviter de re-déboguer demain)
- Exposez les métriques cgroup (Prometheus node exporter + cAdvisor ou métriques orchestrateur) :
- throttling CPU (
throttled_time) memory.current,oom_kills- I/O (
io.stat, latences)
- throttling CPU (
- Définissez des limites réalistes :
- CPU : évitez des quotas trop bas « pour économiser » si la latence est critique.
- Mémoire : dimensionnez avec marge, sinon swap/GC.
- Soignez la stratégie de logs :
- rotation, driver adapté
- Préférez les volumes pour données et écritures intensives.
- Testez sous charge avec les mêmes limites qu’en prod.
14) Commandes récapitulatives (copier-coller)
# Identifier conteneur
docker ps
CID=<id_ou_nom>
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
# Voir cgroups
cat /proc/$PID/cgroup
mount | grep cgroup
# CPU throttling (v1 ou v2)
CGPATH=$(awk -F: '$1 == 0 {print $3}' /proc/$PID/cgroup)
cat /sys/fs/cgroup"$CGPATH"/cpu.stat 2>/dev/null || true
cat /sys/fs/cgroup"$CGPATH"/cpu.max 2>/dev/null || true
# Inspect limites Docker
docker inspect "$CID" | jq '.[0].HostConfig | {NanoCpus, CpuQuota, CpuPeriod, CpuShares, CpusetCpus, Memory, MemorySwap, BlkioWeight}'
# Vue globale
vmstat 1 5
iostat -xz 1 5
free -h
# I/O
sudo iotop -oPa
docker inspect --format '{{.LogPath}}' "$CID"
# Réseau
docker exec -it "$CID" sh -lc 'time curl -sS -o /dev/null -w "dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://example.com'
docker exec -it "$CID" sh -lc 'ss -tanp | head'
# OOM
dmesg -T | egrep -i 'oom|killed process' | tail -n 50
docker inspect --format '{{.State.OOMKilled}}' "$CID"
Conclusion
Déboguer un conteneur Docker lent revient à prouver quel mécanisme le freine :
- CPU throttling (quota CFS) : visible dans
cpu.stat, corrigé par--cpus/quota/cpuset. - I/O disque : visible via
iostat/iotop/strace, souvent aggravée par overlay2 et logs, corrigée par volumes, rotation, stockage plus performant ou suppression de limites I/O. - Mémoire : swap et
pgmajfaulttransforment une appli en « lenteur » sans crash, corrigé par dimensionnement et réglages applicatifs. - Limites mal configurées :
ulimit, cpuset, quotas trop agressifs, drivers de logs.
Si vous me donnez votre sortie (anonymisée) de docker inspect ... HostConfig, cpu.stat et iostat -xz 1, je peux vous aider à interpréter précisément où se situe le goulot et quelle correction appliquer.