cgroups v2 Controller im Detail: CPU, Memory und I/O gezielt begrenzen
AI generated
$
/etc
Linux
cgroups v2 Controller im Detail
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.

11 Min. Lesezeit Linux Kernel cgroups

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

11. FAQ: cgroups v2 Controller

1Was ist der Hauptunterschied zwischen cgroups v1 und v2?
cgroups v1 nutzte getrennte Hierarchien pro Controller, wodurch ein Prozess in mehreren unabhaengigen Baeumen gleichzeitig stecken konnte. cgroups v2 fuehrt eine einzige, einheitliche Hierarchie ein, in der jeder Prozess genau einem Knoten zugeordnet ist.
2Wie setze ich ein CPU Limit für einen laufenden systemd Service?
Am einfachsten über systemctl set-property servicename.service CPUQuota=50%, was das Limit sofort live anwendet, ohne den Dienst neu starten zu müssen. Für dauerhafte Konfiguration gehoert die Direktive in die Unit Datei selbst.
3Was bedeutet memory.high im Unterschied zu memory.max?
memory.high ist eine weiche Grenze, bei deren Ueberschreitung der Kernel den Prozess aktiv drosselt und Speicher reclaimed, ohne ihn sofort zu beenden. memory.max ist die harte Grenze, deren Ueberschreitung einen Out Of Memory Kill innerhalb der Cgroup ausloest.
4Wofuer wird memory.low konkret gebraucht?
memory.low schuetzt eine Mindestmenge Speicher vor Reclaim, solange kein systemweiter Speicherdruck besteht. Das verhindert, dass wichtige Prozesse bei allgemeinem Speicherdruck auf dem Host unnoetig ausgebremst werden, bevor sie ihr eigentliches Limit erreicht haben.
5Wie finde ich heraus, ob ein Prozess durch CPU Quota gedrosselt wird?
Der Wert nr_throttled in cpu.stat zählt, wie oft ein Prozess durch die gesetzte Quota gedrosselt wurde. Ein kontinuierlich steigender Wert bei ansonsten unauffaelliger durchschnittlicher CPU Auslastung deutet auf ein zu knapp bemessenes Limit hin.
6Was ist Delegation im Kontext von cgroups v2?
Delegation erlaubt es einer verschachtelten Cgroup, etwa der einer Container Runtime, selbst Kontrolle über bestimmte Controller innerhalb ihres eigenen Teilbaums auszuueben. Ohne aktivierte Delegation kann eine Runtime wie Docker keine eigenen Limits für einzelne Container setzen.
7Wie haengen Docker --memory und cgroups v2 memory.max zusammen?
Docker übersetzt das --memory Flag direkt in den memory.max Wert der für den Container angelegten Cgroup. Es handelt sich technisch um denselben Mechanismus, Docker liefert lediglich eine komfortablere CLI Syntax darueber.
8Kann ich I/O Limits pro Datentraeger unterschiedlich setzen?
Ja, io.max adressiert jedes Blockgeraet einzeln über dessen Major und Minor Nummer, sodass sich für schnelle NVMe und langsamere Netzwerkspeicher unterschiedliche Bandbreiten und IOPS Grenzen definieren lassen.
9Was passiert, wenn ich systemd-run ohne dauerhafte Unit Datei nutze?
Der Befehl läuft als transiente Unit mit den angegebenen Ressourcenlimits, verschwindet aber nach Beendigung des Prozesses wieder vollstaendig. Für wiederkehrende Limits ist eine reguelaere Unit Datei die passendere, dauerhafte Lösung.
10Wo finde ich die tatsaechlichen Cgroup Werte eines Kubernetes Pods?
Ueblicherweise unter /sys/fs/cgroup/kubepods.slice/, wobei sich der exakte Pfad über den Pod und Container Namen im Kubernetes Cgroup Treiber ableiten lässt. Ein direkter Blick auf memory.current und memory.events liefert oft schneller Klarheit als Kubernetes eigene Debugging Werkzeuge.