Docker & ContainersXVIII Β· MonitoringMetric sources
Where the numbers come from β cgroup v2 metric sources
What you'll learn
- Locate a running container's cgroup directory on the host
- Read memory, CPU, PID and I/O accounting straight from the kernel
- Explain the difference between memory.current and the figure docker stats prints
- Collect CPU throttling, OOM kill counts and PSI, which most stacks omit
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11
docker stats and cAdvisor are not two sources of truth. They are
two readers of the same files under /sys/fs/cgroup. When they
disagree, one of them is doing arithmetic the other is not, and
knowing which arithmetic is the difference between a working alert
and a false one.
Finding the cgroup
With the systemd cgroup driver β the default on any systemd host β each container gets a scope named after its full ID:
$ docker info --format 'cgroup {{.CgroupVersion}} / driver {{.CgroupDriver}}'
CID=$(docker inspect grafana --format '{{.Id}}')
CG=/sys/fs/cgroup/system.slice/docker-$CID.scope
ls "$CG" | grep -E '^(memory|cpu|pids|io)\.' | head -14cgroup 2 / driver systemd
cpu.max
cpu.pressure
cpu.stat
cpu.weight
io.pressure
io.stat
memory.current
memory.events
memory.max
memory.peak
memory.pressure
memory.stat
pids.current
pids.maxEverything below is a plain file. cat is a valid monitoring agent.
Memory
$ cat "$CG/memory.current" "$CG/memory.max" "$CG/memory.peak"
grep -E '^(anon|file|inactive_file|active_file|kernel) ' "$CG/memory.stat"
cat "$CG/memory.events"297140224
536870912
346812416
anon 71393280
file 216993792
inactive_file 93097984
active_file 123895808
kernel 8499200
low 0
high 0
max 0
oom 0
oom_kill 0
oom_group_kill 0The discrepancy is not a bug, and reproducing the arithmetic is the whole point of this lesson:
memory.current 297140224 283 MiB everything charged to the cgroup
inactive_file 93097984 88 MiB reclaimable page cache
----------
current - inactive_file 194 MiB what docker stats prints
docker stats and cAdvisorβs container_memory_working_set_bytes
both report working set β current usage minus the page cache the
kernel can evict under pressure. That is the right number to alert
on, because it approximates what the container cannot give back.
memory.current is what the kernel compares against memory.max
when deciding to OOM-kill. A container whose memory.current is
pinned at the limit while its working set is comfortable is not in
trouble; it is using its whole allowance as file cache, which is
what caches are for.
CPU, and the metric that is actually missing
$ cat "$CG/cpu.max"
cat "$CG/cpu.stat"100000 100000
usage_usec 4855653170
user_usec 3681038790
system_usec 1174614380
nice_usec 0
core_sched.force_idle_usec 0
nr_periods 3411231
nr_throttled 9
throttled_usec 1488817
nr_bursts 0
burst_usec 0usage_usec is what every dashboard shows. nr_throttled and
throttled_usec are what explain your latency.
CFS enforces a quota per 100 ms period. A container that uses its entire quota in the first 30 ms of a period is stopped for the remaining 70 ms, no matter how idle the host is. Averaged over a minute the same container reports comfortable CPU usage. The p99 latency graph shows the truth and nobody can explain it.
PIDs and block I/O
cat "$CG/pids.current" "$CG/pids.max"
cat "$CG/io.stat"
pids.current against pids.max catches fork bombs and thread
leaks before they take the hostβs PID table with them. io.stat is
per block device, keyed by major:minor:
8:0 rbytes=378687488 wbytes=13639680 rios=2996 wios=1948 dbytes=0 dios=0
Map the device with lsblk -o NAME,MAJ:MIN when the numbers matter.
Pressure stall information
The most useful saturation signal on a modern kernel, and the one almost no Docker monitoring stack collects:
$ cat "$CG/memory.pressure"some avg10=0.00 avg60=0.00 avg300=0.00 total=884
full avg10=0.00 avg60=0.00 avg300=0.00 total=884cpu.pressure, memory.pressure and io.pressure answer the
question utilisation cannot: not βhow much of the resource is in
useβ but βhow much time did work spend waiting for itβ. A disk at
60% utilisation with io.pressure some avg60=40.0 is a disk that is
hurting you.
Mapping to Prometheus
Everything above has a cAdvisor equivalent. Collect the right-hand column and you have covered the sources.
| Kernel file | cAdvisor metric |
|---|---|
memory.current minus inactive_file | container_memory_working_set_bytes |
memory.current | container_memory_usage_bytes |
memory.max | container_spec_memory_limit_bytes |
memory.events oom_kill | container_oom_events_total |
cpu.stat usage_usec | container_cpu_usage_seconds_total |
cpu.stat nr_periods | container_cpu_cfs_periods_total |
cpu.stat nr_throttled | container_cpu_cfs_throttled_periods_total |
cpu.stat throttled_usec | container_cpu_cfs_throttled_seconds_total |
pids.current | container_processes |
io.stat | container_fs_reads_bytes_total, container_fs_writes_bytes_total |
Sanity check
Knowledge check Β· 4 questions
Q1. memory.current reads 297140224 and inactive_file in memory.stat reads 93097984. What will docker stats report as memory usage?
Q2. A latency-sensitive service averages 15% CPU usage but its p99 is erratic. Which cgroup value is most likely to explain it?
Q3. Which of these are recorded durably enough to answer a question the morning after? Select all that apply.
Q4. A PSI reading of io.pressure some avg60=40.0 means that over the last minute, 40% of wall time had at least one task blocked waiting for I/O.
Passing score: 75%. Answers are checked in this browser.