← Terug naar tutorials

Trage Docker-containers debuggen: CPU-throttling, I/O-bottlenecks en verkeerd ingestelde limieten

dockercontainer performancecpu throttlingcgroupsi/o bottleneckoverlay2resource limitslinux performanceobservabilitytroubleshooting

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:

Noteer:


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):

2.2 Docker-overzicht

docker ps --format 'table {{.Names}}\t{{.ID}}\t{{.Status}}\t{{.Image}}'
docker stats --no-stream
docker system df

Let op:


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:

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

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:

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:

4.3 Correlatie met app-symptomen

Typische patronen:

4.4 Oplossingen voor CPU-throttling

  1. Geef meer CPU (meest direct):
docker update --cpus 2 "$CID"
  1. 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.

  1. Verhoog quota-burst gedrag (indirect): hogere --cpus of langere periode (niet altijd exposed). In Kubernetes kun je cpu.cfs_period_us soms tunen via kubelet flags, maar dat is advanced en niet altijd wenselijk.

  2. 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:

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:

PID=$(docker inspect -f '{{.State.Pid}}' "$CID")
ps -p "$PID" -o pid,cmd
sudo ls -l /proc/"$PID"/fd | head
sudo nsenter -t "$PID" -m -p -u -i -n sh
# binnen:
mount | head
df -hT
exit

5.3 Overlay2 valkuilen en oplossingen

Symptomen:

Aanpak:

  1. Zet write-heavy data op een volume of bind mount:
docker run -v /fastdisk/appdata:/var/lib/appdata ...
  1. Maak logging minder duur:
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 ...
  1. Let op fsync-intensieve databases: gebruik dedicated storage, tune durability waar verantwoord, of gebruik managed DB.

  2. Check filesystem en storage stack:

5.4 Blkio/cgroup I/O limits

Soms is de container beperkt door blkio settings (v1) of io controller (v2).

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:

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:

6.4 Oplossingen

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).

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:

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:


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

  1. Host:
    vmstat 1
    iostat -xz 1
    mpstat -P ALL 1
  2. Container:
    docker stats --no-stream "$CID"

Stap B: check CPU-throttling expliciet

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:

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:

Diagnose: disk verzadigd; overlay2 en log writes verergeren.

Fixes:

Scenario 3: Periodieke latency spikes door DNS timeouts

Observatie:

Diagnose: resolver timeouts / slechte DNS.

Fix:


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

  1. Expose cgroup metrics:
  1. Alert op:
  1. Capacity planning:

13) Samenvatting (snelle checklist)

Als je wil, kan ik je helpen met een gerichte diagnose als je de output deelt van: