CPU, Memory und I/O gezielt begrenzen
Wer Container Ressourcenlimits wie Docker --memory oder Kubernetes resources.limits nur als CLI Flag kennt, versteht selten, was darunter tatsächlich passiert. Beide Mechanismen sind lediglich dünne Wrapper um die cgroups v2 Controller cpu, memory und io, deren direkte Bedienung über systemd deutlich mehr Kontrolle bietet als jedes Container Tool an der Oberfläche zeigt.
Inhaltsverzeichnis
- 1. Die Unified Hierarchy als Fundament von cgroups v2
- 2. Der CPU-Controller: Quota, Weight und Bursting
- 3. Der Memory-Controller: von Low bis OOM
- 4. Der I/O-Controller: Bandbreite und IOPS pro Device
- 5. Limits ad hoc über systemd-run setzen
- 6. Dauerhafte Limits über systemd-Unit-Direktiven
- 7. Delegation: Verschachtelte Cgroups für Container Runtimes
- 8. Zusammenhang mit Docker- und Kubernetes-Resource-Limits
- 9. Troubleshooting: Limits verifizieren und Engpaesse finden
- 10. Zusammenfassung
- 11. FAQ
1. Die Unified Hierarchy als Fundament von cgroups v2
cgroups v1 organisierte jeden Controller in einer eigenen, unabhängigen Hierarchie, was dazu führte, dass ein Prozess gleichzeitig in unterschiedlichen Baumstrukturen für CPU, Memory und Blkio stecken konnte, mit entsprechend komplexer und fehleranfälliger Zuordnung. cgroups v2 räumt mit dieser Fragmentierung auf und führt eine einzige, einheitliche Hierarchie ein, in der jeder Prozess genau einem Knoten zugeordnet ist und alle relevanten Controller innerhalb desselben Baums aktiviert werden.
Diese Unified Hierarchy wird üblicherweise unter /sys/fs/cgroup/ gemountet und von systemd als Init System automatisch verwaltet, das für jeden Service, jeden Scope und jede Slice Unit eine eigene Cgroup anlegt. Für Administratoren bedeutet das: Ressourcenlimits lassen sich heute größtenteils direkt über systemd Unit Direktiven konfigurieren, ohne die Cgroup Dateisystemstruktur unter /sys/fs/cgroup/ manuell anzufassen, auch wenn das darunterliegende Modell weiterhin einfache virtuelle Dateien für jeden Controller bereitstellt.
# Pruefen, ob cgroups v2 als Unified Hierarchy aktiv ist
mount | grep cgroup2
# Verfuegbare Controller für die Root Cgroup anzeigen
cat /sys/fs/cgroup/cgroup.controllers
2. Der CPU-Controller: Quota, Weight und Bursting
Der cpu Controller regelt CPU Zuteilung über zwei komplementäre Mechanismen: cpu.max setzt eine harte Obergrenze in Form eines Quota und Period Paars, üblicherweise als Anzahl Mikrosekunden CPU Zeit pro festgelegter Periode, während cpu.weight die relative Priorität bei Konkurrenz um verfügbare CPU Zeit zwischen mehreren Cgroups auf derselben Ebene steuert, standardmäßig im Bereich von 1 bis 10000 mit einem Default von 100.
Diese beiden Mechanismen ergänzen sich: Ein Quota Limit begrenzt absolut, wie viel CPU Zeit eine Cgroup maximal beanspruchen darf, unabhängig davon, ob die CPU sonst im Leerlauf wäre, während das Weight nur greift, wenn tatsächlich Konkurrenz um CPU Zeit besteht. Seit Kernel 5.14 unterstützt cpu.max.burst zusätzlich kurzfristiges Überschreiten der Quota innerhalb eines vordefinierten Burst Budgets, was für Workloads mit gelegentlichen, kurzen Lastspitzen wie PHP FPM Prozessen bei Cache Misses realistischer ist als ein hartes Limit ohne jede Toleranz.
# CPU Quota auf 50 Prozent einer CPU begrenzen (50000 von 100000 Mikrosekunden)
echo "50000 100000" > /sys/fs/cgroup/shop-php.slice/cpu.max
# Relative Prioritaet gegenueber anderen Cgroups auf gleicher Ebene setzen
echo 200 > /sys/fs/cgroup/shop-php.slice/cpu.weight
3. Der Memory-Controller: von Low bis OOM
Der memory Controller kennt vier gestaffelte Grenzwerte statt eines einzelnen Limits: memory.low definiert eine Schutzschwelle, unterhalb derer Speicher nur bei systemweitem Druck reclaimed wird, memory.high setzt eine weiche Grenze, bei deren Überschreitung der Kernel den Prozess aktiv drosselt und Reclaim forciert, memory.max ist die harte Grenze, deren Überschreitung einen Out Of Memory Kill innerhalb der Cgroup auslöst, und memory.min garantiert eine Mindestmenge, die selbst unter systemweitem Speicherdruck nicht reclaimed wird.
Diese Abstufung erlaubt deutlich feinere Steuerung als das binäre Limit von cgroups v1: Ein memory.high Wert knapp unterhalb von memory.max gibt einem Prozess die Chance, durch aktiven Reclaim und Drosselung selbst gegenzusteuern, bevor der harte OOM Killer zuschlägt, was bei Datenbank oder PHP Prozessen mit internem Cache Management den entscheidenden Unterschied zwischen einer kontrollierten Verlangsamung und einem abrupten Absturz ausmacht.
# Harte Memory Obergrenze und weiche Drosselungsgrenze setzen
echo "900M" > /sys/fs/cgroup/shop-php.slice/memory.max
echo "750M" > /sys/fs/cgroup/shop-php.slice/memory.high
# Aktuellen Speicherverbrauch und OOM Ereignisse pruefen
cat /sys/fs/cgroup/shop-php.slice/memory.current
cat /sys/fs/cgroup/shop-php.slice/memory.events
4. Der I/O-Controller: Bandbreite und IOPS pro Device
Der io Controller begrenzt Lese und Schreibdurchsatz sowie IOPS pro Blockgerät über io.max, wobei jedes Device einzeln über seine Major und Minor Nummer adressiert wird, weil unterschiedliche physische oder virtuelle Datenträger unterschiedliche Leistungsprofile haben und ein pauschales Limit über alle Geräte hinweg wenig aussagekräftig wäre. Konfigurierbar sind separate Limits für rbps und wbps (Lese und Schreib Bandbreite in Byte pro Sekunde) sowie riops und wiops.
Für die relative Priorisierung zwischen mehreren Cgroups bei I/O Konkurrenz existiert zusätzlich io.weight, analog zum CPU Weight, das nur bei tatsächlicher I/O Sättigung greift. In der Praxis ist der I/O Controller besonders relevant für Datenbank Workloads auf gemeinsam genutztem Storage, wo ein einzelner Container mit exzessivem Schreibverhalten sonst die I/O Latenz für alle anderen Cgroups auf demselben physischen Datenträger in die Höhe treiben kann.
# Device Major:Minor Nummer ermitteln
lsblk -d -o NAME,MAJ:MIN
# Lese und Schreibbandbreite für Device 8:0 begrenzen
echo "8:0 rbps=50000000 wbps=20000000" > /sys/fs/cgroup/shop-db.slice/io.max
5. Limits ad hoc über systemd-run setzen
Statt die virtuellen Cgroup Dateien direkt zu editieren, was fehleranfällig ist und bei einem Neustart des Prozesses verloren geht, bietet systemd-run einen deutlich komfortableren Weg, um einen Befehl direkt mit definierten Ressourcenlimits als transiente systemd Unit zu starten. Die relevanten Properties wie MemoryMax, CPUQuota und IOWriteBandwidthMax mappen dabei eins zu eins auf die zugrunde liegenden Cgroup v2 Controller Dateien, werden von systemd aber in menschenlesbaren Einheiten statt roher Byte oder Mikrosekunden Werte angegeben.
Dieser Ansatz eignet sich hervorragend, um schnell zu testen, wie sich ein Prozess unter einem bestimmten Limit verhält, etwa ein Datenbank Import unter reduziertem Memory Budget, ohne gleich eine dauerhafte systemd Unit Datei anlegen zu müssen. Für produktive, dauerhafte Limits gehören dieselben Properties dagegen in eine reguläre Unit Datei unter /etc/systemd/system/.
# Befehl transient mit CPU und Memory Limit ausführen
systemd-run --scope -p CPUQuota=50% -p MemoryMax=512M \
mysqldump shop_db > /srv/backup/shop_db.sql
# I/O Bandbreite für einen einzelnen Befehl begrenzen
systemd-run --scope -p IOWriteBandwidthMax="/dev/sda 10M" \
rsync -a /srv/data/ /srv/backup/
6. Dauerhafte Limits über systemd-Unit-Direktiven
Für langlebige Dienste gehören Ressourcenlimits direkt in die systemd Unit Datei, entweder im [Service] Abschnitt einer regulären Service Unit oder zentral in einer Slice Unit, an die mehrere zusammengehörige Services gebunden werden. Slice Units bilden dabei eine hierarchische Gruppierung: Ein Limit auf Slice Ebene begrenzt automatisch die Summe aller darin laufenden Services, was sich hervorragend eignet, um etwa allen PHP FPM Workern eines Kunden gemeinsam ein Gesamtbudget zuzuweisen, unabhängig davon, wie viele einzelne Worker Prozesse gerade aktiv sind.
Änderungen an Unit Direktiven erfordern nach dem Bearbeiten einen systemctl daemon-reload gefolgt von einem Neustart oder, bei laufenden Diensten, lassen sich viele Properties auch live über systemctl set-property anpassen, ohne den Dienst selbst neu zu starten, weil systemd die Änderung direkt in die entsprechende Cgroup Datei durchreicht.
# /etc/systemd/system/shop-php.slice
[Unit]
Description=Ressourcen Slice für alle PHP FPM Worker des Shops
[Slice]
CPUQuota=200%
MemoryMax=2G
IOWeight=100
7. Delegation: Verschachtelte Cgroups für Container Runtimes
Container Runtimes wie Docker oder Podman erzeugen für jeden Container eine eigene, verschachtelte Cgroup unterhalb der systemd verwalteten Hierarchie, was voraussetzt, dass systemd diesem Teilbaum explizit Kontrolle über bestimmte Controller überlässt, ein Vorgang, den systemd als Delegation bezeichnet. Ohne aktivierte Delegation kann die Container Runtime innerhalb ihres eigenen Cgroup Zweigs keine weiteren Limits für einzelne Container setzen, weil systemd die Controller Kontrolle standardmäßig auf oberster Ebene behält.
Bei modernen systemd Versionen ist Delegation für den cpu, memory, io und pids Controller innerhalb einer als Delegate=yes markierten Unit standardmäßig für Docker und Podman bereits korrekt konfiguriert, sofern die Runtime über systemd als cgroup Treiber läuft. Wer eigene, benutzerdefinierte Slice Units für Container Gruppen anlegt, muss diese Delegation explizit setzen, sonst schlagen Ressourcenlimits, die die Runtime selbst zu setzen versucht, mit einer Permission Denied Fehlermeldung fehl.
# /etc/systemd/system/shop-containers.slice
[Slice]
CPUQuota=400%
MemoryMax=4G
[Unit]
# Erlaubt der Container Runtime, innerhalb dieser Slice eigene Limits zu setzen
8. Zusammenhang mit Docker- und Kubernetes-Resource-Limits
Docker CLI Flags wie --cpus=1.5 und --memory=512m übersetzen sich intern eins zu eins in die cgroups v2 Properties cpu.max und memory.max für die vom Container Runtime angelegte Cgroup, mit dem Unterschied, dass Docker die menschenlesbare Syntax abstrahiert und Kubernetes darüber noch eine weitere Abstraktionsebene mit resources.requests und resources.limits legt. Ein Kubernetes limits.memory: 512Mi landet letztlich exakt als derselbe memory.max Wert in der zugrunde liegenden Pod Cgroup, den man auch manuell über systemd setzen könnte.
Der praktische Nutzen, die zugrunde liegende Cgroup Ebene zu verstehen, zeigt sich beim Debugging: Wenn ein Kubernetes Pod trotz vermeintlich ausreichendem Memory Limit unerwartet OOM gekillt wird, hilft ein direkter Blick auf memory.current und memory.events der entsprechenden Cgroup unter /sys/fs/cgroup/kubepods.slice/ oft schneller weiter als jedes Kubernetes eigene Debugging Tool, weil hier die tatsächlichen Kernel Zahlen ohne jede Abstraktionsschicht sichtbar sind.
# Cgroup Pfad eines laufenden Docker Containers ermitteln
docker inspect --format '{{.Id}}' shop-php
cat /sys/fs/cgroup/system.slice/docker-*.scope/memory.max
# Aktuellen Speicherverbrauch und OOM Zaehler direkt am Kernel pruefen
cat /sys/fs/cgroup/system.slice/docker-*.scope/memory.events
9. Troubleshooting: Limits verifizieren und Engpaesse finden
Bei unerwartetem Verhalten lohnt sich zuerst ein Blick in cpu.stat, das unter anderem die kumulierte Throttled Time ausweist, also wie lange ein Prozess durch die CPU Quota tatsächlich gedrosselt wurde. Ein hoher Wert bei nr_throttled ist ein klares Signal, dass das gesetzte CPU Limit zu knapp bemessen ist und die Anwendung regelmäßig an ihre Grenze stößt, selbst wenn die durchschnittliche CPU Auslastung unauffällig wirkt.
Für Memory Probleme liefert memory.events die entscheidenden Zähler: high zeigt, wie oft die weiche Grenze überschritten wurde, max wie oft die harte Grenze erreicht wurde, und oom_kill die tatsächliche Anzahl ausgelöster Out Of Memory Kills innerhalb der Cgroup. Diese Zähler sind kumulativ seit Erstellung der Cgroup und liefern damit eine verlässlichere historische Sicht als ein einzelner Snapshot mit free oder top, der nur den aktuellen Moment abbildet.
| Controller | Wichtigste Dateien | Harte Grenze | Weiche Grenze / Prioritaet |
|---|---|---|---|
| cpu | cpu.max, cpu.weight, cpu.stat | cpu.max (Quota/Period) | cpu.weight für relative Prioritaet |
| memory | memory.max, memory.high, memory.min, memory.events | memory.max (OOM Kill) | memory.high (Drosselung), memory.low (Schutz) |
| io | io.max, io.weight, io.stat | io.max (rbps/wbps/riops/wiops) | io.weight für relative Prioritaet |
| pids | pids.max, pids.current | pids.max (Prozessanzahl) | keine, nur harte Grenze |
Mironsoft
Server-Administration, Docker-Hosts und Performance-Tuning
Linux-Server, die niemand im Team richtig versteht?
Wir übernehmen Setup, Absicherung und Performance-Tuning von Linux-Servern und Docker-Hosts für Magento-Deployments, dokumentiert und nachvollziehbar statt gewachsen und unklar.
Server-Audit
Bestehende Server-Konfiguration auf Sicherheitslücken und Performance-Bremsen prüfen.
Docker-Host-Setup
Produktionsreife Docker-Umgebungen für Magento sauber aufsetzen und absichern.
Monitoring & Tuning
Ressourcenverbrauch messen und Systemd, Kernel und Dienste gezielt optimieren.
10. Zusammenfassung
cgroups v2 Controller
Fundament
Eine einheitliche Cgroup Hierarchie statt getrennter Baeume pro Controller
CPU
cpu.max für harte Quota, cpu.weight für relative Prioritaet
Memory
Vier Stufen von memory.min bis memory.max statt einem Limit
Praxisweg
systemd-run für Tests, Unit Direktiven für dauerhafte Limits