Zum Inhalt springen
← Blog

cgroup v1 ist weg. Sauber migrieren.

systemd 258 hat cgroup v1 ersatzlos entfernt – Ihre Hosts booten jetzt unified, ob das jemand angefordert hat oder nicht. Hier stehen das Audit, die Umrechnung Datei für Datei und die vier Behauptungen zu dieser Migration, die schlicht falsch sind.

·28 Min. Lesezeit
  • Linux
  • cgroups
  • systemd
  • Container

Die meisten Migrationen beginnen mit einer Entscheidung. Diese beginnt mit einer Tatsache: Im September 2025 hat systemd 258 die Unterstützung für cgroup v1 vollständig entfernt, zusammen mit dem Notausgang SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1, den Version 256 ein Jahr zuvor eingeführt hatte. Die Unified Hierarchy wird jetzt beim Booten gemountet, auf jeder Maschine mit diesem systemd oder neuer, und es gibt keinen unterstützten Weg, etwas anderes zu verlangen. Was Ihre Container-Plattform von cgroups hält, spielt dabei keine Rolle – das Betriebssystem darunter hat bereits abgestimmt.

Dreiteiliges Titelbild. Die linke Tafel, überschrieben mit "cgroup v1 – eine Hierarchie pro Controller", zeigt sechs getrennte Bäume pro Controller: memory, cpu,cpuacct, blkio, devices, net_cls und freezer, darunter die Bildunterschrift "ein Prozess sitzt in jedem einzelnen davon". Die mittlere Spalte, überschrieben mit "was seine Bedeutung ändert", nennt sechs Umrechnungspaare: memory.limit_in_bytes wird zu memory.max, cpu.cfs_quota_us plus period wird zu cpu.max, blkio.weight wird zu io.weight, memory.memsw.limit wird zu einer Grenze nur für Swap statt für Speicher plus Swap, cpu.shares 1024 wird zu cpu.weight 100, und net_cls und devices werden zu eBPF ganz ohne Controller. Die rechte Tafel, überschrieben mit "cgroup v2 – eine einzige vereinheitlichte Hierarchie", zeigt einen einzigen Baum, in dem Prozesse nur noch in den Blättern sitzen, dazu eine Leiste mit den vier Speicherstufen memory.min, low, high und max.
Die strukturelle Änderung in einem Bild: Aus einem eigenen Baum pro Controller wird ein einziger, Prozesse sind nur noch in den Blättern erlaubt, und aus einem einzelnen harten Speicherlimit werden vier Stufen mit tatsächlich unterschiedlichem Verhalten.

Was folgt, ist die Migration so, wie sie in echten Serverlandschaften abläuft: wie Sie feststellen, auf welcher Hierarchie ein Host läuft, ohne sich vom Hybrid-Layout täuschen zu lassen; eine Umrechnungstabelle Datei für Datei mit den passenden systemd-Direktiven; die beiden Übersetzungen, die das Verhalten stillschweigend ändern, wenn Sie die Zahlen einfach übernehmen; die CPU-Gewichtungsformel, die 2025 klammheimlich in den OCI-Runtimes ausgetauscht wurde; und eine ehrliche Bilanz dessen, was Docker und Kubernetes getan und was sie eben nicht getan haben – denn genau an diesem letzten Punkt ist das meiste, was geschrieben wird, falsch, und wer danach handelt, zahlt entweder mit einem Ausfall oder mit einem Jahr unnötiger Panik.

Sie haben das nicht entschieden – und genau darum geht es

Die Symptome sind unangenehm indirekt, denn nichts meldet sich als cgroup-Problem. Ein Monitoring-Agent liefert plötzlich Nullen. Das Speicherlimit eines Containers wird nicht mehr durchgesetzt. Ein Rootless-Workload, der auf der alten Kiste problemlos lief, startet nicht mehr. Ein Skript, das seit acht Jahren /sys/fs/cgroup/memory/memory.usage_in_bytes liest, protokolliert auf einmal ein File-not-found, das niemandem auffällt, weil es in ein Log schreibt, das keiner liest. All das ist dasselbe Ereignis aus verschiedenen Blickwinkeln: Die Pfade sind umgezogen, die Semantik darunter hat sich geändert, und nichts ist laut genug fehlgeschlagen, um ein Deployment zu stoppen.[sd258]

Was Sie sehenWas es meistens bedeutetWo es behandelt wird
Ein Monitoring-Agent meldet für jeden Container null Speicher oder null CPUEr liest /sys/fs/cgroup/memory/… oder cpu,cpuacct, die es unter der Unified Hierarchy nicht gibtDie Umrechnungstabelle
write error: Device or resource busy beim Hinzufügen einer PID zu einer cgroupDie No-Internal-Process-Regel: Diese cgroup delegiert bereits Controller an KinderDrei Regeln
Ein Container swappt bei identischen Limits deutlich mehr als frühermemory.memsw.limit_in_bytes wurde nach memory.swap.max kopiert, das aber nur Swap begrenztSpeicher-Semantik
Rootless Podman nimmt --memory an, setzt es aber nicht durchsystemd hat den Memory-Controller nicht an den User-Manager delegiertContainer
Container bekommen unter Last weniger CPU als früherDie lineare Umrechnung von Shares in Gewichte ergab für einen Ein-CPU-Request das Gewicht 39 gegen einen Standard von 100CPU-Gewichtung
kubelet startet nach einem Upgrade des Node-Images nicht mehrcgroup-v1-Knoten, kubelet v1.35 oder neuer, failCgroupV1 auf dem Standardwert trueKubernetes
--oom-kill-disable bleibt stillschweigend wirkungslosUnter cgroup v2 verworfen. Es gibt kein Äquivalent und es ist keines geplantContainer

Eine Gewohnheit sollten Sie vor allem anderen ablegen. Wenn ein Werkzeug von Ihnen auf einem systemd-Host direkt in /sys/fs/cgroup schreibt, wird es diese Migration nicht in einer Form überleben, die Ihnen gefällt. systemd besitzt diesen Baum und setzt seine Sicht darauf jedes Mal neu durch, wenn sich eine Unit ändert; das Delegations-Dokument des Upstream-Projekts sagt ausdrücklich, dass genau ein Schreiber pro Teilbaum eine Regel ist und keine Empfehlung. Unter v1 kam man mit einem Verstoß meistens noch durch. Unter v2, mit seiner strikten Delegation von oben nach unten, nicht mehr.[sddeleg]

Auf welcher Hierarchie läuft dieser Host tatsächlich?

Klären Sie zuerst die Faktenlage, denn die üblichen Folklore-Kommandos lügen auf eine sehr spezifische und sehr konsistente Weise. Die von Kubernetes dokumentierte Prüfung ist die richtige, und es ist ein einziger Befehl: Fragen Sie den Dateisystemtyp von /sys/fs/cgroup ab. Unter der Unified Hierarchy ist dieser Mountpoint ein cgroup2-Dateisystem, also meldet stat den Wert cgroup2fs. Unter v1 – und, entscheidend, auch unter dem alten Hybrid-Layout – ist es ein tmpfs, in dem die Controller-Verzeichnisse eingehängt sind.[k8scg]

# The only check that cannot lie. /sys/fs/cgroup is a tmpfs under v1 and under
# the old hybrid layout, and a cgroup2 filesystem under the unified hierarchy.
stat -fc %T /sys/fs/cgroup/
# cgroup2fs   -> unified, cgroup v2 only
# tmpfs       -> cgroup v1, or hybrid: v2 mounted under a v1 tmpfs

# Why `mount | grep cgroup2` is not enough: hybrid mounts a cgroup2 hierarchy
# too, at /sys/fs/cgroup/unified, with no controllers attached to it. Grepping
# for the string finds it and tells you the opposite of the truth.
mount | grep -E '^cgroup' | sed 's/ (.*//'
# cgroup2 on /sys/fs/cgroup type cgroup2          <- unified: good
# cgroup2 on /sys/fs/cgroup/unified type cgroup2  <- hybrid:  not good

# What the kernel will actually let you control here. Under the unified
# hierarchy this file exists at the top of the tree and lists the controllers.
# Under hybrid it is not here at all - it is one level down, at
# /sys/fs/cgroup/unified/cgroup.controllers, and it is empty. Its absence from
# the top level is the clearest single tell that you are not on v2.
cat /sys/fs/cgroup/cgroup.controllers 2>/dev/null
# cpuset cpu io memory hugetlb pids rdma misc

# Where a given process ended up. Under v2 there is exactly one line and it
# starts with `0::`. More than one line means controllers are still split.
cat /proc/self/cgroup
# 0::/user.slice/user-1000.slice/session-3.scope

# And the two userspace pieces that have to agree with the kernel:
systemctl --version | head -1
docker info --format 'driver={{.CgroupDriver}} version={{.CgroupVersion}}' 2>/dev/null

Der Hybrid-Fall ist der Grund, warum mount | grep cgroup2 nicht nur unvollständig, sondern aktiv irreführend ist: Hybrid mountet unter /sys/fs/cgroup/unified eine echte cgroup2-Hierarchie, an die kein einziger Controller angehängt ist. Der grep trifft, Sie schließen daraus auf v2, und jedes Limit, das Sie anschließend setzen, landet auf einem v1-Controller. Das zweite Erkennungsmerkmal ist /proc/self/cgroup: Unter v2 enthält die Datei genau eine Zeile, die mit 0:: beginnt, unter v1 oder Hybrid eine Zeile pro Controller. Fahren Sie das Audit unten über die gesamte Flotte, bevor Sie irgendetwas planen – meiner Erfahrung nach ist das Ergebnis nie einheitlich.[cgman]

#!/usr/bin/env bash
# Fleet audit. Read-only: it changes nothing. Run it before you plan anything,
# because the answer is almost never uniform across a real estate of servers.
set -u

host=$(hostname -s)
ver=$(stat -fc %T /sys/fs/cgroup/ 2>/dev/null)
case "$ver" in
  cgroup2fs) mode=unified ;;
  tmpfs)     [ -d /sys/fs/cgroup/unified ] && mode=hybrid || mode=legacy ;;
  *)         mode=unknown ;;
esac

kernel=$(uname -r)
sd=$(systemctl --version 2>/dev/null | awk 'NR==1{print $2}')
printf '%-16s mode=%-8s kernel=%-14s systemd=%s\n' "$host" "$mode" "$kernel" "$sd"

# --- the things that will break, rather than the things that will complain ---

# a) v1-only controllers with no v2 equivalent. If anything you run writes to
#    these, it needs an eBPF replacement, not a path change.
for c in net_cls net_prio devices; do
  [ -d "/sys/fs/cgroup/$c" ] && echo "  uses v1-only controller: $c"
done

# b) anything with a hardcoded v1 path. This is the single most common cause of
#    a migration failing three weeks later in an agent nobody remembered.
grep -rIl --exclude-dir=.git \
     -e '/sys/fs/cgroup/memory/' \
     -e '/sys/fs/cgroup/cpu,cpuacct/' \
     -e 'memory.limit_in_bytes' \
     -e 'cpu.cfs_quota_us' \
     /etc /opt /usr/local 2>/dev/null | sed 's/^/  hardcoded v1 path: /'

# c) kernel command line pinning the old hierarchy. On systemd 258 and later
#    this parameter no longer does anything, which is its own kind of trap:
#    the host silently boots unified and the runbook still says otherwise.
grep -o 'systemd\.unified_cgroup_hierarchy=[01]' /proc/cmdline \
  | sed 's/^/  kernel cmdline: /'
grep -o 'SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1' /proc/cmdline \
  | sed 's/^/  legacy force flag (removed in systemd 258): /'

# d) container runtimes and their drivers, which have to match the kernel
command -v docker  >/dev/null && docker info 2>/dev/null \
  | grep -E 'Cgroup (Driver|Version)' | sed 's/^/ /'
command -v podman  >/dev/null && podman info --format \
  '  podman cgroupVersion={{.Host.CgroupsVersion}} manager={{.Host.CgroupManager}}' 2>/dev/null
[ -f /var/lib/kubelet/config.yaml ] && \
  grep -E '^(cgroupDriver|failCgroupV1):' /var/lib/kubelet/config.yaml | sed 's/^/  kubelet /'

Führen Sie das auf jedem Host aus, nicht auf einem repräsentativen. Die Maschinen, die 2026 noch auf v1 laufen, sind fast definitionsgemäß die Maschinen, die niemand angefasst hat: die Appliance, der Build-Agent, den 2019 jemand von Hand hochgezogen hat, der Datenbankknoten, der bewusst aus dem Konfigurationsmanagement ausgenommen ist. Genau dort wartet ein fest verdrahteter Pfad /sys/fs/cgroup/memory/, und genau dort wird niemand bemerken, dass er kaputtgegangen ist.

Wer hat was entfernt – und wer hat es nur angekündigt

Vier Projekte sind beteiligt, und sie folgen völlig unterschiedlichen Zeitplänen – das ist die mit Abstand größte Verwirrungsquelle bei dieser Migration. systemd war zuerst dran und hat am härtesten zugeschlagen. Version 256 hörte im Juni 2024 auf, cgroup v1 standardmäßig zu booten, ließ aber einen Notausgang offen. Version 258 hat im September 2025 den Code entfernt; die Release Notes sind eindeutig: Die Unterstützung für cgroup v1 (die Hierarchien „legacy" und „hybrid") wurde entfernt, und cgroup v2 wird beim Systemstart immer gemountet. Dieses Release hat außerdem die minimale Kernel-Basis auf 5.4 angehoben, empfohlen wird 5.7.[sdnews][sd256]

ProjektWas tatsächlich passiert istWannWas das für Sie heißt
systemd 256Bootet cgroup v1 nicht mehr standardmäßig; SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 ergänztJuni 2024257 wertet das Flag weiterhin aus – 257 ist also das letzte Release, in dem Sie v1 anfordern und auch bekommen konnten
systemd 258cgroup-v1-Unterstützung vollständig entfernt, Notausgang inklusive; Kernel-Basis auf 5.4 angehoben, 5.7 empfohlenSeptember 2025Das ist die Deadline. Unified ist der einzige Modus, den der Code hat
Kubernetes 1.35cgroup v1 als veraltet markiert; kubelet startet auf einem v1-Knoten standardmäßig nichtDezember 2025Mit failCgroupV1: false überschreibbar. Keine Entfernung
Kubernetes 1.38+Frühestes Release, in dem KEP-5573 den Code entferntNicht terminiertSie haben mehr Zeit, als die Schlagzeilen nahelegen
Docker Engine 29.0cgroup v1 als veraltet markiert; keine Entfernungsversion festgelegtNovember 2025Nur Deprecation. Die Dokumentation hält fest, dass die Unterstützung bis Mai 2029 weiterläuft
runc / crunLineare Umrechnung von Shares in Gewichte durch eine logarithmische ersetztIm Lauf von 2025Ändert die CPU-Priorität auf bereits migrierten Knoten

Die anderen drei haben nicht getan, was Sie vermutlich gelesen haben, und dieser Unterschied ist für die Planung entscheidend. Kubernetes hat cgroup v1 in v1.35 als veraltet markiert – das kubelet startet auf einem v1-Knoten standardmäßig nicht mehr, aber dieser Standard ist ein Konfigurationsfeld, das Sie umlegen können, und KEP-5573 sagt wörtlich, dass die Entfernung des Codes nicht vor 1.38 erfolgen wird – ohne dass daran ein Datum hängt. Docker hat cgroup v1 in Engine v29.0 als veraltet markiert, erschienen im November 2025, ganz ohne festgelegte Entfernungsversion; Dockers eigene Deprecation-Seite hält fest, dass die Unterstützung bis Mai 2029 weiterläuft – dann erreichen die Enterprise-Distributionen, die sie noch brauchen, ihr Supportende. Also: Das Betriebssystem hat es bereits entfernt, die Orchestrierer haben es nur angekündigt. Planen Sie gegen das Betriebssystem.[kep5573][dockdep]

DistributionStandard-HierarchieAnmerkung
Fedora 31 und neuerUnified (v2)Erste breit genutzte Distribution mit der Umstellung; Fedora 43 erbt die Entfernung aus systemd 258
Debian 11 und neuerUnified (v2)Debian 13 liefert systemd 257 aus und ist damit das letzte mit überhaupt einem Legacy-Pfad
Ubuntu 21.10 und neuerUnified (v2)22.04 LTS und neuer sind die Versionen, die bei Ihnen vermutlich noch laufen
RHEL 9 und neuerUnified (v2)RHEL 9.4 hat v1 formal als veraltet markiert; RHEL 10 bootet v1 gar nicht mehr
SLES 15 SP6 und neuerUnified (v2)SP3 bis SP5 waren standardmäßig hybrid – genau das Layout, das grep täuscht
Alles Älterev1 oder hybridUnd inzwischen ohnehin ohne Support. Die cgroup-Frage ist da nicht die dringende

Damit ist die tatsächliche Deadline auf einer beliebigen Maschine die systemd-Version, die ihre Distribution ausliefert – und nicht die Roadmap irgendeiner Container-Plattform. Die meisten Flotten laufen längst unified, und zwar seit Jahren, ohne dass es jemandem aufgefallen wäre: Fedora seit 31, Debian seit 11, Ubuntu seit 21.10, RHEL seit 9. Die Arbeit konzentriert sich auf das, was übrig bleibt – und auf das Tooling, das nach wie vor v1-Pfade voraussetzt, egal was der Host gerade tut.[rhel10][moby51111]

Drei Regeln, an denen handgebaute Layouts zerbrechen

Drei strukturelle Regeln unterscheiden v2 von v1, und jede einzelne bringt ein Layout zu Fall, das unter v1 von Hand gebaut wurde. Die erste ist die Unified Hierarchy selbst: Ein Prozess hat genau eine Position in genau einem Baum, und jeder Controller liest dieselbe Position – deshalb ist /proc/self/cgroup von einem Dutzend Zeilen auf eine geschrumpft. Die zweite ist die Delegation von oben nach unten: Ein Controller existiert in einer Kind-cgroup nur dann, wenn der Elternknoten ihn über cgroup.subtree_control ausdrücklich weiterreicht. Die dritte ist die, die wirklich weh tut.[kdoc][knoint]

# DEMONSTRATION ONLY. This writes into the cgroup tree by hand, which is the
# exact thing the rest of this article tells you not to do on a systemd host.
# Read it to understand the rules, then set limits through systemd.

# Rule 1 - one tree. Under v1 a process had a position in each controller's
# hierarchy independently, which is why /proc/PID/cgroup had a dozen lines.
# Under v2 it has one, and every controller reads the same position.
cd /sys/fs/cgroup
mkdir -p demo/worker demo/batch

# Rule 2 - a controller only exists in a child if the parent hands it down.
# cgroup.controllers is what you HAVE; cgroup.subtree_control is what you GIVE.
cat demo/cgroup.controllers        # what the parent has delegated so far
echo '+cpu +memory +io' > cgroup.subtree_control   # root delegates to demo
cat demo/cgroup.controllers        # cpu io memory
echo '+cpu +memory' > demo/cgroup.subtree_control  # demo delegates to its kids
ls demo/worker/ | grep -E '^(cpu|memory)\.'        # the knobs now exist

# Rule 3 - no internal processes. A cgroup may hold processes, or hand
# resources to children, never both. This is the rule that breaks hand-built
# v1 layouts, and it fails at write() time with a very unhelpful error.
echo $$ > demo/cgroup.procs
# bash: echo: write error: Device or resource busy
#
# ... because demo already has subtree_control set. Processes live on leaves:
echo $$ > demo/worker/cgroup.procs   # fine

# The root cgroup is exempt from rule 3, which is why the mistake survives
# testing at the top level and only shows up one directory down.

Die No-Internal-Process-Regel besagt, dass eine cgroup unterhalb der Wurzel entweder Prozesse enthalten oder Ressourcen an Kinder verteilen darf – aber niemals beides. Sie beseitigt eine echte Mehrdeutigkeit aus v1, wo die eigenen Prozesse eines Elternknotens ohne definierte Regel gegen dessen Kinder konkurrierten. In der Praxis heißt das: Ein v1-Layout mit Limits auf jeder Ebene des Baums lässt sich nicht eins zu eins übertragen. Sie müssen die Prozesse in die Blätter schieben und die inneren Knoten leer halten. Und die Regel schlägt auf die denkbar unfreundlichste Art zu – ein nacktes write error: Device or resource busy beim echo in cgroup.procs, ohne jeden Hinweis darauf, welche der beiden Regeln Sie verletzt haben. Die Wurzel-cgroup ist ausgenommen, und genau deshalb überlebt der Fehler einen schnellen Test auf oberster Ebene.[kdeleg]

Die Umrechnungstabelle, Datei für Datei

Hier ist die Zuordnung, jeweils mit der systemd-Direktive daneben – denn auf einem systemd-Host ist die Direktive das, was Sie tatsächlich setzen sollten. Es ändern sich mehr Namen als Werte, aber drei Zeilen ändern die Bedeutung statt nur der Schreibweise, und die sind markiert. Beachten Sie insbesondere, dass cpu.max zwei v1-Dateien in einer Datei mit zwei Werten zusammenfasst und dass die Skala von cpu.weight nicht die Skala von cpu.shares ist – allein die Standardwerte unterscheiden sich um den Faktor zehn.[sdresctl]

cgroup v1cgroup v2systemd-DirektiveAnmerkung
memory.limit_in_bytesmemory.maxMemoryMax=Umbenennung. Gleiche Bedeutung: hartes Limit, OOM-Kill bei Überschreitung
memory.soft_limit_in_bytesmemory.highMemoryHigh=Verbesserung. Das weiche Limit wurde meist ignoriert; memory.high drosselt wirklich
memory.low / memory.minMemoryLow= / MemoryMin=Neu. Schutzuntergrenzen ohne Entsprechung in v1
memory.memsw.limit_in_bytesmemory.swap.maxMemorySwapMax=Andere Bedeutung. memsw war Speicher+Swap; das hier ist Swap allein
memory.usage_in_bytesmemory.currentUmbenennung
memory.failcntmemory.eventsBesser: getrennte Zähler für low, high, max, oom und oom_kill
cpu.sharescpu.weightCPUWeight=Andere Skala. Aus Standard 1024 wird Standard 100; siehe Umrechnung
cpu.cfs_quota_us + cpu.cfs_period_uscpu.maxCPUQuota= + CPUQuotaPeriodSec=Aus zwei Dateien wird eine, geschrieben als "$MAX $PERIOD". CPUQuota= setzt nur die Quota-Hälfte
cpuacct.usagecpu.statEnthält jetzt nr_throttled und throttled_usec
blkio.weightio.weightIOWeight=Umbenennung, aber die Buchhaltung darunter stimmt endlich
blkio.throttle.*_bps_deviceio.maxIOReadBandwidthMax= usw.Eine Datei mit verschachtelten Schlüsseln statt vier; erfasst jetzt gepufferte Schreibvorgänge
pids.maxpids.maxTasksMax=Unverändert
freezer.statecgroup.freezeUmbenennung; schreiben Sie 1 oder 0
devices.allow / devices.deny— (eBPF)DeviceAllow=Kein Controller. Ersetzt durch BPF_PROG_TYPE_CGROUP_DEVICE
net_cls.classid / net_prio.*— (eBPF)Kein Controller und keine Ersatzdatei. eBPF auf cgroup-Pfaden verwenden
cpu.pressure / memory.pressure / io.pressureNeu. PSI: der Grund zu migrieren statt ein Preis der Migration

Drei v1-Controller haben überhaupt kein Gegenstück in v2, und das ist die Zeile, die aus einer mechanischen Migration eine Engineering-Aufgabe macht. net_cls und net_prio wurden ersatzlos entfernt statt neu implementiert; Traffic-Klassifizierung und Shaping pro cgroup laufen heute über eBPF-Programme, die an cgroup-v2-Pfade gebunden werden, mit passender Unterstützung in iptables und nftables. Dem devices-Controller ist es genauso ergangen: Statt einer Whitelist-Datei bekommt ein eBPF-Programm vom Typ BPF_PROG_TYPE_CGROUP_DEVICE Major- und Minor-Nummer, den Gerätetyp und die Zugriffsart und antwortet mit „erlaubt" oder -EPERM. Wenn Ihre Plattform eines davon direkt genutzt hat, planen Sie echte Arbeit ein und nicht eine Pfadersetzung.[cgman][bpfdev]

Beim Speicher hat sich die Semantik wirklich geändert

Beim Speicher richtet eine schludrige Migration ihren stillen Schaden an, denn die Zahlen passen weiterhin und das Verhalten ändert sich trotzdem. Unter v1 hatten Sie ein hartes Limit und ein weiches, das die meisten Kernel praktisch ignoriert haben. Unter v2 haben Sie vier Stufen, und nur eine davon kann etwas abschießen: memory.max ist das harte Limit und löst einen OOM-Kill innerhalb der cgroup aus; memory.high ist eine Drossel, die die cgroup unter starken Reclaim setzt und – in den Worten der Kernel-Dokumentation – niemals den OOM-Killer aufruft; memory.low ist ein Best-Effort-Schutz; memory.min ist harter Schutz, der überhaupt nie zurückgefordert wird.[kmem][kv1mem]

# --- cgroup v1: two numbers, and the second one is not what people think ---
#   memory.limit_in_bytes        = 2G   -> hard limit on memory
#   memory.memsw.limit_in_bytes  = 3G   -> hard limit on memory PLUS swap
#                                          (so: 2G RAM + up to 1G of swap)
#   memory.soft_limit_in_bytes   = 1G   -> best-effort, and widely ignored
#
# --- cgroup v2: four memory tiers, plus a separate swap cap ---------------
cd /sys/fs/cgroup/demo/worker

echo 2G       > memory.max        # hard limit. Over this, OOM kill inside the cgroup.
echo 1800M    > memory.high       # throttle. Over this, heavy reclaim - never an OOM kill.
echo 512M     > memory.low        # best-effort protection. Reclaimed only as a last resort.
echo 256M     > memory.min        # hard protection. Never reclaimed, at all.
echo 1G       > memory.swap.max   # SWAP ONLY. Not memory+swap. Read that twice.

# The migration trap, stated as arithmetic:
#   v1: memsw.limit=3G with limit=2G  ->  2G RAM, 1G swap
#   v2: memory.swap.max=3G            ->  memory.max RAM, 3G swap
# Copying 3G across gives the workload three times the swap it used to have.
# The correct translation is (memsw.limit - limit), and if that is zero you
# want memory.swap.max=0, not "unset".

# What is actually happening, rather than what you configured:
cat memory.current                 # bytes in use right now
cat memory.events                  # low high max oom oom_kill oom_group_kill
# low 0
# high 148          <- throttled 148 times: memory.high is doing work
# max 0
# oom 0
# oom_kill 0        <- and it never had to kill anything
cat memory.pressure                # PSI: how much time was lost waiting on memory
# some avg10=0.42 avg60=0.31 avg300=0.11 total=9214430
# full avg10=0.00 avg60=0.00 avg300=0.00 total=118221

# memory.high plus memory.events is the pair that turns "the box OOMs at 3am"
# into a number you can alert on before it happens. v1 could not do this.
v2-DateiWas sie tutKann sie töten?Nächste Entsprechung in v1
memory.minHarter Schutz: Speicher unterhalb dieses Werts wird unter keinem Druck zurückgefordertIndirektKeine
memory.lowBest-Effort-Schutz: wird erst zurückgefordert, wenn nichts Ungeschütztes mehr übrig istNeinKeine
memory.highDrossel: darüber gerät die cgroup unter starken Reclaim und ihre Prozesse werden langsamerNeinmemory.soft_limit_in_bytes, grob
memory.maxHartes Limit: darüber und ohne Reclaim-Möglichkeit läuft der OOM-Killer innerhalb der cgroupJamemory.limit_in_bytes
memory.swap.maxObergrenze ausschließlich für Swap-Nutzung, unabhängig von memory.maxIndirektmemory.memsw.limit_in_bytes minus dem Speicherlimit
memory.eventsZähler: wie oft jeder der obigen Fälle eingetreten ist, inklusive oom_killmemory.failcnt, ohne jede Aufschlüsselung

Die Falle heißt memory.swap.max, und man sollte da sehr deutlich werden, denn die Zahl einfach zu übernehmen ist die naheliegendste Handlung überhaupt. In v1 begrenzte memory.memsw.limit_in_bytes Speicher plus Swap zusammen; in v2 begrenzt memory.swap.max ausschließlich Swap. Ein Container mit limit=2G, memsw=3G durfte 2 GB RAM und 1 GB Swap belegen. Setzen Sie memory.swap.max=3G, haben Sie ihm gerade das Dreifache seines bisherigen Swap zugestanden – und das Fehlerbild ist kein Absturz, sondern eine Maschine, die unter Last langsamer wird, und zwar so, dass es in keinem Speicherdiagramm auffällt. Die korrekte Übersetzung ist die Differenz der beiden v1-Werte, und wenn diese Differenz null ist, wollen Sie eine explizite 0 und keine ungesetzte Datei. Im Gegenzug bekommen Sie memory.events, das die Zähler aufschlüsselt, die v1 in einem einzigen failcnt zusammengeworfen hat, und memory.pressure, das in v1 überhaupt keine Entsprechung hat: eine Stall-Messung, mit der Sie alarmieren können, lange bevor der OOM-Killer läuft.[psi]

CPU-Quota – und ein I/O-Controller, der endlich richtig zählt

Bei der CPU trennen sich sauber zwei Ideen, die v1 vermischt hat. cpu.weight ist ein relativer Anteil an umkämpfter CPU-Zeit – auf einer unausgelasteten Maschine bewirkt es schlicht gar nichts, und es ist das richtige Mittel für Priorisierung. cpu.max ist eine absolute Obergrenze, geschrieben als "$MAX $PERIOD" in Mikrosekunden; sie ersetzt die zwei getrennten v1-Dateien, deren Verhältnis zueinander regelmäßig verwechselt wurde. Die Datei, die ins Monitoring gehört, ist cpu.stat: nr_throttled und throttled_usec beantworten die Frage „Ist dieser Dienst langsam, weil wir ihn gedeckelt haben?", die sich von außen sonst kaum klären lässt.[kio]

# --- CPU: two knobs, and only one of them is a limit ---------------------
cd /sys/fs/cgroup/demo/worker

# Weight: relative share of contended CPU. Default 100, range 1-10000.
# It does nothing at all while the machine is idle.
echo 200 > cpu.weight

# Quota: an absolute ceiling, written as "$MAX $PERIOD" in microseconds.
# 150000 out of every 100000us = 1.5 CPUs. "max" removes the ceiling.
echo '150000 100000' > cpu.max
echo 20000 > cpu.max.burst      # allow short bursts above quota (v2 only)

# The v1 equivalents were three files, and the period was easy to forget:
#   cpu.shares = 200            -> but the scale was different: default 1024
#   cpu.cfs_quota_us  = 150000
#   cpu.cfs_period_us = 100000

# Throttling, which is the number people actually need and rarely find:
cat cpu.stat
# usage_usec 918422311
# nr_periods 41822
# nr_throttled 219       <- how often the quota was hit
# throttled_usec 411920  <- and how much time was lost to it

# --- IO: blkio became io, and the numbers finally mean something ----------
# v1's blkio.throttle.* only saw direct IO; buffered writes were charged to
# whatever kernel thread flushed them, so the accounting was fiction.
echo '259:0 rbps=104857600 wbps=52428800 riops=max wiops=2000' > io.max
echo 'default 100' > io.weight
cat io.stat
# 259:0 rbytes=2841579520 wbytes=1120043008 rios=48211 wios=22103 dbytes=0 dios=0
cat io.pressure
# some avg10=1.94 avg60=0.88 avg300=0.31 total=41822193

# Device numbers, because io.max will not take a path:
lsblk -no MAJ:MIN,NAME /dev/nvme0n1
v2-DateiArtStandardWofür sie da ist
cpu.weightRelativ100 (Bereich 1–10000)Priorität unter Last. Ohne Wirkung auf einer unausgelasteten Maschine
cpu.maxAbsolutmax 100000Eine Obergrenze. 150000 100000 sind 1,5 CPUs
cpu.max.burstAbsolut0Erlaubt kurzes Überschreiten der Quota statt sofortiger Drosselung
cpu.statNur lesendnr_throttled und throttled_usec: der Nachweis, dass eine Obergrenze weh tut
io.weightRelativdefault 100Relativer Anteil an der Plattenzeit, pro Gerät oder insgesamt
io.maxAbsolutnicht gesetztrbps, wbps, riops, wiops je MAJ:MIN
io.latencyZielwertnicht gesetztSchützt ein Latenzziel statt einer Bandbreitenzahl

Der I/O-Controller ist der Teil von v2, der eine echte Verbesserung ist und keine Umbenennung. Die Drosselung im v1-blkio hat immer nur Direct I/O gesehen; gepufferte Schreibvorgänge wurden dem Kernel-Thread angelastet, der sie irgendwann rausgeschrieben hat – die Schreibbuchhaltung pro cgroup war also, freundlich formuliert, Fiktion. Der v2-io-Controller versteht Writeback und rechnet ihn der cgroup zu, die die Seiten schmutzig gemacht hat. Allein diese Änderung macht io.max und io.weight auf einem Datenbank-Host konfigurationswürdig, während die entsprechenden v1-Einstellungen es meistens nicht waren. io.latency und io.cost gehen weiter und schützen das Latenzziel eines Workloads statt seiner Bandbreite – und wenn Sie unsicher sind, wo Sie anfangen sollen: io.latency ist der weniger invasive der beiden.[kdoc]

Die Änderung der CPU-Gewichtung, die Ihnen niemand angekündigt hat

Diese Sache verdient einen eigenen Abschnitt, weil sie die CPU-Priorität jedes containerisierten Workloads verschoben hat – und zwar in einer Komponente, deren Changelog die wenigsten lesen. Kubernetes leitet cpu.shares seit jeher aus dem Request ab, als milliCPU × 1024 / 1000; ein Container, der eine CPU anfordert, bekam also 1024 Shares. Die OCI-Runtime rechnete diese Shares dann in ein v2-Gewicht um, und die ursprüngliche Umrechnung war linear über den Share-Bereich [2, 262144] des Kernels. Rechnen Sie das für 1024 Shares durch, kommt ein Gewicht von 39 heraus – gegen einen cgroup-v2-Standard von 100.[k8scpu][runcissue]

# A quiet change that moved every containerised workload's CPU priority, with
# no release note in most people's changelog because it happened in the OCI
# runtime rather than in the orchestrator.

# Kubernetes derives shares from the CPU request, and always has:
#   cpu.shares = milliCPU * 1024 / 1000
#   request 1000m -> 1024 shares      request 100m -> 102 shares

# runc then converted shares to a v2 weight. The original conversion was
# linear over the kernel's [2, 262144] share range:
#   weight = 1 + ((shares - 2) * 9999) / 262142
python3 -c 'print(1 + ((1024 - 2) * 9999) // 262142)'
# 39
#
# 39. Against a cgroup v2 default of 100. Every container asking for a full CPU
# was scheduled at roughly a third of the weight of anything not in a
# container - including the kubelet and the runtime themselves.

# The replacement is log-based, and is chosen so that one CPU lands on the
# default rather than well below it:
python3 - <<'PY'
import math
def weight(shares):
    if shares == 0: return 0
    if shares <= 2: return 1
    if shares >= 262144: return 10000
    l = math.log2(shares)
    return math.floor(10 ** ((l*l + 125*l) / 612.0 - 7/34) + 0.99)
for req, sh in (("100m",102), ("500m",512), ("1",1024), ("4",4096), ("16",16384)):
    print("%-6s shares=%-6d weight=%d" % (req, sh, weight(sh)))
PY
# 100m   shares=102    weight=17
# 500m   shares=512    weight=59
# 1      shares=1024   weight=100
# 4      shares=4096   weight=303
# 16     shares=16384  weight=942
#
# 1024 lands on exactly 100 because the curve is fitted through three fixed
# points: 2 -> 1, 1024 -> 100, and 262144 -> 10000.

# Check what your nodes are doing, because this depends on the runtime version
# and not on the Kubernetes version. The new conversion ships in runc 1.3.2 and
# later, and in crun 1.23 and later:
runc --version; crun --version 2>/dev/null
cat /sys/fs/cgroup/kubepods.slice/*/*/cpu.weight 2>/dev/null | sort -n | uniq -c

Die Folge: Ein Container, der eine volle CPU anforderte, konkurrierte mit ungefähr einem Drittel der Priorität von allem, was auf demselben Knoten nicht in einem Container lief – System-Daemons und das kubelet selbst eingeschlossen. Der Fix ersetzt die lineare Abbildung durch eine logarithmische Kurve, die durch drei feste Punkte gelegt ist – 2 Shares auf Gewicht 1, 1024 auf 100 und 262144 auf 10000 –, sodass ein Ein-CPU-Request jetzt exakt auf dem cgroup-v2-Standard landet. Ausgeliefert wird sie in runc 1.3.2 und neuer sowie in crun 1.23 und neuer. Zwei Konsequenzen übersieht man leicht. Erstens ist das eine Änderung der Runtime und nicht von Kubernetes: Sie kommt mit dem Upgrade von runc oder crun, das mit einem Cluster-Upgrade zusammenfallen kann, aber nicht muss – prüfen Sie also die Runtime-Version und nicht die Cluster-Version. Zweitens verschiebt sie die relativen Prioritäten zwischen Workloads auf Knoten, die Sie längst migriert haben – wenn Sie das CPU-Verhalten unter v2 vor dieser Änderung gemessen haben, ist Ihr Benchmark veraltet.[runcpr]

Über systemd steuern, nicht über /sys

Auf einem systemd-Host ist die richtige Schnittstelle systemd und nicht das Dateisystem. Das ist keine Stilfrage. systemd legt den cgroup-Baum an und setzt seine eigene Sicht auf die Ressourceneinstellungen einer Unit jedes Mal neu durch, wenn diese Unit neu geladen, neu gestartet oder umkonfiguriert wird – ein Wert, den Sie in memory.max geechot haben, überlebt also exakt bis zur nächsten völlig unbeteiligten Änderung und verschwindet dann ohne eine einzige Logzeile. Das Delegations-Dokument des Upstream-Projekts formuliert die Regel unmissverständlich: ein Schreiber pro Teilbaum. Über systemd zu gehen bringt Ihnen außerdem gratis Persistenz über Reboots hinweg, was Handarbeit an den Dateien nie leistet.[sddeleg][sdresctl]

# Writing into /sys/fs/cgroup by hand works exactly until systemd next touches
# that unit, at which point your values are overwritten without warning.
# systemd owns the tree; ask it, and the setting also survives a reboot.

# Try a limit on something already running, for this boot only:
systemctl set-property --runtime nginx.service MemoryHigh=1G IOWeight=50

# Make it permanent. This writes a drop-in for you - under
# /etc/systemd/system.control/nginx.service.d/, not /etc/systemd/system/, which
# is why hand-searching for your setting in the obvious place turns up nothing.
# No daemon-reload needed.
systemctl set-property nginx.service MemoryMax=2G MemoryHigh=1800M CPUWeight=200

# Or write the drop-in yourself, which is what you want in configuration
# management: /etc/systemd/system/nginx.service.d/50-resources.conf
#
#   [Service]
#   MemoryMax=2G            # -> memory.max
#   MemoryHigh=1800M        # -> memory.high
#   MemoryMin=256M          # -> memory.min
#   MemorySwapMax=0         # -> memory.swap.max
#   CPUWeight=200           # -> cpu.weight
#   CPUQuota=150%           # -> cpu.max  (150% of one CPU)
#   IOWeight=50             # -> io.weight
#   IOReadBandwidthMax=/dev/nvme0n1 100M
#   TasksMax=512            # -> pids.max
#
# Note CPUQuota is a percentage of ONE CPU, not of the machine: 150% is 1.5
# cores. This is the systemd unit that trips people most often.

# Put a limit on a command you are about to run, without writing a unit:
systemd-run --scope --user -p MemoryMax=4G -p CPUQuota=200% -- ./import-job.sh

# And look at the tree systemd actually built, not the one you configured:
systemd-cgls --unit nginx.service
systemd-cgtop --order=memory --iterations=1

# Reading the values back. Note that there is no CPUQuota property to query:
# the unit-file setting CPUQuota= is exposed as CPUQuotaPerSecUSec, and asking
# for the name you wrote is the usual reason this returns nothing.
systemctl show nginx.service -p MemoryMax -p MemoryHigh -p CPUQuotaPerSecUSec

Drei Kommandos erledigen den größten Teil der täglichen Arbeit. systemctl set-property setzt ein Limit sofort und schreibt es für künftige Boots auf die Platte – es sei denn, Sie übergeben --runtime, dann bleibt es temporär. systemd-run --scope -p … legt ein Limit um ein Kommando, das Sie gleich ausführen; das ist der ehrliche Weg, einen Ad-hoc-Import oder ein Backup zu begrenzen, statt auf das Beste zu hoffen. Und systemd-cgtop zeigt den Ressourcenverbrauch pro cgroup statt pro Prozess – genau die Sicht, die Sie brauchen, wenn ein Container-Host beschäftigt ist und top Ihnen zweihundert Prozesse ohne jede Struktur anzeigt. Eine Direktive erwischt alle: CPUQuota= ist ein Prozentsatz einer einzelnen CPU, 150% heißt also anderthalb Kerne und nicht 150 % der Maschine.[sdctl][sdcgtop]

Docker, Podman und Rootless-Limits, die jetzt greifen

Für Container sind die Nachrichten überwiegend gut, denn die Runtime übernimmt die Übersetzung. --memory, --cpus, --memory-reservation und --pids-limit bedeuten weiterhin dasselbe; sie landen jetzt eben auf memory.max, cpu.max, memory.low und pids.max. Auf einem Unified-Host verwendet Docker standardmäßig den cgroup-Treiber systemd und einen privaten cgroup-Namespace – beides sind die richtigen Vorgaben. Die Ausnahme, die man kennen sollte, ist --oom-kill-disable: Dockers eigene Dokumentation sagt, dass die Option unter v2 verworfen wird – nicht übersetzt, nicht mit einer Warnung versehen, verworfen. Ein v2-Äquivalent gibt es bewusst nicht, alles, was darauf baut, muss also neu gedacht und nicht portiert werden.[dockrun]

# --- Docker ---------------------------------------------------------------
docker info --format 'version={{.CgroupVersion}} driver={{.CgroupDriver}}'
# version=2 driver=systemd     <- the defaults on a unified host

# Most flags are unchanged, because the daemon translates them for you:
docker run --memory 2g --memory-reservation 1g --cpus 1.5 --pids-limit 512 nginx
#          -> memory.max        memory.low       cpu.max      pids.max

# Two that are not:
#   --oom-kill-disable   is discarded on cgroup v2. Not translated - discarded.
#                        There is no v2 equivalent, by design.
#   --kernel-memory      removed from the Engine in v23.0. It is gone, not moved.

# Setting the driver explicitly, in /etc/docker/daemon.json. Use systemd unless
# something specific stops you: it is the default on v2 and it is the only
# option that keeps one writer per subtree.
#   { "exec-opts": ["native.cgroupdriver=systemd"] }

# --- Podman rootless: this is the part that only works on v2 --------------
# Under v1, an unprivileged user could not be given controllers at all, so
# rootless resource limits silently did nothing. Under v2 they work, but only
# once systemd delegates the controllers to the user manager:
#
#   /etc/systemd/system/user@.service.d/delegate.conf
#   [Service]
#   Delegate=cpu cpuset io memory pids
#
sudo systemctl daemon-reload    # then log out and back in

# Verify from inside the user session, before blaming the container:
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers
# cpuset cpu io memory pids     <- if memory is missing, --memory does nothing
podman info --format '{{.Host.CgroupsVersion}} {{.Host.CgroupManager}} {{.Host.OCIRuntime.Name}}'
# v2 systemd crun

Rootless-Container sind die eine Stelle, an der v2 keine Abgabe ist, sondern ein Feature. v1-Controller an einen unprivilegierten Benutzer zu delegieren galt nie als sicher, deshalb haben die meisten Rootless-Implementierungen auf einem v1-Host schlicht keine Ressourcenlimits unterstützt. Unter v2 lässt sichere Delegation von Teilbäumen sie wirklich funktionieren – allerdings erst, nachdem systemd die Controller an den User-Manager delegiert hat, wofür es ein Drop-in für user@.service und ein erneutes Anmelden braucht. Das ist der fehlende Schritt hinter praktisch jeder Meldung der Sorte „rootless Podman ignoriert --memory". Prüfen Sie cgroup.controllers in Ihrer eigenen User-Slice, bevor Sie dem Container die Schuld geben: Wenn memory dort nicht aufgeführt ist, wird kein Flag durchgesetzt, das Sie übergeben. Für die Delegation von cpuset brauchen Sie zusätzlich systemd 244 oder neuer.[podman][crun]

Kubernetes: was stimmt – und was Sie überall lesen

Jetzt der Teil, über den am häufigsten falsch berichtet wird – sorgfältig formuliert. Kubernetes hat cgroup v1 nicht entfernt. Die Dokumentation markiert es seit v1.35 als veraltet, und die praktische Folge ist, dass das kubelet auf einem cgroup-v1-Knoten standardmäßig nicht startet. Dieser Standard ist ein Feld der KubeletConfiguration, failCgroupV1, und wer es auf false setzt, stellt das alte Verhalten wieder her. KEP-5573 – das Enhancement, das die Entfernung irgendwann vollzieht – sagt, die Entfernung erfolge nicht vor 1.38. Wenn Ihnen ein Blogpost erzählt hat, 1.36 habe cgroup v1 gelöscht, dann war er falsch, und der Unterschied zwischen „veraltet, aber überschreibbar" und „entfernt" ist der Unterschied zwischen einer geplanten Migration und einem verlorenen Wochenende.[k8scg][kep5573]

# What the cluster thinks it is standing on. Run this first; mixed node pools
# are the normal case, not the exception.
kubectl get nodes -o custom-columns=\
'NODE:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion,'\
'RUNTIME:.status.nodeInfo.containerRuntimeVersion,OS:.status.nodeInfo.osImage'

# The kernel version alone does not tell you the hierarchy. Ask each node.
# Note the -it: without it, kubectl debug does not attach, the output goes to
# the debug pod's log instead of your terminal, and you are left with one
# orphaned pod per node.
kubectl get nodes -o name | while read -r n; do
  printf '%-40s ' "${n#node/}"
  kubectl debug "$n" -it --image=busybox --profile=general -- \
    stat -fc %T /host/sys/fs/cgroup/ 2>/dev/null || echo '(debug unavailable)'
done
# Clean up afterwards - the debug pods are not removed for you:
kubectl delete pod -l app.kubernetes.io/managed-by=kubectl-debug 2>/dev/null

# On the node itself - the three files that have to agree:
stat -fc %T /sys/fs/cgroup/                       # cgroup2fs
grep -E '^(cgroupDriver|failCgroupV1):' /var/lib/kubelet/config.yaml
grep -A2 'runc.options' /etc/containerd/config.toml   # SystemdCgroup = true

# --- what is actually true about Kubernetes and cgroup v1 -----------------
# cgroup v1 is DEPRECATED as of v1.35, not removed. The kubelet refuses to
# start on a v1 node by default, and that default is overridable:
#
#   /var/lib/kubelet/config.yaml
#   apiVersion: kubelet.config.k8s.io/v1beta1
#   kind: KubeletConfiguration
#   cgroupDriver: systemd
#   failCgroupV1: false        # <- the escape hatch. Buys time, not a fix.
#
# KEP-5573 states the code removal will happen no earlier than 1.38.

# The v2-only features you get in exchange, and how to see them:
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl get --raw "/api/v1/nodes/$NODE/proxy/metrics/cadvisor" \
  | grep -E '^container_pressure_(cpu|memory|io)_' | head
# container_pressure_memory_stalled_seconds_total{...}
# container_pressure_memory_waiting_seconds_total{...}

Die Anforderungen auf Knotenseite sind überschaubar und sollten geprüft statt angenommen werden: Kernel 5.8 oder neuer, containerd v1.4+ oder CRI-O v1.20+, und kubelet und Runtime müssen speziell den systemd-cgroup-Treiber verwenden – nicht bloß denselben. Die letzte Bedingung war früher eine ständige Quelle halb funktionierender Knoten, weil sie davon abhing, dass zwei Konfigurationsdateien übereinstimmen; seit v1.34 fragt das kubelet die CRI-Runtime direkt nach ihrem Treiber, was diese ganze Problemklasse für alle mit hinreichend aktueller Runtime erledigt. Zurück bekommen Sie für die Migration eine Reihe von Funktionen, die es nur unter v2 gibt:[k8sdriver][k8spsi]

  • PSI-Metriken, GA in v1.36. Das kubelet liest cpu.pressure, memory.pressure und io.pressure pro cgroup und stellt sie über die Summary-API und den cAdvisor-Metrics-Endpunkt bereit. Diese Dateien existieren unter v1 nicht, das ist also kein Feature, das sich zurückportieren lässt – es ist ein Grund zu migrieren.
  • Memory QoS. Das kubelet kann memory.high aus dem memoryThrottlingFactor ableiten, sodass ein Container hart zurückgefordert wird, bevor er abgeschossen wird; davon getrennt kann es unter memoryReservationPolicy: TieredReservation zusätzlich memory.min für Guaranteed-Pods und memory.low für Burstable-Pods setzen. Stand v1.36 ist das Alpha und standardmäßig aus, also etwas zum Testen und nicht zum Verlassen – aber die Form davon ist genau das, wofür das vierstufige Speichermodell gedacht war.
  • Rootless- und User-Namespace-Workloads, die Limits tatsächlich durchsetzen. Alles aus dem Podman-Abschnitt gilt auch für einen Kubernetes-Knoten, und deshalb sind User-Namespaces auf dem Weg zu GA und cgroup v2 zusammenhängende und keine zufällig gleichzeitigen Geschichten.
  • Ehrliche I/O-Buchhaltung pro Pod. Gepufferte Schreibvorgänge werden der cgroup zugerechnet, die sie verursacht hat – damit wird der Plattenverbrauch pro Pod zu einer Zahl, mit der man arbeiten kann, statt zu einer, für die man sich entschuldigt.

Eine Planungsnotiz, die man in gemischten Landschaften leicht falsch macht. Weil der Treiber dieser Entwicklung systemd ist und nicht Kubernetes, migrieren sich Node-Pools tendenziell von selbst, sobald ihre Basis-Images weiterrollen – lange vor jeder Entscheidung auf Cluster-Ebene. Das ist in Ordnung, bedeutet aber, dass Sie mit einem Cluster enden können, in dem die Hälfte der Knoten auf v2 läuft und die andere nicht, mit denselben Workloads bei deutlich unterschiedlichem Speicher- und CPU-Verhalten – und kein Alarm der Welt wird Ihnen das sagen. Auditieren Sie die Knoten, schließen Sie nicht von der Cluster-Version auf sie.[k8sqos]

v2 dort einschalten, wo es noch nicht läuft

Wenn Sie noch Hosts auf v1 oder Hybrid haben, kommt jetzt der mechanische Teil, und der ist kurz. Zunächst: Ab systemd 258 gibt es nichts einzuschalten und nichts auszuschalten – unified ist der einzige Modus, den der Code kennt. Den alten Kernelparameter sollten Sie auf diesen Hosts trotzdem entfernen. systemd 258 wertet ihn nicht mehr aus, aber wenn eine Initrd das noch tut und eine v1-Hierarchie mountet, verweigert PID 1 den Start und fordert Sie auf, die veraltete Kommandozeilenoption zu entfernen – ein deutlich besseres Fehlerbild als ein stilles, und trotzdem ein Bootvorgang, den Sie von der Konsole aus reparieren müssen. Alles Folgende gilt nur für Hosts, die alt genug sind, um überhaupt noch eine Wahl zu haben. Und dort sollte die Wahl in einem Wartungsfenster getroffen werden, das auch die Container-Runtimes mitzieht, denn ein Knoten, der mit einem cgroupfs-Treiber nach v2 hochkommt, kommt in einem interessanten Zustand zurück.[sd258]

# Only needed on hosts old enough to still default to v1 or hybrid. On
# systemd 258 and later there is nothing to enable: unified is the only mode.

# 1. Check you can. Kubernetes wants kernel 5.8+; systemd 258 needs 5.4 as an
#    absolute floor and recommends 5.7. Below that, upgrade the OS instead.
uname -r

# 2. Set the kernel parameter. Debian and Ubuntu. The grep guard matters:
#    without it, running this twice adds the parameter twice.
grep -q 'systemd.unified_cgroup_hierarchy' /etc/default/grub || \
  sudo sed -i 's/^GRUB_CMDLINE_LINUX="/&systemd.unified_cgroup_hierarchy=1 /' \
       /etc/default/grub
sudo update-grub

#    RHEL, Fedora, Rocky, Alma - grubby, and note ALL rather than the running
#    kernel, or the setting vanishes at the next kernel update:
sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"

# 3. Line up the container runtimes in the SAME maintenance window. A node
#    that reboots into v2 with a cgroupfs driver is a node that does not come
#    back cleanly.
#    /etc/docker/daemon.json      -> "exec-opts": ["native.cgroupdriver=systemd"]
#    /etc/containerd/config.toml  -> SystemdCgroup = true
#    /var/lib/kubelet/config.yaml -> cgroupDriver: systemd

sudo reboot

# 4. Verify, in this order. If step one disagrees with step three, stop.
stat -fc %T /sys/fs/cgroup/            # cgroup2fs
cat /sys/fs/cgroup/cgroup.controllers  # non-empty
systemctl --failed
docker info --format '{{.CgroupVersion}}/{{.CgroupDriver}}'   # 2/systemd

# Rolling back is removing the parameter and rebooting - but only while your
# systemd is older than 258. After that the parameter is inert and the only
# way back is downgrading the OS, which is not a rollback plan.
Was kaputtgehtWarumWas Sie stattdessen tun
Skripte, die /sys/fs/cgroup/memory/… lesenVerzeichnisse pro Controller gibt es unter der Unified Hierarchy nichtDie flachen v2-Pfade lesen oder den Wert über systemctl show abfragen
Werkzeuge, die in systemd-eigene cgroups schreibensystemd setzt seine Einstellungen bei jeder Unit-Änderung stillschweigend neu durchsystemctl set-property oder eine Drop-in-Datei
Traffic-Markierung über net_clsController entfernt, ohne ErsatzdateieBPF an den cgroup-Pfad hängen und aus nftables darauf matchen
Whitelists über devices.allowController durch einen eBPF-Programmtyp ersetztDeviceAllow= in einer Unit oder ein eBPF-Device-Programm
Layouts mit Limits auf jeder Ebene des BaumsNo-Internal-Process-RegelProzesse in die Blätter schieben; innere cgroups leer halten
--oom-kill-disableUnter v2 bewusst verworfenmemory.max richtig dimensionieren und über memory.high vorher gewarnt werden
Wortwörtlich übernommene Swap-Limitsmemory.swap.max ist Swap allein, nicht Speicher plus SwapDie Differenz der beiden v1-Werte setzen oder eine explizite 0

Zwei Anmerkungen zum Rollback, denn niemand plant eine Migration ohne eines. Solange Ihr systemd 257 oder älter ist, besteht das Rollback darin, den Kernelparameter zu entfernen und neu zu starten – das ist wirklich billig. Sobald Sie auf 258 oder neuer sind, gibt es keinen unterstützten Weg zurück außer einem Downgrade des Betriebssystems; das ist kein Rollback-Plan, das ist eine Neuinstallation. Sortieren Sie Ihre Flotte entsprechend: erst die Hosts, die noch einen Fluchtweg haben, aus deren Umstellung lernen, und erst danach die ohne.[dockrun]

Nachweisen statt hoffen

Verifizieren ist keine Geschmacksfrage. Der Sinn des Skripts unten ist, dass jede Zeile entweder OK ausgibt oder sich selbst erklärt, und dass der Exit-Code die Anzahl der Fehlschläge ist – damit kann es direkt in das laufen, was nach einem Reboot ausgeführt wird. Zwei Teile davon verdienen ihren Platz. Der erste gibt eine Bestandsaufnahme aller laufenden Units aus, für die tatsächlich ein MemoryMax gesetzt ist – die vergleichen Sie mit der Liste derer, die Sie konfigurieren wollten: Eine Unit, die in dieser Ausgabe fehlt, hat ihr Drop-in im falschen Verzeichnis, und ohne den Soll-Zustand zu kennen lässt sich das gar nicht erkennen. Der zweite durchsucht den gesamten Baum nach Drosselung, denn ein steigender nr_throttled bei einem Dienst, über den sich niemand beschwert, ist das klassische Zeichen für ein zu eng übersetztes Limit.[sdcgls]

#!/usr/bin/env bash
# Post-migration verification. Every check either prints OK or explains
# itself; nothing here is judged by eye. Exit code is the number of failures.
fail=0
chk() { if eval "$2" >/dev/null 2>&1; then printf 'OK   %s\n' "$1";
        else printf 'FAIL %s\n' "$1"; fail=$((fail+1)); fi; }

chk 'unified hierarchy'        '[ "$(stat -fc %T /sys/fs/cgroup/)" = cgroup2fs ]'
chk 'controllers available'    '[ -s /sys/fs/cgroup/cgroup.controllers ]'
chk 'memory controller'        'grep -qw memory /sys/fs/cgroup/cgroup.controllers'
chk 'io controller'            'grep -qw io     /sys/fs/cgroup/cgroup.controllers'
chk 'single cgroup line'       '[ "$(wc -l < /proc/self/cgroup)" -eq 1 ]'
chk 'no v1 leftovers mounted'  '! mount | grep -q "type cgroup "'
chk 'no failed units'          '[ -z "$(systemctl list-units --state=failed --no-legend)" ]'
chk 'PSI available'            '[ -r /sys/fs/cgroup/cpu.pressure ]'

# Limits are actually applied, rather than merely configured. A unit whose
# MemoryMax reads "infinity" after you set it is a unit whose drop-in is in
# the wrong place - a very common outcome of hand-editing.
for u in $(systemctl list-units --type=service --state=running \
           --no-legend --plain | awk '{print $1}'); do
  m=$(systemctl show "$u" -p MemoryMax --value)
  [ "$m" != "infinity" ] && printf '     %-34s MemoryMax=%s\n' "$u" "$m"
done

# Nothing is being silently throttled. nr_throttled climbing on a service that
# is not busy means cpu.max is too tight, and it will not appear in load
# average. Search the whole tree, not just the top-level slices: throttling
# happens on the leaf that holds the process.
find /sys/fs/cgroup -name cpu.stat -exec \
  awk '/^nr_throttled/ && $2>0 {print FILENAME": "$0}' {} + 2>/dev/null

# Containers agree with the host.
command -v docker >/dev/null && \
  chk 'docker on v2/systemd' '[ "$(docker info -f "{{.CgroupVersion}}/{{.CgroupDriver}}")" = 2/systemd ]'

printf '\n%d failure(s)\n' "$fail"; exit "$fail" 

Lassen Sie es auch vor der Migration laufen und heben Sie die Ausgabe auf. Ein großer Teil dessen, was später einer cgroup-Migration angelastet wird, war vorher schon wahr, und der einzige Weg, das zu wissen, ist gemessen zu haben. Wenn nr_throttled bei diesem Dienst schon letzte Woche gestiegen ist, war die neue Hierarchie nicht die Ursache.

Die Reihenfolge, in der Sie vorgehen

Die Entscheidung, komprimiert. Die ehrliche Zusammenfassung lautet: Die meisten haben längst ohne Projekt migriert, und was übrig bleibt, sind nicht die Hosts, sondern das Tooling – die Skripte, Agenten und Dashboards, die weiterhin v1-Pfade lesen und still Nullen liefern werden, bis jemand nachsieht. Kubernetes gibt Ihnen frühestens bis 1.38 Zeit und Docker deutlich länger, aber keines dieser Daten ist Ihre Deadline. Ihre Deadline ist der Zeitpunkt, an dem das nächste Basis-Image systemd über 258 hebt – und auf den meisten Landschaften ist das bereits passiert.[kep5573]

Wenn Ihre Lage so aussieht …… dann ist die Deadline …… und die Arbeit ist …
Überall meldet stat bereits cgroup2fsLängst verstrichen, unbemerktNur das Tooling: die noch gelesenen v1-Pfade finden und korrigieren
Eine Handvoll alter Hosts auf v1 oder hybridSobald deren nächstes OS-Upgrade kommtKernelparameter plus Runtime-Treiber, je ein Wartungsfenster
Kubernetes-Knoten auf gemischten Basis-ImagesWenn die Node-Images weiterrollen, nicht beim Cluster-UpgradePro Knoten auditieren; die Hierarchie nicht aus der Cluster-Version ableiten
Docker-Hosts ohne OrchestriererEngine v29.0 hat es als veraltet markiert; die Entfernung ist Jahre entferntVon Docker her wenig dringend, aber systemd zieht den Host ohnehin zuerst um
Eigenes Tooling, das cgroup-Dateien direkt schreibtJetzt – und das ist das eigentliche ProjektGegen die systemd-Schnittstellen neu schreiben, bevor die Hosts unter Ihnen wegziehen
Eine Appliance oder ein Hersteller-Agent, den Sie nicht ändern könnenDer Zeitplan des Herstellers, der nicht Ihrer istSchriftlich einfordern und den Host isolieren, falls die Antwort unbefriedigend ist
  1. Auditieren, bevor Sie planen. Fahren Sie das Flotten-Skript auf jedem Host und heben Sie die Ausgabe auf. Sie suchen zwei Dinge: Hosts, die noch auf v1 oder Hybrid laufen, und fest verdrahtete v1-Pfade in /etc, /opt und /usr/local. Die zweite Liste ist fast immer länger als die erste – und sie ist die eigentliche Arbeit.
  2. Erst das Tooling reparieren, solange beide Hierarchien noch existieren. Alles, was /sys/fs/cgroup liest, sollte beide Layouts beherrschen oder stattdessen über systemctl show gehen. Wenn Sie das vor dem Umzug der Hosts erledigen, können Sie den Fix gegen genau das testen, was er reparieren soll.
  3. Limits nach Bedeutung übersetzen, nicht nach Namen. Zwei Zeilen der Umrechnungstabelle ändern das Verhalten: Swap, wo memory.swap.max ausschließlich Swap begrenzt, und die CPU-Gewichtung, deren Skala sich um mehr als den Faktor zehn von den Shares unterscheidet. Jede andere Zeile ist eine Umbenennung.
  4. Runtimes und Hosts im selben Fenster umstellen. Kernelparameter, Docker-Treiber, containerd SystemdCgroup, kubelet cgroupDriver – alle vier, ein Reboot, dann verifizieren, bevor die nächste Charge drankommt.
  5. Den Ertrag einstreichen. Verdrahten Sie memory.high und memory.events, bringen Sie PSI auf ein Dashboard und schalten Sie die I/O-Limits pro cgroup ein, die unter v1 nicht der Mühe wert waren. Diese Migration hat am Ende kein Feature, wenn Sie sich keines holen.

Das gehört zu drei weiteren Teilen derselben Umstellung, und sie verstärken sich gegenseitig: die Migration von SysV-Init-Skripten und rc.local auf systemd-Units, weil beide Änderungen in denselben systemd-Releases und auf denselben Servern landen; das Server-Upgrade von Ubuntu 24.04 auf 26.04, wo die meisten Flotten die Linie tatsächlich überschreiten werden; und die Breaking Changes in Docker Engine 29, dessen Deprecation von cgroup v1 die Container-Hälfte dieser Geschichte ist. Und wenn Sie gerade abwägen, wie viel von dieser Komplexität Sie überhaupt brauchen: wann man Kubernetes besser nicht einsetzt ist die andere Seite dieser Diskussion.

Häufige Fragen

Wie finde ich heraus, ob ich cgroup v1 oder v2 nutze?

Führen Sie stat -fc %T /sys/fs/cgroup/ aus. Gibt es cgroup2fs aus, laufen Sie auf der Unified Hierarchy; gibt es tmpfs aus, sind Sie auf v1 oder auf dem Hybrid-Layout. Verwenden Sie nicht mount | grep cgroup2: Das Hybrid-Layout mountet unter /sys/fs/cgroup/unified eine cgroup2-Hierarchie ohne angehängte Controller, der grep trifft also und sagt Ihnen das Gegenteil der Wahrheit. Eine zweite Bestätigung ist /proc/self/cgroup, das unter v2 genau eine Zeile mit 0:: am Anfang enthält.

Hat Kubernetes 1.36 cgroup v1 entfernt?

Nein. cgroup v1 ist seit Kubernetes v1.35 als veraltet markiert, und die praktische Wirkung ist, dass das kubelet auf einem cgroup-v1-Knoten standardmäßig nicht startet. Dieser Standard ist ein Feld der KubeletConfiguration, failCgroupV1, und wer es auf false setzt, stellt das bisherige Verhalten wieder her. KEP-5573, das Enhancement, das den Code irgendwann entfernt, hält fest, dass die Entfernung nicht vor v1.38 erfolgt. Mehrere viel geteilte Beiträge behaupten etwas anderes; maßgeblich ist das KEP.

Ist memory.swap.max dasselbe wie memory.memsw.limit_in_bytes?

Nein, und das ist das folgenreichste Missverständnis der ganzen Migration. In cgroup v1 begrenzte memory.memsw.limit_in_bytes Speicher und Swap zusammen, eine cgroup mit limit=2G und memsw=3G konnte also 2 GB RAM plus 1 GB Swap nutzen. In cgroup v2 begrenzt memory.swap.max ausschließlich Swap. Wer die 3G einfach übernimmt, gewährt das Dreifache des bisherigen Swap-Kontingents. Die korrekte Übersetzung ist die Differenz der beiden v1-Werte; ist diese Differenz null, setzen Sie eine explizite 0.

Was ist der Unterschied zwischen memory.high und memory.max?

memory.max ist ein hartes Limit: Erreicht eine cgroup es und kann nichts mehr zurückfordern, läuft der OOM-Killer innerhalb dieser cgroup. memory.high ist eine Drossel: Ein Überschreiten setzt die cgroup unter starken Reclaim-Druck und bremst ihre Prozesse aus, und die Kernel-Dokumentation stellt ausdrücklich fest, dass ein Überschreiten niemals den OOM-Killer aufruft. In der Praxis setzen Sie memory.high etwas unter memory.max und alarmieren auf den Zähler high in memory.events – so werden Sie gewarnt, bevor etwas abgeschossen wird. cgroup v1 hatte dafür keinen vergleichbaren Mechanismus.

Warum bekommen meine Container nach der Umstellung auf cgroup v2 weniger CPU?

Wegen der Umrechnung von Shares in Gewichte, nicht wegen v2 selbst. Kubernetes leitet cpu.shares aus dem CPU-Request als milliCPU × 1024 / 1000 ab, ein Ein-CPU-Request ergab also 1024 Shares. Die ursprüngliche Umrechnung in den OCI-Runtimes bildete das linear auf den v2-Gewichtsbereich ab und lieferte 39 – gegen einen cgroup-v2-Standard von 100. Container konkurrierten damit mit etwa einem Drittel der Priorität von Prozessen außerhalb jedes Containers. Die logarithmische Ablösung legt 1024 Shares auf exakt 100, und sie wird in runc 1.3.2 und neuer sowie in crun 1.23 und neuer ausgeliefert. Prüfen Sie also Ihre Runtime-Version, nicht Ihre Kubernetes-Version: Das kommt mit einem Node-Image- oder Runtime-Upgrade, nicht mit einem der Control Plane.

Kann ich cgroup v1 noch mit systemd.unified_cgroup_hierarchy=0 erzwingen?

Nur auf systemd 257 und älter, und dort braucht es auf der Kernel-Kommandozeile sowohl systemd.unified_cgroup_hierarchy=0 als auch SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1. systemd 256 hat aufgehört, cgroup v1 standardmäßig zu booten, und diesen Notausgang eingeführt; 257 wertet ihn weiterhin aus; systemd 258 hat die cgroup-v1-Unterstützung vollständig entfernt, den Notausgang eingeschlossen. Ab 258 wertet systemd die Option nicht mehr aus – entfernen Sie sie aber trotzdem, statt sie stehen zu lassen: Wenn Ihre Initrd noch darauf reagiert und eine v1-Hierarchie mountet, verweigert PID 1 den Start und fordert Sie auf, die veraltete Kommandozeilenoption zu löschen.

Was ersetzt die Controller devices, net_cls und net_prio?

In allen drei Fällen eBPF. Die Zugriffskontrolle auf Geräte ist heute ein eBPF-Programm vom Typ BPF_PROG_TYPE_CGROUP_DEVICE, das Major- und Minor-Nummer, den Gerätetyp und die Zugriffsart erhält und mit „erlaubt" oder -EPERM antwortet; auf einem systemd-Host steuert die Unit-Direktive DeviceAllow= das für Sie. Für Netzwerk-Klassifizierung und -Priorisierung gibt es weder einen v2-Controller noch eine Ersatz-Interface-Datei: Sie hängen ein eBPF-Programm an den cgroup-Pfad und matchen aus iptables oder nftables darauf. Das sind die drei Zeilen der Umrechnungstabelle, die Engineering-Arbeit erfordern und keine Pfadersetzung.

Warum ignoriert rootless Podman mein Memory-Limit unter cgroup v2?

Fast immer, weil systemd den Memory-Controller nicht an Ihren User-Manager delegiert hat. Legen Sie ein Drop-in für user@.service mit Delegate=cpu cpuset io memory pids an, laden Sie systemd neu und melden Sie sich ab und wieder an. Prüfen Sie mit cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers: Steht memory nicht in dieser Liste, kann kein an Podman übergebenes Flag durchgesetzt werden. Die Delegation von cpuset setzt speziell systemd 244 oder neuer voraus. Unter cgroup v1 galt es nicht als sicher, Controller an einen Nicht-Root-Benutzer zu delegieren, und die meisten Rootless-Implementierungen haben es deshalb nicht unterstützt – Rootless-Ressourcenlimits sind also eine Fähigkeit, die v2 hinzufügt, und kein Fehler, den v2 eingeführt hat.

Muss ich meine Docker-Befehle ändern?

Größtenteils nein. --memory, --cpus, --memory-reservation und --pids-limit behalten ihre Bedeutung und werden auf memory.max, cpu.max, memory.low und pids.max übersetzt. Zwei Ausnahmen zählen: --oom-kill-disable wird unter cgroup v2 ersatzlos verworfen, und --kernel-memory wurde bereits in v23.0 aus der Engine entfernt. Auf einem Unified-Host verwendet Docker standardmäßig den cgroup-Treiber systemd und einen privaten cgroup-Namespace; lassen Sie beides in Ruhe, sofern nichts Konkretes dagegen spricht.

Bringt cgroup v2 einen Performance-Vorteil oder ist das reiner Migrationsaufwand?

Es gibt einen echten Vorteil, konzentriert auf I/O und Observability. Der v1-Controller blkio hat nur Direct I/O verbucht, gepufferte Schreibvorgänge wurden also dem jeweils flushenden Kernel-Thread angelastet und Schreiblimits pro cgroup waren weitgehend Dekoration; der v2-Controller io versteht Writeback und rechnet ihn korrekt zu, was io.max und io.latency auf Datenbank- und Build-Hosts konfigurationswürdig macht. Dazu kommt PSI – cpu.pressure, memory.pressure, io.pressure –, das es nur unter v2 gibt und das den Unterschied ausmacht zwischen zu wissen, dass eine Maschine unter Druck steht, und es zu erfahren, wenn etwas stirbt.

Die darunterliegende Runtime folgt derselben Uhr: containerd 1.7 verlässt den erweiterten Support im September 2026, und für Kubernetes 1.36 listet die Support-Matrix des Projekts nur 2.3.0+ und 2.2.0+ — ganz ohne 1.x-Eintrag. die Migration von containerd 1.7 auf 2.x behandelt die Umschreibung der Konfiguration auf Version 3, die Registry-Umstellung, die das Laden des CRI-Plugins verhindert, und warum 2.3 der einzige lohnende Ziel-Branch ist.

Die Service-Datenebene auf denselben Knoten folgt ihrer eigenen Uhr: Kubernetes 1.37 hat den ipvs-Modus von kube-proxy hinter einem Feature Gate für veraltet erklärt, 1.40 schaltet ihn standardmäßig ab und 1.43 löscht den Code. die Umstellung von kube-proxy von IPVS auf nftables behandelt die Kernel-Untergrenze 5.13, das NodePort-Verhalten, das sich unbemerkt ändert, und das zurückbleibende kube-ipvs0, das Datenverkehr verschluckt, wenn es niemand aufräumt.

Eine Anmerkung auf Release-Ebene, denn die Bilanz für 1.37 sieht anders aus, als fast überall zu lesen ist: Was tatsächlich Pods in ContainerCreating hängen lassen kann, ist SELinuxMount auf GA - der cgroup-v1-Fehler kam in 1.35, die Static-Pod-Einschränkung in 1.34, und die containerd-Klippe liegt noch vor Ihnen, in 1.38. was beim Upgrade auf Kubernetes 1.37 wirklich kaputtgeht trennt die drei Spalten und liefert das Audit, das vor dem Upgrade gehört und nicht danach.

Quellen

Zuerst die Primärquellen: Die Kernel-Dokumentation definiert jede hier zitierte Interface-Datei, und die Release Notes und Enhancement Proposals der Projekte selbst sind die einzige verlässliche Aussage darüber, was wann entfernt wurde. Wo dieser Artikel der Sekundärberichterstattung widerspricht – insbesondere zu Kubernetes und zu Docker –, richtet sich der Widerspruch gegen die Schlagzeile, nicht gegen die Primärquelle.

  1. Linux kernel — Control Group v2: the normative document. Every interface file, default value and range quoted in this article was checked here, including the fact that memory.max defaults to "max" and cpu.max defaults to "max 100000"
  2. Control Group v2 — Memory interface files: memory.min, memory.low, memory.high and memory.max, and the sentence that going over memory.high never invokes the OOM killer. This is the four-tier model cgroup v1 did not have
  3. Control Group v2 — IO interface files: io.weight, io.max with its rbps/wbps/riops/wiops keys, io.latency and io.cost. The v2 io controller is also the first one that accounts for writeback correctly
  4. Control Group v2 — No Internal Process Constraint: non-root cgroups can only distribute resources to children when they hold no processes of their own. This single rule is what breaks hand-rolled v1 layouts on contact
  5. Control Group v2 — Delegation: the model that makes rootless containers possible, and the reason a delegated subtree must not be allowed to write its own resource-control files
  6. Linux kernel — Memory Resource Controller (cgroup v1): the source for what memory.limit_in_bytes and memory.memsw.limit_in_bytes actually meant, which is the only way to see how different memory.swap.max is
  7. cgroups(7) — the manual page, including the statement that there is no direct equivalent of the net_cls and net_prio controllers, and that iptables gained support for eBPF filters hooking on cgroup v2 pathnames instead
  8. BPF_PROG_TYPE_CGROUP_DEVICE — the eBPF program type that replaced the v1 devices controller: it receives major, minor, device type and access type, and returns allow or -EPERM
  9. systemd — NEWS: the upstream changelog and the authoritative statement of what happened in which release. The v258 section carries both the cgroup v1 removal and the kernel baseline bump quoted here
  10. systemd v258 release notes — "Support for cgroup v1 ('legacy' and 'hybrid' hierarchies) has been removed", and the bump of the minimum kernel baseline to v5.4 with v5.7 recommended
  11. systemd v256 release notes — the release that stopped booting cgroup v1 by default and introduced the SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1 escape hatch that v258 then took away
  12. systemd.resource-control(5) — MemoryMax=, MemoryHigh=, MemoryLow=, MemoryMin=, MemorySwapMax=, CPUWeight=, CPUQuota=, IOWeight=, IOReadBandwidthMax= and TasksMax=: the directive names for every interface file in the conversion table
  13. systemctl(1) — set-property, and the fact that it applies changes immediately and stores them on disk for future boots unless --runtime is passed
  14. systemd-run(1) — --scope and --property=, the pair that lets you put a limit on a command you are about to run without writing a unit file first
  15. systemd-cgls(1) — recursively show control group contents: the fastest way to see the tree systemd actually built, as opposed to the one you think you configured
  16. systemd-cgtop(1) — top control groups by resource usage, which is the per-cgroup view that plain top cannot give you
  17. systemd — Control Group APIs and Delegation: upstream's own rules for who owns which part of the tree, and why writing into systemd's cgroups from outside systemd is a bug rather than a technique
  18. Kubernetes — About cgroup v2: the requirements (kernel 5.8 or later, containerd v1.4+, cri-o v1.20+, systemd cgroup driver), the stat -fc %T check, and the deprecation notice marking cgroup v1 deprecated as of v1.35
  19. KEP-5573, Remove cgroup v1 support — the document that says removal "will be done no earlier than 1.38". Worth reading before believing any headline that says Kubernetes has already removed it
  20. Kubernetes blog — New Conversion from cgroup v1 CPU Shares to v2 CPU Weight: why a container requesting 1 CPU ended up below the default weight on v2, and the replacement formula
  21. runc pull request 4785 — the dependency bump that pulls the new shares-to-weight conversion into runc. The conversion itself lives in the opencontainers/cgroups library, which is where the change reaches everyone regardless of orchestrator
  22. runc issue 4772 — the report behind that change: the linear conversion gave 1024 shares a weight of 39 against a default of 100, so containers lost CPU to everything not in a container
  23. Kubernetes blog — Autoconfiguration for Node Cgroup Driver Goes GA: the kubelet now asks the CRI runtime which cgroup driver it uses instead of trusting two files to agree
  24. Kubernetes — Understand PSI metrics: pressure stall information read from cpu.pressure, memory.pressure and io.pressure, which exist only under cgroup v2
  25. Kubernetes blog — Tiered Memory Protection with Memory QoS: the kubelet writing memory.high and, under memoryReservationPolicy, memory.min and memory.low. None of this has a cgroup v1 equivalent
  26. Docker — Runtime metrics: the cgroup v2 requirements (containerd v1.4+, kernel v4.15+ with v5.2+ recommended), the default driver being systemd on v2 and cgroupfs on v1, and the sentence that --oom-kill-disable is discarded on v2
  27. Docker Engine — Deprecated features: the table row recording that support for cgroup v1 was deprecated in Engine v29.0, with no removal version set, and that the kernel memory limit was removed back in v23.0
  28. moby issue 51111 — the proposal to deprecate cgroup v1 while maintaining it until the enterprise distributions that still need it reach end of life. This is why Docker's deadline is much later than systemd's
  29. Rootless Containers — cgroup v2: the systemd user-manager Delegate= drop-in that gives an unprivileged user real cpu, memory, io and pids limits, and the note that delegating cpuset needs systemd 244 or newer
  30. crun — the OCI runtime with native cgroup v2 support and the default on current Podman installations, which matters because runc reached v2 later and older builds handle it badly
  31. Red Hat Enterprise Linux 10 release notes — the release where systemd no longer supports booting in cgroup v1 mode at all, for readers whose deadline is an enterprise distribution rather than upstream
  32. Linux kernel — PSI, Pressure Stall Information: what the numbers in cpu.pressure, memory.pressure and io.pressure mean, and why "some" and "full" are different questions

War das hilfreich?