cgroup v2 (Unified Hierarchy)
In plain English
Plain definition
cgroup v2 is the modern Linux kernel resource-control subsystem that unifies CPU, memory, I/O, and PID accounting into one consistent hierarchy instead of cgroup v1's independently mounted controllers.
Under cgroup v1, each controller (cpu, memory, blkio, devices) could be mounted on a separate hierarchy, letting a process belong to different cgroup trees for different resources. This made cross-controller correlation nearly impossible and produced subtle bugs where a container’s CPU cgroup and memory cgroup disagreed about its parentage. cgroup v2 collapses this into a single unified hierarchy: one directory tree under /sys/fs/cgroup, where every controller that is enabled applies uniformly to every node in that tree. A process can only exist in one cgroup at a time, which restores a clean containment invariant that Kubernetes’ QoS classes (Guaranteed, Burstable, BestEffort) and systemd’s slice/scope model both depend on for correctness.
The interface files themselves changed semantics, not just names. memory.limit_in_bytes became memory.max (hard limit, triggers OOM kill) alongside a new memory.high (soft limit that throttles the cgroup via reclaim and stalls before the kernel resorts to killing anything). This two-tier model lets orchestrators implement graceful memory pressure backoff instead of binary OOM events. Similarly, cpu.max replaces the v1 cpu.cfs_quota_us/cpu.cfs_period_us pair with a single quota period line, and the io controller unifies what was split across blkio.throttle.* files. Critically, cgroup v2 exposes PSI (Pressure Stall Information) via cpu.pressure, memory.pressure, and io.pressure, giving a time-weighted measure of tasks stalled waiting on a resource — this is what kubelet’s node-pressure eviction and tools like oomd/systemd-oomd use to preempt failure before the global OOM killer fires indiscriminately.
Operationally, the biggest migration hazard is the cgroup driver mismatch: kubelet and the container runtime (containerd, CRI-O) must agree on whether they manage cgroups directly (cgroupfs driver) or delegate to systemd (systemd driver), and mixing drivers across the node produces double-accounting or orphaned cgroups that silently stop enforcing limits. Another edge case is that v2 requires all-or-nothing controller delegation — you cannot mount memory accounting without also exposing the unified tree to pid and cpu controllers the way v1 allowed selective mounting, which breaks older monitoring agents that assumed independent controller hierarchies. Swap accounting also behaves differently: memory.max without swap limits configured can let a cgroup page out instead of getting OOM-killed, distorting node memory pressure signals if swap is enabled inconsistently across a fleet.
For platform engineers, the practical consequence is that node bootstrapping, kubelet cgroup-driver configuration, and container runtime version pinning are no longer independent decisions — they form a single compatibility contract that must be validated per kernel version, since cgroup v2 adoption depends on distro defaults (systemd ≥ 245, kernel ≥ 5.8 for full controller parity) and older workloads assuming v1 paths will fail silently rather than loudly.