cgroups v2 wirklich verstehen: Wie Docker CPU-, Memory- und IO-Limits umsetzt
AI generated
FROM
RUN
Docker · Linux · Kernel
cgroups v2 wirklich verstehen
Was hinter --cpus und --memory technisch passiert

Wenn docker run --memory=512m --cpus=1.5 ausgeführt wird, schreibt Docker im Hintergrund schlicht ein paar Werte in Textdateien unterhalb von /sys/fs/cgroup. Wer diese Dateien einmal selbst gelesen hat, versteht Ressourcenlimits nicht mehr als Docker-Magie, sondern als das, was sie sind: Kernel-Mechanik.

17 Min. Lesezeit cpu.max memory.max io.max Unified Hierarchy

1. Was cgroups sind und warum Docker sie braucht

Control Groups, kurz cgroups, sind ein Linux-Kernel-Feature, das Prozessgruppen bündelt und für diese Gruppen Ressourcenverbrauch begrenzt, priorisiert und misst. Docker nutzt cgroups nicht als eigene Erfindung, sondern als eine von zwei tragenden Säulen der Container-Isolation, neben Namespaces. Während Namespaces dafür sorgen, dass ein Container seine eigene Sicht auf Prozesse, Netzwerk und Dateisystem hat, sorgen cgroups dafür, dass ein Container nicht unbegrenzt CPU, Speicher oder I/O-Bandbreite des Hosts beanspruchen kann.

Jeder gestartete Container bekommt dabei automatisch eine eigene cgroup zugewiesen, unabhängig davon, ob explizite Limits gesetzt wurden. Ohne Limits ist diese cgroup praktisch unbegrenzt, mit einem Flag wie --memory schreibt der Docker-Daemon konkrete Zahlenwerte in die entsprechenden Steuerdateien dieser cgroup. Der Kernel selbst, nicht Docker, überwacht danach den tatsächlichen Ressourcenverbrauch und greift ein, sobald ein Limit überschritten wird, etwa durch Drosselung bei der CPU oder durch den OOM-Killer beim Speicher.

2. Die Unified Hierarchy in cgroups v2

In cgroups v2 gibt es nur noch eine einzige, einheitliche Verzeichnishierarchie unter /sys/fs/cgroup, die sogenannte Unified Hierarchy. Jede cgroup ist dort ein Verzeichnis, das eine Reihe von Steuerdateien enthält, sogenannte Controller-Interface-Files. Für einen laufenden Docker-Container liegt diese cgroup typischerweise unter einem Pfad wie /sys/fs/cgroup/system.slice/docker-.scope/, wenn Docker mit dem systemd-Cgroup-Treiber läuft, oder unter /sys/fs/cgroup/docker// beim cgroupfs-Treiber.

Innerhalb dieses Verzeichnisses finden sich Dateien wie cpu.max, memory.max, io.max und viele weitere, die jeweils genau einen Ressourcentyp steuern. Diese Dateien lassen sich mit einfachen Shell-Befehlen wie cat lesen und mit ausreichenden Rechten auch direkt beschreiben, ganz ohne Docker-CLI. Das macht die Unified Hierarchy zu einem hervorragenden Debugging-Werkzeug: Wenn ein Limit nicht wie erwartet wirkt, lohnt sich immer zuerst der Blick in genau diese Dateien, bevor komplexere Ursachen vermutet werden.


# cgroup-Pfad eines laufenden Containers ermitteln
CID=$(docker inspect --format '{{.Id}}' mein-container)
docker exec mein-container cat /proc/self/cgroup

# Auf dem Host: Verzeichnis der Container-cgroup anzeigen
ls /sys/fs/cgroup/system.slice/docker-${CID}.scope/
# oder je nach Treiber:
ls /sys/fs/cgroup/docker/${CID}/

3. cpu.max: Wie CPU-Limits kodiert werden

Die Datei cpu.max enthält genau zwei durch ein Leerzeichen getrennte Zahlen: die Quota und die Periode, beide in Mikrosekunden. Die Periode ist standardmäßig 100000 Mikrosekunden, also 100 Millisekunden. Die Quota gibt an, wie viel CPU-Zeit die Prozesse in dieser cgroup innerhalb einer Periode maximal verbrauchen dürfen. Ein Wert von 150000 100000 bedeutet: In jedem 100-Millisekunden-Fenster dürfen die Prozesse insgesamt 150 Millisekunden CPU-Zeit verbrauchen, was 1,5 CPU-Kernen entspricht, wenn sie parallel auf mehreren Kernen laufen könnten.

Wenn kein Limit gesetzt ist, steht in cpu.max schlicht das Wort max anstelle einer Quota, was unbegrenzte CPU-Nutzung bedeutet. Genau dieser Mechanismus liegt hinter dem Docker-Flag --cpus: docker run --cpus=1.5 übersetzt sich intern in exakt die Werte 150000 100000 in der Datei cpu.max der Container-cgroup. Wer diese Datei direkt liest, sieht also exakt, was Docker im Hintergrund konfiguriert hat, ohne Umweg über docker inspect.


# CPU-Limit eines Containers direkt aus der cgroup lesen
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/cpu.max
# Ausgabe z.B.: 150000 100000  --> 1.5 CPU-Kerne

# Zum Vergleich: Docker mit --cpus=1.5 starten
docker run -d --name cpu-test --cpus=1.5 nginx:alpine
docker exec cpu-test cat /sys/fs/cgroup/cpu.max

4. memory.max, memory.high und memory.swap.max

Beim Speicher gibt es in cgroups v2 mehrere abgestufte Steuerdateien statt nur einer harten Grenze. memory.max ist das harte Limit, entspricht dem Docker-Flag --memory: Wird dieser Wert überschritten und lässt sich kein Speicher mehr freigeben, greift der OOM-Killer innerhalb dieser cgroup. Daneben existiert memory.high, ein weicheres Limit, ab dem der Kernel Prozesse aktiv drosselt und versucht, Speicher zurückzugewinnen, bevor es überhaupt zum harten Limit kommt, eine Art Frühwarnstufe.

memory.swap.max steuert separat, wie viel Swap-Speicher zusätzlich zum regulären Memory-Limit genutzt werden darf. Das entspricht dem Docker-Flag --memory-swap, wobei Dockers Rechenlogik hier etwas gewöhnungsbedürftig ist: Der Wert von --memory-swap bezeichnet die Summe aus RAM und Swap zusammen, nicht den Swap-Anteil allein. Wer docker run --memory=512m --memory-swap=512m setzt, deaktiviert Swap faktisch komplett, weil dann kein zusätzlicher Swap-Anteil über das RAM-Limit hinaus übrig bleibt.


# Memory-Limits einer laufenden Container-cgroup pruefen
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.high
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.swap.max

# Aktuellen Verbrauch und Spitzenwert seit Containerstart lesen
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.current
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.peak

5. io.max: Bandbreiten- und IOPS-Limits pro Gerät

Die Datei io.max unterscheidet sich strukturell von cpu.max und memory.max, weil I/O-Limits geräteabhängig sind: Ein Host kann mehrere Block-Devices haben, und ein Limit für eine SSD ergibt für eine andere Festplatte keinen Sinn. Deshalb enthält jede Zeile in io.max zuerst die Major:Minor-Gerätenummer, gefolgt von Schlüssel-Wert-Paaren wie rbps für Lesebandbreite in Bytes pro Sekunde, wbps für Schreibbandbreite, sowie riops und wiops für die jeweiligen IOPS-Grenzen.

Docker setzt diese Werte über Flags wie --device-read-bps und --device-write-iops, die explizit ein Gerät referenzieren müssen, weil der Kernel sonst nicht weiß, für welches Device der Wert gelten soll. In der Praxis werden I/O-Limits deutlich seltener genutzt als CPU- und Memory-Limits, sind aber entscheidend, wenn ein einzelner Container mit intensiven Schreibzugriffen die I/O-Performance aller anderen Container auf demselben Host beeinträchtigen könnte, etwa bei Datenbank- oder Log-Workloads auf gemeinsam genutztem Storage.


# Geraetenummer der Root-Disk ermitteln
stat -c '%t:%T' -L /var/lib/docker

# IO-Limit setzen: max. 10 MB/s Lesebandbreite auf /dev/sda
docker run -d --device-read-bps /dev/sda:10mb nginx:alpine

# Ergebnis direkt in der cgroup nachlesen
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/io.max
# Ausgabe z.B.: 8:0 rbps=10485760 wbps=max riops=max wiops=max

6. Der Unterschied zu cgroups v1

In cgroups v1 gab es für jeden Controller, also CPU, Memory, Blkio und weitere, eine eigene, unabhängige Verzeichnishierarchie unter jeweils eigenen Mountpunkten wie /sys/fs/cgroup/cpu/ und /sys/fs/cgroup/memory/. Ein Prozess konnte dadurch theoretisch in unterschiedlichen Gruppen für unterschiedliche Controller stecken, was die Verwaltung komplex machte und Inkonsistenzen begünstigte. Auch die Dateinamen unterschieden sich: Statt memory.max hieß die Datei in v1 memory.limit_in_bytes, statt cpu.max gab es getrennt cpu.cfs_quota_us und cpu.cfs_period_us.

cgroups v2 hat diese Fragmentierung durch die Unified Hierarchy ersetzt: Ein Prozess gehört zu genau einer cgroup, und diese eine cgroup steuert alle Ressourcentypen gemeinsam über konsistent benannte Dateien. Das vereinfacht nicht nur die Administration, sondern schließt auch eine Klasse von Sicherheitslücken, die aus der Möglichkeit entstanden, cgroup-Zugehörigkeiten pro Controller unterschiedlich zu manipulieren. Seit Docker 20.10 wird cgroups v2 unterstützt, seit Docker 24 ist es auf modernen Distributionen wie Ubuntu 22.04 oder neuer der Standard, sofern der Kernel und das Init-System es anbieten.

7. Ein laufender Container live inspizieren

Am anschaulichsten wird das Konzept, wenn man einen Container mit gesetzten Limits startet und die cgroup-Dateien währenddessen live beobachtet. Ein einfacher Container mit CPU- und Memory-Limit lässt sich starten, während parallel mit einem Watch-Befehl der aktuelle Ressourcenverbrauch mitgelesen wird. So wird sichtbar, wie memory.current mit der Last im Container steigt und wie cpu.stat mit jeder verstrichenen Periode neue Werte für verbrauchte CPU-Zeit anzeigt.

Diese Live-Beobachtung ist auch der zuverlässigste Weg, um zu verifizieren, dass ein gesetztes Limit tatsächlich greift, unabhängig davon, was docker inspect an konfigurierten Werten anzeigt. Es kommt vor, dass ein Limit zwar korrekt gesetzt, aber durch einen falsch konfigurierten Cgroup-Treiber oder eine veraltete Docker-Version nicht tatsächlich durchgesetzt wird. Der direkte Blick in die cgroup-Dateien auf dem Host ist in solchen Fällen die einzige Methode, die Wahrheit unabhängig von der Docker-CLI zu prüfen.


# Container mit Limits starten
docker run -d --name live-test --cpus=0.5 --memory=256m \
  polinux/stress stress --cpu 2 --vm 1 --vm-bytes 200M --timeout 60s

CID=$(docker inspect --format '{{.Id}}' live-test)
CG=/sys/fs/cgroup/system.slice/docker-${CID}.scope

# Live mitverfolgen, alle 2 Sekunden
watch -n2 "cat $CG/memory.current; echo ---; cat $CG/cpu.stat"

8. Was sich für Nutzer praktisch ändert

Für die tägliche Arbeit mit Docker ändert sich durch cgroups v2 an der Oberfläche fast nichts: Die Flags --cpus, --memory und --device-read-bps funktionieren syntaktisch identisch wie unter cgroups v1. Der Unterschied zeigt sich erst, wenn etwas nicht wie erwartet funktioniert oder wenn tiefergehendes Debugging nötig ist, weil sich die Pfade und teils die Dateinamen der zugrunde liegenden Steuerdateien geändert haben. Wer alte Debugging-Skripte oder Monitoring-Tools mit hartkodierten v1-Pfaden wie cpu.cfs_quota_us im Einsatz hat, muss diese für v2-Hosts anpassen.

Ein praktischer Vorteil von v2 ist die konsistentere und oft genauere Ressourcenberichterstattung, etwa über memory.peak, das den Speicher-Höchstwert seit Erstellung der cgroup ohne separates Monitoring-Tool zugänglich macht. Auch Tools wie docker stats oder cAdvisor liefern unter cgroups v2 in der Regel präzisere Werte, weil die zugrunde liegenden Kernel-Schnittstellen konsistenter sind. Mit docker info lässt sich schnell prüfen, welche Cgroup-Version und welcher Treiber auf einem Host aktiv sind.


# Aktive cgroup-Version und Treiber pruefen
docker info | grep -i cgroup
# Cgroup Driver: systemd
# Cgroup Version: 2

# Alternative: direkt am Kernel pruefen
mount | grep cgroup2
stat -fc %T /sys/fs/cgroup/

9. cgroups v1 und v2 im direkten Vergleich

Die wichtigsten strukturellen Unterschiede lassen sich am besten tabellarisch zusammenfassen, weil sich Pfade, Dateinamen und die grundlegende Architektur zwischen den beiden Versionen an mehreren Stellen gleichzeitig ändern. Wer heute noch auf einem älteren Host mit cgroups v1 arbeitet, etwa auf CentOS 7 oder älteren Debian-Versionen, sollte diese Unterschiede kennen, um Debugging-Befehle korrekt zwischen beiden Welten übersetzen zu können.

Für neue Deployments ist die Empfehlung eindeutig: cgroups v2 nutzen, wo immer der Kernel und die Distribution es anbieten, weil hier sowohl das Ressourcenmanagement konsistenter als auch das Debugging über die Unified Hierarchy deutlich einfacher ist. Die folgende Tabelle stellt die zentralen Unterschiede gegenüber, als schnelle Nachschlagehilfe für den nächsten Wechsel zwischen einem v1- und einem v2-Host.

Merkmal cgroups v1 cgroups v2 Praxisrelevanz
Hierarchie Mehrere, pro Controller getrennt Eine Unified Hierarchy Einfacheres Debugging in v2
CPU-Limit-Datei cpu.cfs_quota_us / cpu.cfs_period_us cpu.max (zwei Werte in einer Datei) Andere Pfade in Skripten nötig
Memory-Limit-Datei memory.limit_in_bytes memory.max, memory.high v2 bietet weiche Vorwarnstufe
Docker-Support Seit jeher Ab Docker 20.10, Standard ab v24 Mit docker info pruefbar

Mironsoft

Container-Infrastruktur, CI-Pipelines und Deployment-Automatisierung

Docker-Setups, die im Team und in Produktion tragfähig bleiben?

Wir prüfen bestehende Dockerfiles und Compose-Stacks auf Sicherheitslücken, aufgeblähte Images und fragile Build-Pipelines und bauen daraus eine Container-Infrastruktur, die schnell baut, sicher läuft und im Team nachvollziehbar bleibt.

Dockerfile-Review

Multi-Stage-Builds, Layer-Caching und Image-Größe systematisch optimieren.

Security-Audit

Container-Isolation, Secrets-Handling und Image-Scanning gegen echte Angriffsflächen absichern.

CI/CD-Integration

Build-Pipelines, Registries und Deployment-Strategien für reproduzierbare Releases aufbauen.

10. Zusammenfassung

cgroups v2 Limits: Das Wichtigste auf einen Blick

Kernidee

Docker-Flags wie --cpus schreiben nur Werte in Textdateien unter /sys/fs/cgroup.

Wichtigste Datei CPU

cpu.max enthält Quota und Periode in Mikrosekunden, getrennt durch ein Leerzeichen.

Wichtigste Datei Memory

memory.max ist das harte Limit, memory.high die weiche Vorwarnstufe.

v1 zu v2

Eine Unified Hierarchy statt vieler getrennter Controller-Bäume, seit Docker 24 Standard.

11. FAQ: cgroups v2 Limits: Das Wichtigste auf einen Blick

1Was ist der Unterschied zwischen cgroups und Namespaces?
Namespaces isolieren die Sicht eines Prozesses auf Ressourcen wie Netzwerk, Prozess-IDs oder Dateisysteme. Cgroups begrenzen und messen dagegen den tatsächlichen Ressourcenverbrauch wie CPU-Zeit, Speicher oder I/O-Bandbreite. Beide zusammen bilden die Grundlage der Container-Isolation.
2Wo finde ich die cgroup eines laufenden Docker-Containers?
Über docker inspect --format '{{.Id}}' lässt sich die Container-ID ermitteln, die dann im Pfad unter /sys/fs/cgroup/system.slice/docker-.scope/ (systemd-Treiber) oder /sys/fs/cgroup/docker// (cgroupfs-Treiber) auftaucht.
3Was bedeuten die zwei Zahlen in cpu.max?
Die erste Zahl ist die Quota, die zweite die Periode, beide in Mikrosekunden. Die Quota gibt an, wie viel CPU-Zeit pro Periode maximal verbraucht werden darf. Ein Wert von 150000 100000 entspricht 1,5 CPU-Kernen.
4Was ist der Unterschied zwischen memory.max und memory.high?
memory.max ist das harte Limit, bei dessen Überschreitung der OOM-Killer eingreift. memory.high ist eine weichere Schwelle, ab der der Kernel bereits aktiv drosselt und versucht, Speicher freizugeben, bevor das harte Limit erreicht wird.
5Wie hängt --memory-swap mit memory.swap.max zusammen?
Docker rechnet --memory-swap als Summe aus RAM- und Swap-Anteil, nicht als reinen Swap-Wert. Wird --memory-swap gleich --memory gesetzt, bleibt für memory.swap.max kein zusätzlicher Spielraum übrig, Swap ist damit faktisch deaktiviert.
6Warum enthält io.max Geraetenummern?
I/O-Limits gelten pro Block-Device, weil unterschiedliche Geräte wie SSDs und Festplatten unterschiedliche Performance-Charakteristiken haben. Deshalb muss jede Limit-Zeile in io.max mit der Major:Minor-Nummer des jeweiligen Geräts beginnen.
7Wie finde ich heraus, ob mein Host cgroups v1 oder v2 nutzt?
Der Befehl docker info zeigt die Zeile Cgroup Version direkt an. Alternativ zeigt stat -fc %T /sys/fs/cgroup/ den Dateisystemtyp: cgroup2fs bedeutet v2, tmpfs deutet meist auf v1 mit mehreren Mountpunkten hin.
8Seit welcher Docker-Version wird cgroups v2 unterstützt?
Grundlegende Unterstützung kam mit Docker 20.10. Auf modernen Distributionen wie Ubuntu 22.04 oder neuer, deren Kernel und systemd cgroups v2 standardmäßig aktivieren, läuft Docker seit Version 24 in der Regel automatisch im v2-Modus.
9Kann ich die cgroup-Dateien eines Containers direkt beschreiben?
Technisch ja, mit ausreichenden Root-Rechten auf dem Host. Davon wird aber abgeraten, weil Docker den internen Zustand nicht automatisch synchronisiert, wenn Werte extern verändert werden. Änderungen sollten immer über docker update oder Docker-Flags erfolgen.
10Warum zeigt cat /sys/fs/cgroup/.../cpu.max manchmal max an?
Das Wort max anstelle einer Zahl bedeutet, dass für diese cgroup kein CPU-Limit gesetzt ist. Der Prozess kann in diesem Fall so viel CPU-Zeit nutzen, wie der Scheduler und die physisch verfügbaren Kerne hergeben.