Trage Docker-containers debuggen: CPU-throttling, I/O-bottlenecks en verkeerd ingestelde limieten
Docker-containers die “ineens” traag worden zijn zelden magisch: meestal is er sprake van CPU-throttling door CFS-quota, I/O-wachttijden (disk of netwerk), memory pressure (swap/oom), of verkeerd ingestelde limieten (te strakke --cpus, --memory, --pids-limit, ulimit, blkio). Deze tutorial geeft een praktische, diepgaande aanpak om de oorzaak te vinden en gericht op te lossen, met echte commando’s voor Linux hosts (cgroups v1 en v2 waar relevant).
1) Symptomen scherp krijgen: wat betekent “traag”?
Voor je gaat meten, definieer je het probleem:
- Hoge latency (requests duren langer) maar CPU lijkt laag.
- Throughput zakt (minder requests/sec).
- Sporadische spikes (periodiek traag).
- Alle containers traag of één specifieke.
- Alleen onder load of ook idle.
Noteer:
- Containernaam/ID
- Tijdvenster wanneer traagheid optreedt
- Workload type: CPU-bound, I/O-bound, memory-bound, netwerk-bound
- Docker runtime: Docker Engine, containerd, Kubernetes (als van toepassing)
2) Snelle triage: host vs container
2.1 Host-overzicht
Op de host:
uptime
top -o %CPU
htop
free -h
df -hT
iostat -xz 1
vmstat 1
Interpretatie (globaal):
- Load average hoog + CPU idle laag → CPU-bound of teveel runnable threads.
- Load hoog + CPU idle hoog +
wa(iowait) hoog → I/O bottleneck. - Veel swap in gebruik → memory pressure, mogelijk thrashing.
- Disk bijna vol → filesystem kan traag worden; overlay2 kan extra pijn doen.
2.2 Docker-overzicht
docker ps --format 'table {{.Names}}\t{{.ID}}\t{{.Status}}\t{{.Image}}'
docker stats --no-stream
docker system df
Let op:
docker statstoont CPU%, MEM, NET I/O, BLOCK I/O. BLOCK I/O is vaak een hint richting disk.- CPU% in
docker statskan misleiden bij throttling: je ziet soms “laag” CPU, maar toch traag.
3) Containerlimieten inventariseren (veelvoorkomende valkuil)
3.1 Inspecteer de containerconfig
CID=mycontainer
docker inspect "$CID" --format '
Name: {{.Name}}
CPUQuota: {{.HostConfig.CpuQuota}}
CPUPeriod: {{.HostConfig.CpuPeriod}}
CPUShares: {{.HostConfig.CpuShares}}
CpusetCpus: {{.HostConfig.CpusetCpus}}
Memory: {{.HostConfig.Memory}}
MemorySwap: {{.HostConfig.MemorySwap}}
PidsLimit: {{.HostConfig.PidsLimit}}
BlkioWeight: {{.HostConfig.BlkioWeight}}
OomKillDisable: {{.HostConfig.OomKillDisable}}
'
Belangrijke interpretaties:
- CPUQuota/CPUPeriod: CFS quota. Bijvoorbeeld
CPUPeriod=100000enCPUQuota=50000betekent 0,5 CPU. - CpusetCpus: pinning naar specifieke cores; kan performance beperken of NUMA-issues veroorzaken.
- Memory: harde limiet. Te laag → OOM kills of agressieve reclaim.
- MemorySwap: als
-1onbeperkt swap (afhankelijk van host), als gelijk aanMemory→ swap uitgeschakeld voor container. - PidsLimit: te laag kan tot “resource temporarily unavailable” leiden, wat als traagheid kan aanvoelen.
- BlkioWeight: relatieve I/O-prioriteit (cgroups v1).
3.2 Check ook ulimits (file descriptors, processes)
docker inspect "$CID" --format '{{json .HostConfig.Ulimits}}' | jq
En binnen de container:
docker exec -it "$CID" sh -lc 'ulimit -a'
Te lage nofile kan leiden tot accept() failures, connection churn en latentie.
4) CPU-throttling: herkennen, meten en oplossen
CPU-throttling is een van de meest onderschatte oorzaken: je app wil CPU, maar de kernel knijpt hem af door CFS quota. Resultaat: latency stijgt, CPU lijkt “niet vol”, en threads wachten.
4.1 Begrijp CFS quota in het kort
- CFS werkt met een periode (vaak 100ms = 100000µs).
- Binnen die periode mag een cgroup maximaal quota microseconden CPU gebruiken.
- Is quota op, dan wordt de groep throttled tot de volgende periode.
Voorbeeld: --cpus=1 → quota ~100ms per 100ms.
--cpus=0.2 → quota ~20ms per 100ms: korte bursts, daarna 80ms wachten.
4.2 Throttling meten via cgroups (v2 en v1)
Eerst: vind de PID van de container init:
PID=$(docker inspect -f '{{.State.Pid}}' "$CID")
echo "$PID"
cgroups v2 (moderne distro’s)
Check mount:
stat -fc %T /sys/fs/cgroup
# verwacht: cgroup2fs
Lees CPU-statistieken:
CG=$(cat /proc/$PID/cgroup | awk -F: '$2=="" {print $3}')
# Bij v2 is dit vaak een pad als /docker/<id> of /system.slice/...
cat /sys/fs/cgroup"$CG"/cpu.stat
Je ziet velden zoals:
usage_usecuser_usec,system_usecnr_periodsnr_throttledthrottled_usec
Signaal: nr_throttled en throttled_usec lopen snel op tijdens traagheid.
cgroups v1 (oudere setups)
Mount check:
mount | grep cgroup
CPU cgroup pad vinden:
cat /proc/$PID/cgroup | grep cpu
Lees:
# pad kan verschillen; voorbeeld:
cat /sys/fs/cgroup/cpu/docker/<containerid>/cpu.stat
cat /sys/fs/cgroup/cpu/docker/<containerid>/cpu.cfs_quota_us
cat /sys/fs/cgroup/cpu/docker/<containerid>/cpu.cfs_period_us
cpu.stat bevat vaak:
nr_periodsnr_throttledthrottled_time(ns)
4.3 Correlatie met app-symptomen
Typische patronen:
- p95/p99 latency piekt elke ~100ms (of periodiek).
- CPU usage in metrics blijft onder 100% van toegewezen CPU, maar throughput blijft laag.
- In Java/.NET: GC of JIT kan extra gevoelig zijn voor throttling; in Go: scheduler ziet minder CPU-slices.
4.4 Oplossingen voor CPU-throttling
- Geef meer CPU (meest direct):
docker update --cpus 2 "$CID"
- Gebruik cpuset i.p.v. quota (voor voorspelbaarheid):
docker update --cpuset-cpus="2-3" "$CID"
Let op: pinning kan contention verminderen, maar ook juist problemen geven als je te weinig cores kiest.
-
Verhoog quota-burst gedrag (indirect): hogere
--cpusof langere periode (niet altijd exposed). In Kubernetes kun jecpu.cfs_period_ussoms tunen via kubelet flags, maar dat is advanced en niet altijd wenselijk. -
Check host contention: als host overcommit CPU heeft, kan zelfs zonder quota performance instorten. Kijk naar:
mpstat -P ALL 1
pidstat -u 1
5) I/O-bottlenecks: disk, overlay2 en “iowait”
Als CPU niet de limiter is, is I/O vaak de boosdoener. Containers gebruiken meestal overlay2, wat extra overhead kan geven bij veel kleine writes, fsync, of metadata-heavy workloads.
5.1 Host I/O meten
Installeer tools indien nodig:
sudo apt-get update
sudo apt-get install -y sysstat iotop
Meet:
iostat -xz 1
Belangrijke velden per device:
%utildicht bij 100% → device verzadigd.awaithoog → wachttijd (latency).r/s,w/s,rkB/s,wkB/s→ throughput.svctm(soms) en queue metrics (afhankelijk van versie).
Processen die I/O doen:
sudo iotop -oPa
Per-proces I/O statistieken:
pidstat -d 1
5.2 Container I/O meten
docker stats geeft BLOCK I/O, maar zonder latency. Combineer met host tools:
- Vind container PID en kijk welke processen I/O doen:
PID=$(docker inspect -f '{{.State.Pid}}' "$CID")
ps -p "$PID" -o pid,cmd
sudo ls -l /proc/"$PID"/fd | head
- Gebruik
nsenterom in de mount namespace te kijken (handig voor path mapping):
sudo nsenter -t "$PID" -m -p -u -i -n sh
# binnen:
mount | head
df -hT
exit
5.3 Overlay2 valkuilen en oplossingen
Symptomen:
- Veel writes in container filesystem (niet in volume).
- Veel kleine file updates (logs, sqlite, cache).
- Hoge
awaiten%utilop de disk waar/var/lib/dockerstaat.
Aanpak:
- Zet write-heavy data op een volume of bind mount:
docker run -v /fastdisk/appdata:/var/lib/appdata ...
- Maak logging minder duur:
- Docker json-file logging kan I/O veroorzaken. Check driver:
docker inspect "$CID" --format '{{.HostConfig.LogConfig.Type}}'
Overweeg local driver met rotatie:
docker run --log-driver local --log-opt max-size=10m --log-opt max-file=3 ...
-
Let op
fsync-intensieve databases: gebruik dedicated storage, tune durability waar verantwoord, of gebruik managed DB. -
Check filesystem en storage stack:
- Is dit een netwerkdisk? (EBS, NFS, Ceph)
- Is er encryptie/compressie die CPU of latency toevoegt?
- Is er throttling op cloud volumes (IOPS/throughput caps)?
5.4 Blkio/cgroup I/O limits
Soms is de container beperkt door blkio settings (v1) of io controller (v2).
- Docker inspect:
docker inspect "$CID" --format '
BlkioWeight: {{.HostConfig.BlkioWeight}}
BlkioDeviceReadBps: {{json .HostConfig.BlkioDeviceReadBps}}
BlkioDeviceWriteBps: {{json .HostConfig.BlkioDeviceWriteBps}}
BlkioDeviceReadIOps: {{json .HostConfig.BlkioDeviceReadIOps}}
BlkioDeviceWriteIOps: {{json .HostConfig.BlkioDeviceWriteIOps}}
' | sed 's/\\n/\n/g'
Als je per ongeluk een lage limiet hebt gezet, verhoog of verwijder die.
6) Memory pressure: OOM, swap, reclaim en “stille” traagheid
Te weinig memory veroorzaakt niet altijd meteen een crash. Vaak zie je:
- Major page faults stijgen
- Swap in/out (als swap aan staat)
- Kernel reclaim en compaction
- Latency spikes
6.1 Memorylimieten en actuele usage
docker stats --no-stream "$CID"
docker inspect "$CID" --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
Binnen container (cgroup-aware tools zijn beter, maar dit helpt):
docker exec -it "$CID" sh -lc 'free -h || true; cat /proc/meminfo | head'
6.2 OOM events checken
Host kernel log:
sudo dmesg -T | egrep -i 'oom|killed process|out of memory' | tail -n 50
Container exit code 137 wijst vaak op OOM kill.
6.3 cgroups v2 memory.stat (zeer nuttig)
PID=$(docker inspect -f '{{.State.Pid}}' "$CID")
CG=$(cat /proc/$PID/cgroup | awk -F: '$2=="" {print $3}')
cat /sys/fs/cgroup"$CG"/memory.current
cat /sys/fs/cgroup"$CG"/memory.max
cat /sys/fs/cgroup"$CG"/memory.stat | egrep 'file|anon|slab|pgfault|pgmajfault|workingset|oom'
Signalen:
pgmajfaultloopt op → disk reads door page faults (traag).memory.currentdicht tegenmemory.max→ constante reclaim.
6.4 Oplossingen
- Verhoog memory:
docker update --memory 2g --memory-swap 2g "$CID"
--memory-swap gelijk aan --memory betekent geen extra swap (kan goed zijn voor latency, maar verhoogt OOM-risico).
Wil je wél swap toestaan: zet --memory-swap hoger dan --memory (als host swap heeft en policy dit toelaat).
- Fix memory leaks, caches, te grote JVM heap, etc.
- Zet tmp-files op tmpfs (als passend):
docker run --tmpfs /tmp:rw,noexec,nosuid,size=256m ...
7) Netwerkbottlenecks en DNS: “traag” zonder CPU/I/O signalen
Soms is de container traag door:
- DNS timeouts
- Packet loss
- Conntrack issues
- Rate limiting
- Service discovery problemen
7.1 Basis netwerkmetingen
In container:
docker exec -it "$CID" sh -lc '
ip addr
ip route
cat /etc/resolv.conf
getent hosts example.com || nslookup example.com || true
'
Latency naar dependencies:
docker exec -it "$CID" sh -lc 'apk add --no-cache curl 2>/dev/null || true; curl -sS -o /dev/null -w "time_total=%{time_total}\n" https://example.com'
Op host:
ss -s
ss -tanp | head
sudo conntrack -S 2>/dev/null || true
7.2 DNS als sluipmoordenaar
Veel apps doen DNS lookups per request. Als /etc/resolv.conf naar een trage resolver wijst of er timeouts zijn, krijg je latency spikes.
Test resolvers:
docker exec -it "$CID" sh -lc 'time getent hosts kubernetes.default.svc.cluster.local 2>/dev/null || true'
Oplossingen:
- Cache DNS (nscd, systemd-resolved, of app-level caching).
- Fix upstream resolver of reduceer lookup-frequentie.
- In Docker: check of je custom DNS hebt gezet (
--dns).
8) “Verkeerd ingestelde limieten”: concrete checklist
Hier een lijst van limieten die vaak per ongeluk te strak staan en “traagheid” veroorzaken:
8.1 CPU: --cpus, --cpu-quota, --cpu-period, --cpuset-cpus
Check:
docker inspect "$CID" --format 'CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} Cpuset={{.HostConfig.CpusetCpus}}'
8.2 Memory: --memory, --memory-swap
Check:
docker inspect "$CID" --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
8.3 PIDs: --pids-limit
Te lage pids-limit kan thread creation blokkeren → request handlers wachten.
Check:
docker inspect "$CID" --format 'PidsLimit={{.HostConfig.PidsLimit}}'
8.4 File descriptors: --ulimit nofile=...
Check:
docker exec -it "$CID" sh -lc 'ulimit -n'
8.5 IPC/shm: --shm-size
Voor o.a. Chrome, Postgres, sommige ML workloads. Te klein → crashes of fallback gedrag.
Check:
docker inspect "$CID" --format 'ShmSize={{.HostConfig.ShmSize}}'
Run met:
docker run --shm-size=1g ...
8.6 Storage driver en log driver
Check:
docker info | egrep 'Storage Driver|Logging Driver'
9) Praktische debugflow: van symptoom naar oorzaak
Onderstaande flow werkt goed in incidenten.
Stap A: bevestig of het CPU, memory, disk of netwerk is
- Host:
vmstat 1 iostat -xz 1 mpstat -P ALL 1 - Container:
docker stats --no-stream "$CID"
Stap B: check CPU-throttling expliciet
- cgroups v2:
PID=$(docker inspect -f '{{.State.Pid}}' "$CID") CG=$(cat /proc/$PID/cgroup | awk -F: '$2=="" {print $3}') watch -n 1 "cat /sys/fs/cgroup$CG/cpu.stat" - Let op
nr_throttledenthrottled_usec.
Stap C: check disk latency en saturation
iostat -xz 1
sudo iotop -oPa
Als /var/lib/docker op dezelfde disk staat als je database volumes: overweeg scheiding.
Stap D: check memory pressure / OOM / swap
free -h
sudo dmesg -T | egrep -i 'oom|killed process' | tail
cgroups v2:
cat /sys/fs/cgroup$CG/memory.current
cat /sys/fs/cgroup$CG/memory.max
cat /sys/fs/cgroup$CG/memory.stat | egrep 'pgmajfault|oom|anon|file'
Stap E: check netwerk en DNS
docker exec -it "$CID" sh -lc 'time getent hosts example.com'
docker exec -it "$CID" sh -lc 'curl -sS -o /dev/null -w "connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://example.com'
10) Voorbeeldscenario’s (met diagnose en fix)
Scenario 1: API is traag, CPU lijkt laag, maar throttling is hoog
Observatie:
docker stats: CPU 20%, latency hoog.cpu.stat:nr_throttledstijgt snel.
Diagnose: container heeft --cpus=0.5 en workload heeft bursts.
Fix:
docker update --cpus 2 mycontainer
# of pin naar 2 cores:
docker update --cpuset-cpus="0-1" mycontainer
Scenario 2: Database in container, hoge iowait, %util ~100%
Observatie:
iostat -xz 1:await30-200ms,%util99-100% opnvme0n1.iotop: database process schrijft veel.
Diagnose: disk verzadigd; overlay2 en log writes verergeren.
Fixes:
- Zet data op dedicated volume op snellere disk.
- Verminder container filesystem writes.
- Configureer log-rotatie of andere log driver.
Scenario 3: Periodieke latency spikes door DNS timeouts
Observatie:
- CPU/disk/memory ok.
time getent hosts service.localsoms 5s.
Diagnose: resolver timeouts / slechte DNS.
Fix:
- Fix DNS upstream, of configureer
--dnsnaar betrouwbare resolver. - Cache DNS; verminder lookups in app.
11) Handige tools en technieken (advanced)
11.1 Per-thread CPU en scheduling delays
pidstat -t -p "$PID" 1
11.2 eBPF (als je dit mag gebruiken)
Met bcc/bpftrace kun je latency in syscalls of block I/O zien. Voorbeeld met bpftrace (als geïnstalleerd):
sudo bpftrace -e 'tracepoint:block:block_rq_complete { @lat[comm] = hist(args->nr_sector); }'
(Exacte scripts verschillen per kernel; dit is vooral om te illustreren dat eBPF diep inzicht kan geven.)
11.3 perf voor CPU hotspots
sudo perf top -p "$PID"
# of sampling:
sudo perf record -F 99 -p "$PID" -g -- sleep 30
sudo perf report
12) Preventie: maak throttling en bottlenecks zichtbaar vóór incidenten
- Expose cgroup metrics:
- In Kubernetes: cAdvisor/metrics-server/Prometheus.
- Op Docker hosts: node_exporter + cgroup collectors, of scrape
/sys/fs/cgroup/....
- Alert op:
- CPU throttled time (rate)
- Disk
awaiten%util - OOM kills
pgmajfaultrate- DNS latency (synthetic checks)
- Capacity planning:
- Zet realistische CPU/memory limits.
- Vermijd te agressieve overcommit voor latency-gevoelige services.
13) Samenvatting (snelle checklist)
- CPU traag? Check
cpu.statthrottling en quota (--cpus). - I/O traag? Check
iostat -xz,iotop, overlay2 writes, log driver. - Memory traag? Check OOM logs,
memory.current/max,pgmajfault, swap. - Netwerk/DNS traag? Meet
getent hosts,curl -w,ss, conntrack. - Limieten fout? Inspecteer
CpuQuota/Period,Memory/Swap,PidsLimit,Ulimits,ShmSize, blkio.
Als je wil, kan ik je helpen met een gerichte diagnose als je de output deelt van:
docker inspect <container>(relevante HostConfig velden)docker stats --no-streamiostat -xz 1(10 seconden)- cgroup
cpu.statenmemory.statvoor de container