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.
Inhaltsverzeichnis
- 1. Was cgroups sind und warum Docker sie braucht
- 2. Die Unified Hierarchy in cgroups v2
- 3. cpu.max: Wie CPU-Limits kodiert werden
- 4. memory.max, memory.high und memory.swap.max
- 5. io.max: Bandbreiten- und IOPS-Limits pro Gerät
- 6. Der Unterschied zu cgroups v1
- 7. Ein laufender Container live inspizieren
- 8. Was sich für Nutzer praktisch ändert
- 9. cgroups v1 und v2 im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
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-, 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.