Linux Control Groups v2: A Deep Engineering Analysis of Modern Resource Isolation
Published: September 2026 | Reading Time: 25 min | Topic: Linux Kernel Resource Management
1. Introduction: Why cgroups v2 Matters
Control groups (cgroups) are a fundamental Linux kernel feature that limits, accounts for, and isolates resource usage (CPU, memory, disk I/O, network) for a collection of processes. Since their introduction by Google engineers Paul Menage and Rohit Seth in 2006 (initially as "process containers"), cgroups have become the bedrock of modern containerization — every Docker container, Kubernetes pod, and systemd service relies on them.
Despite the revolutionary impact of cgroups v1, its design accumulated significant inconsistencies: hierarchical conflicts between controllers, confusing thread-level granularity, and an inability to guarantee safe resource distribution across subtrees. After nearly a decade of community debate, Tejun Heo's radical redesign — cgroups v2 — was merged into Linux kernel 4.5 (March 2016) and became the default in Fedora 31, Ubuntu 22.04, and modern enterprise distributions.
This article provides an exhaustive engineering analysis of cgroups v2: its architectural philosophy, internal mechanics, controller semantics, BPF integration, container runtime applications, and production debugging methodologies.
2. The Architecture Gap: v1 Pain Points vs. v2 Solutions
2.1 v1 Fundamental Design Flaws
The original cgroups implementation suffered from several architectural deficiencies that became increasingly problematic at scale:
- Multiple Hierarchies: Each controller (cpu, memory, blkio) mounted its own独立的 cgroup hierarchy, resulting in a process belonging to different subtree paths across controllers — e.g.,
cpu: /sys/fs/cgroup/cpu/containers/web-servervsmemory: /sys/fs/cgroup/memory/services/web-server. This fragmentation made administration and auditing enormously complex. - Thread-Granularity Inconsistency: While most controllers operated at thread-group (process) level, the cpu controller (via
cpuacct) and blkio allowed per-thread membership, creating ambiguity about whether resource limits applied to a process, a thread, or the entire cgroup. - No Resource Distribution Guarantees: Without the concept of "domain" controllers, v1 could not safely handle resource contention between a parent cgroup's direct children vs. deeper descendants. The
notify_on_releasemechanism was fire-and-forget. - Missing Pressure Metrics: v1 provided only soft limits via
memory.soft_limit_in_bytes with no feedback loop. Administrators had no visibility into reclaim efficiency or thrashing behavior. - Inconsistent Configuration Files: Each controller invented its own configuration file naming conventions (
blkio.weightvscpu.sharesvsmemory.limit_in_bytes), complicating tooling and automation.
2.2 v2 Unified Hierarchy Design
Tejun Heo's redesign enforced single unified hierarchy with three cardinal rules:
| Rule | Constraint | Enforcement |
|---|---|---|
| Single Tree | One cgroup hierarchy for all controllers | Any legacy v1 mount disables v2 (and vice-versa) |
| Process Granularity | Only processes (tid-pid), never threads, may be leaf members | cgroup.threads controls thread migration explicitly |
| Internal Process Constraint | Only leaf cgroups may contain processes; non-leaf cgroups must pass through to leaves | Handled by cgroup.subtree_control and cgroup.max.depth |
The single-hierarchy constraint means a process's resource configuration is unambiguous: /sys/fs/cgroup/system.slice/nginx.service applies all controllers simultaneously to that path.
3. Kernel Internals: The cgroup Subsystem
3.1 Data Structures
At the kernel level, cgroups v2 operates through core data structures defined in linux/cgroup-defs.h:
struct cgroup_subsys_state {
struct cgroup *cgroup; /* associated cgroup */
struct percpu_ref refcnt; /* reference count */
struct cgroup_subsys *ss; /* subsystem pointer */
struct rcu_work destroy_rcu; /* RCU deferred destruction */
/* Controller-specific inline data follows */
};
struct cgroup {
struct cgroup_subsys_state self; /* generic state */
unsigned long flags; /* CGRP_* flags */
int level; /* depth from root */
int max_depth; /* maximum allowed depth */
int nr_descendants; /* count of descendants */
int nr_dying_descendants; /* zombie count */
struct cgroup *parent; /* parent pointer */
struct kernfs_node *kn; /* sysfs/kernfs node */
/* Resource distribution model */
struct cgroup_ppd *root_ppd; /* per-parameters for resource model */
};
3.2 The Resource Distribution Model
The v2 "Resource Distribution Model" solved v1's "no safe defaults" problem. Each non-leaf cgroup configures resource sharing between its children via three parameters:
| Parameter | Controller | Function |
|---|---|---|
| cpu.weight | cpu | Proportional CPU share (default 100, range 1–10,000) |
| memory.min | memory | Hard guarantee — kernel never reclaims below this |
| memory.low | memory | Best-effort protection — reclaim only when system is stressed |
| memory.high | memory | Throttle threshold — kernel throttles allocation near this limit |
| memory.max | memory | Hard limit — triggers OOM killer within cgroup |
| io.weight | io | Proportional I/O share (default 100) |
| io.max | io | Per-device bandwidth/IOPS caps (riops/biops/rbps/wbps) |
| io.latency | io | Per-device latency target (microseconds) |
| pids.max | pids | Maximum process count for cgroup subtree |
The key conceptual advance is the memory tiered protection model: memory.min (reservation) → memory.low (best-effort) → memory.high (throttle) → memory.max (kill). This provides a graceful degradation curve rather than the binary "limit-or-OOM" paradigm of v1.
3.3 The no Internal Process (thread_domain) Rule
v2 enforces that resource domain controllers (cpu, io, memory) and threaded controllers cannot both be enabled on a non-leaf cgroup. This is expressed through cgroup.controls and cgroup.subtree_control interaction:
$ cat /sys/fs/cgroup/cgroup.controllers
cpu io memory pids
$ cat /sys/fs/cgroup/cgroup.subtree_control
cpu io memory
$ echo '+cpu +memory -io' > /sys/fs/cgroup/webserver/cgroup.subtree_control
$ cat /sys/fs/cgroup/webserver/cgroup.subtree_control
cpu memory
4. Controller Deep Dives
4.1 Memory Controller — Stateless but Powerful
The memory controller is frequently the most critical for production workloads. v2's memory controller is stateless by default — it does not maintain consumption accounting unless explicitly requested. The statistics file memory.stat provides:
// memory.stat — comprehensive per-cgroup accounting
anon 16777216 // Anonymous pages (heap, mmap(MAP_ANON))
file 4194304 // Page cache (file-backed mmap)
kernel_stack 131072 // Kernel stack pages
pagetable 8192 // Page table slab
percpu 2048 // Per-CPU allocations
sock 0 // Network socket buffers
shmem 1048576 // Shared memory segments
file_mapped 2097152 // File-mapped pages
file_dirty 524288 // Dirty pages awaiting writeback
file_writeback 0 // Pages under active writeback
inactive_anon 4096 // COLD anonymous pages
active_anon 16773120 // HOT anonymous pages
inactive_file 1024 // COLD file cache
active_file 4184064 // HOT file cache
unevictable 0 // Pages pinned (mlock, zram)
The memory.current exposes total consumption; memory.peak (added in kernel 5.19) captures the historical maximum — invaluable for capacity planning.
4.2 CPU Controller — Weighted Fair Queuing
The v2 CPU controller replaced v1's cpu.shares with a normalized weight semantic. Unlike v1 where shares was relative only when there was contention, v2's cpu.weight operates within a scheduling period (default 100ms, configurable via cpu.cfs_period_us):
Process A: weight=100 → receives ~50% CPU when A+B contend 1:1
Process B: weight=100 → receives ~50% CPU when A+B contend 1:1
Process C: weight=300 → receives ~75% CPU when A+B+C contend 1:1:3
The controller supports two throttling modes via cpu.max (format $MAX $PERIOD):
# Guarantee 1 CPU worth of time per 100ms period
echo "100000 100000" > /sys/fs/cgroup/container/cpu.max
# Burst mode: allow up to 2 CPUs usage, but bounded over 100ms
echo "200000 100000" > /sys/fs/cgroup/container/cpu.max
# Read current usage metrics
cat /sys/fs/cgroup/container/cpu.stat
# usage_usec 847392847
# user_usec 612839284
# system_usec 234553563
# nr_periods 18472 // scheduling periods elapsed
# nr_throttled 892 // periods where cgroup was throttled
# throttled_usec 447392847 // total wall-clock throttled time
4.3 IO Controller — From Throttling to Latency Targeting
The v2 IO controller (io, kernel 5.0+) represented a philosophical shift from v1's bandwidth-centric model to a latency-first design:
- io.latency: Set a target latency (in microseconds) per device; kernel has deployed a feedback-loop mechanism that automatically adjusts throttling to meet SLOs.
- io.weight: Weighted proportional sharing (replaces v1's
blkio.weightwith normalized range 1–10,000). - io.max: Hard per-device throttling in bytes/sec and IOPS.
- io.stat: Detailed per-device request latency histograms.
# Ensure device 8:0 (sda) maintains ≤ 5ms read latency
echo "8:0 target=5000" > /sys/fs/cgroup/db/cgroup/io.latency
# Throttle backup-container to 50MB/s write and 5000 IOPS on sda
echo "8:0 wbps=52428800 wiops=5000" > /sys/fs/cgroup/backup-container/io.max
# Inspect actual latency histogram
cat /sys/fs/cgroup/db/io.stat
# 8:0 rbytes=10737418240 wbytes=2147483648rios=2621440 wios=524288
# 8:0 dbytes=0 dios=0
# 8:0 ravg=2345 wavg=6789 cost=1234 cost.pr=2000 cost.max=8000
4.4 Pids Controller — Fork Bomb Protection
The pids controller is the simplest and oldest controller, protecting against fork bombs:
# Limit a cgroup subtree to 512 total processes
echo "512" > /sys/fs/cgroup/sandbox/pids.max
# Monitor in real-time
cat /sys/fs/cgroup/sandbox/pids.current # current process count
cat /sys/fs/cgroup/sandbox/pids.peak # historical peak (kernel 5.19+)
cat /sys/fs/cgroup/sandbox/pids.events # max: 0 (or count if limit hit)
5. cgroup v2 Controllers with eBPF — Programmable Resource Management
5.1 cgroup-BPF Program Types and Attachment Modes
cgroup v2 extended the BPF subsystem with dedicated program types for resource control and tracing. These attach to cgroup filesystem nodes via BPF multi-link (BPF_F_MULTI) since kernel 5.7:
| BPF Prog Type | Attach Type | Function |
|---|---|---|
| BPF_PROG_TYPE_CGROUP_SKB | BPF_CGROUP_INET_INGRESS / EGRESS | Filter/forward all packets for a cgroup |
| BPF_PROG_TYPE_CGROUP_SOCK | BPF_CGROUP_INET_SOCK_CREATE | Override socket creation per-cgroup |
| BPF_PROG_TYPE_CGROUP_SOCK_ADDR | BPF_CGROUP_INET4_BIND / INET6_CONNECT | Redirect binds/connects before they happen |
| BPF_PROG_TYPE_CGROUP_SOCKOPT | BPF_CGROUP_SETSOCKOPT / GETSOCKOPT | Intercept/modify socket options |
| BPF_PROG_TYPE_CGROUP_SYSCTL | BPF_CGROUP_SYSCTL | Intercept sysctl reads/writes |
| BPF_PROG_TYPE_CGROUP_DEVICE | BPF_CGROUP_DEVICE | Manage device access_whitelist |
| BPF_PROG_TYPE_CGROUP_GETSOCKOPT | BPF_CGROUP_GETSOCKOPT | Read socket options of cgroup processes |
5.2 Case: Per-Service Socket Redirect with cgroup-BPF
A production pattern: transparently redirect all outbound connections from a container's cgroup to a local proxy, without modifying application code:
// cgroup_sock_addr.bpf.c — redirects :80 to :8080 transparently
SEC("cgroup/connect4")
int cgroup_connect4(struct bpf_sock_addr *ctx)
{
// Only intercept IPv4 TCP connections to port 80
if (ctx->user_family != AF_INET || ctx->protocol != IPPROTO_TCP)
return 1;
if (ctx->user_port != bpf_htons(80))
return 1;
// Modify destination to localhost:8080
ctx->user_ip4 = bpf_htonl(0x7F000001); // 127.0.0.1
ctx->user_port = bpf_htons(8080);
return 1; // 1 = allow (with modification)
}
The loader attaches this program to /sys/fs/cgroup/web-proxy/cgroup/bpf/connect4, affecting every process in web-proxy without modifying any service code.
6. Container Runtimes: Docker, containerd, and Kubernetes Integration
6.1 Docker and cgroups v2
Docker uses cgroups (v2 when available) for all resource constraints. Key mapping examples from Docker CLI to cgroups files:
| Docker Flag | v2 cgroup File | Value Written |
|---|---|---|
| --cpus=1.5 | docker/ | 150000 100000 |
| --memory=512m | docker/ | 536870912 |
| --memory-reservation=256m | docker/ | 268435456 |
| --pids-limit=1000 | docker/ | 1000 |
| --blkio-weight=300 | docker/ | 300 |
| --device-read-bps=/dev/sda:1mb | docker/ | 8:0 rbps=1048576 |
# Inspect running container's cgroup (requires cgroupv2 support)
$ docker run -d --name nginx --cpus=0.5 --memory=256m nginx
# Find the container's cgroup path
$ docker inspect --format '{{.State.Pid}}' nginx
12345
$ cat /proc/12345/cgroup
0::/system.slice/docker-abc123.scope
# Inspect applied limits
$ cat /sys/fs/cgroup/system.slice/docker-abc123.scope/cpu.max
50000 100000
$ cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max
268435456
6.2 Kubernetes Pod Resource Model on cgroups v2
Kubernetes maps Pod QoS classes directly to cgroup v2 resource knobs:
| QoS Class | CPU Guarantees | Memory Guarantees | Eviction Priority |
|---|---|---|---|
| Guaranteed | requests == limits (cpu.weight = shares) | requests == limits (memory.min = memory.max) | Last |
| Burstable | requests < limits xss=removed> | requests < limits xss=removed> | Middle |
| BestEffort | No guarantees | No memory.min; largest memory.max permitted | First |
Starting with Kubernetes 1.28 (stable), the InPlacePodVerticalScaling feature (alpha) uses the cgroup v2 memory.low → memory.max reconciliation to perform live container resize without pod restart.
7. Production Tuning: Pressure Stall Information (PSI)
7.1 The PSI Tracepoint
Kernel 4.20 introduced Pressure Stall Information (PSI), the first hardware-agnostic, feedback-driven resource monitoring framework. PSI tracks task stalls into three resource dimensions: CPU, memory, and IO.
The format of memory.pressure, cpu.pressure, and io.pressure:
$ cat /sys/fs/cgroup/webserver/memory.pressure
some avg10=0.00 avg60=12.45 avg300=45.67 total=847392847
# "some" = time when at least ONE task is stalled on memory
# avg10/avg60/avg300 = % of last 10s/60s/300s at least one task stalled
# total = cumulative microseconds since cgroup creation
$ cat /sys/fs/cgroup/webserver/memory.pressure
full avg10=0.00 avg60=5.23 avg300=23.45 total=428392847
# "full" = time when ALL non-idle tasks are stalled simultaneously
7.2 PSI-Driven Autoscaling Architecture
Modern container orchestrators implement reactive hysteresis loops based on PSI metrics:
# pseudocode: PSI-triggered memory expansion
def evaluate_memory_pressure(cgroup_path):
with open(f"{cgroup_path}/memory.pressure") as f:
pressure_stats = parse_psi(f.read())
some_avg60 = pressure_stats['some']['avg60'] # % of last 60s at least one task stalled
if some_avg60 > 60: # >60% of the time, tasks are memory-stalled
current_max = read_memory_max(cgroup_path)
new_max = min(current_max * 1.25, HARD_CEILING)
write_memory_max(cgroup_path, new_max)
cgroup_events.emit("memory.pressure.scale_up", factor=1.25)
elif some_avg60 < 10 xss=removed xss=removed factor=0.83)>
7.3 Earlyoom: Userspace OOM Mitigation
Unlike the kernel OOM killer (which indiscriminately terminates processes based on oom_score), earlyoom and systemd-oomd leverage PSI and memory thresholds:
$ cat /etc/oomd/oomd.conf
[OOM]
DefaultMemoryPressureDurationSec=30 # react only if pressure sustains 30s
MemoryPressureLimit=60 # trigger at 60% "some" pressure over 30s
DefaultMemoryPressureLimit=60% # per-cgroup default
[Slice]
# System-level protection
MemoryMin=1G # Never reclaim below 1GB for system.slice
StartupIOWeight=100
IOWeight=100
# User protection (ssh logins)
user.slice.MemoryHigh=4G
systemd-oomd is the default OOM handler on Fedora, Ubuntu 22.04+, and modern enterprise systems, providing graceful degradation before the kernel-level OOM killer activates.
8. Threaded Controllers and Thread Management
8.1 Threaded Resource Domains
Not all controllers operate at thread-group granularity. The cpu controller (via cpu.qos in io controller; threaded), memory, and pids controllers are resource domain controllers. Additional controllers like perf_event and rdma support "threaded" operation:
| Type | Members | Controllers | Leaf Rule |
|---|---|---|---|
| Resource Domain | Processes only | cpu, memory, io, pids | No processes in subtree if resource domain controllers enabled |
| Threaded | Individual threads | perf_event, rdma, hugetlb | Can coexist with resource domain in same level |
8.2 /cgroup.threads — Fine-Grain Thread Migration
# Move a thread to a different cgroup while keeping the process elsewhere
$ echo 12345 > /sys/fs/cgroup/high-priority/cgroup.threads
# List all thread IDs in a cgroup
$ cat /sys/fs/cgroup/worker/cgroup.threads
12345
12346
12347
# Convert a resource domain controller to threaded (allow threads in subtree)
$ echo "threaded" > /sys/fs/cgroup/worker/cpu.threaded
9. Migration Path: v1 to v2 in Production
9.1 Dual-Transition Strategy
A system can run both v1 and v2 simultaneously, but:
- A controller can only be in ONE hierarchy at a time (if memory controller is in v1, it's inactive in v2)
- The v2 "unified hierarchy" is empty for controllers still claimed by v1
- Partially-populated v2 hierarchies work but aren't ideal
9.2 Enabling cgroups v2
# Check current cgroup version
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime) # ✅ v2 only
# Force v2-only boot (kernel cmdline)
# In /etc/default/grub:
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"
# Then: sudo update-grub && reboot
# After reboot: verify
$ stat -fc %T /sys/fs/cgroup
cgroup2fs
$ cat /sys/fs/cgroup/cgroup.controllers
cpu io memory hugetlb pids rdma misc
9.3 Toolchain Compatibility Matrix
| Component | v2 Status | Since Version |
|---|---|---|
| Docker | ✅ Full support | 20.10 (Aug 2020) |
| containerd | ✅ Full support | 1.4+ (Feb 2021) |
| Kubernetes | ✅ Stable v2 mode | 1.28+ (Aug 2023) |
| systemd | ✅ Default v2 | v248+ (Apr 2021) |
| LXC | ✅ Full support | 4.0+ (2019) |
| runc | ✅ Full support | 1.0.0-rc93 (2021) |
| cadvisor | ✅ V2 metrics | v0.43+ (2021) |
| Prometheus node_exporter | ✅ cgroup v2 collector | 1.3.0 (2022) |
10. Debugging and Observability for cgroups v2
10.1 Systemd-Based Inspection
$ systemd-cgls —list
Control group /:
-.slice
├─system.slice
│ ├─nginx.service
│ │ ├─12345 "nginx: master process /usr/sbin/nginx"
│ │ ├─12346 "nginx: worker process"
│ │ └─12347 "nginx: worker process"
│ └─docker-abc123.scope
│ ├─128845 /bin/sh
│ └─129552 node server.js
└─user.slice
└─user-1000.slice
└─session-3.scope
└─15432 bash
# Deep drill on a specific unit with resource summary
$ systemd-cgtop -m -n 3
Control Group Tasks %CPU Memory I/O/s Input/s Output/s
/system.slice/docker-abc123.scope 4 45.2 256.0M 12.4K 1.2K 8.7K
/system.slice/nginx.service 3 8.3 48.0M 2.1K 145.0 890.0
/system.slice/mysql.service 12 78.1 4.2G 234.5K 15.6K 34.2K
10.2 Advanced BPF-Based Analytics
# Monitor all cgroup migrations in real-time
$ sudo bpftrace -e 'tracepoint:cgroup:cgroup_attach_task {
printf("task %s migrated to cgroup %s by PID %d\n",
args->task->comm, args->cgrp->kn->name, pid);
}'
# Track OOM events (not just kills)
$ sudo bpftrace -e 'kprobe:oom_kill_process {
printf("OOM: killed PID %d (%s), cgroup %s, totalpages=%lu\n",
pid, comm, "try_cgroup_path(args->memcgp)", args->totalpages);
}'
# Count cgroup write operations by file name (detect config churn)
$ sudo bpftrace -e 'kprobe:cgroup_file_write {
@[str(args->path->filename)] = count();
}'
^C
@memory.max: 347
@cpu.max: 128
@cgroup.procs: 89
10.3 Common cgroups v2 Errors and Resolutions
| Symptom | Root Cause | Resolution |
|---|---|---|
| "Dummy Process" in non-leaf cgroup | Process attempted to populate non-leaf cgroup | Move process to leaf; enable cgroup.subtree_control first |
| "Cannot allocate memory" despite memory.max available | Global failure: parent has memory.min set higher than leaf provisions | Verify memory.min chain: parent.min ≤ Σ(child.min) |
| cpu.max throttling but cpu.weight idle | Burst allocation during max period too small; high-priority neighbor | Increase period (e.g., 500000 100000 for 5 CPU equivalent); check sibling weights |
| io.latency ineffective on NVMe with BFQ | BFQ scheduler does not honor io.latency targets; MQ-DEADLINE needed | Switch to MQ-DEADLINE or NONE (none for SCSI/multiqueue bypass) |
| Docker stats report incorrect CPU % | Reading from v1 paths while v2 is active | Restart daemon with --exec-opt native.cgroupdriver=systemd |
11. cgroups v2 in Emerging Paradigms
11.1 eBPF-Centric Security Posture
Projects like Cilium Tetragon leverage cgroup-BPF hooks for runtime security enforcement — tracking process execution, file access, and network activity at the cgroup level without modifying workloads:
# Cilium Tetragon TracingPolicy scoped by cgroup
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: cgroup-socket-audit
spec:
kprobes:
- call: "sock_alloc"
selectors:
- matchBinaries:
- "/usr/bin/curl"
matchArgs:
- index: 0
operator: "Postfix"
values:
- "cgroup/web-frontend/*"
syscall: false
11.2 Virtual Machine Integration (KVM/QEMU)
libvirt and QEMU/KVM map virtual machines to cgroup subtrees. With v2, the IOMMU group isolation and per-VM memory ballooning align with cgroups hierarchy:
# libvirt/QEMU automatically creates:
/sys/fs/cgroup/machine.slice/machine-qemu\x2d1\x2dwebserver.scope/
# Apply IO limits to the VM (vhost-user front-end)
echo "8:0 rbps=104857600 wbps=52428800" \
> /sys/fs/cgroup/machine.slice/machine-qemu*webserver.scope/memory.max
11.3 Windows Subsystem for Linux (WSL2)
WSL2 (since 2022) enabled cgroups v2 in its lightweight utility VM. Users can now run systemd natively inside WSL2, with docker-ce installed directly — Microsoft orchestrates the full cgroups v2 stack on the host side.
12. Best Practices and Anti-Patterns
12.1 Resource Reservation Hierarchy
Define a clean memory.min chain to prevent resource starvation:
memory.min hierarchy (cgroup v2 best practice):
/sys/fs/cgroup/ ← root: no min (everything passes through)
├── system.slice ← memory.min = 1G (kernel + systemd services reserved)
├── user.slice ← memory.min = 512M (user sessions reserved)
└── machine.slice ← memory.min = 0 (VMs get what VMs get)
A well-behaved system under memory pressure:
1. Reclaim from machine.slice (VM guests first)
2. Then user.slice discretionary cache
3. Finally system.slice page cache (but not memory.min)
12.2 Anti-Patterns to Avoid
| Anti-Pattern | Why It's Harmful | Better Approach |
|---|---|---|
| Setting memory.min = memory.max on all cGroups | Eliminates page cache sharing; reduces overall system efficiency | Tune memory.high as soft throttle, leave memory.min for true guarantees |
| Nested <50ms> | Context switch overhead outweighs scheduling precision | Use cpu.weight for granularity, set period=100000 (default) |
| 1:1 Container-to-CPU-Core Pinning Without io.max | CPU-bound container triggers adjacent IO stalls | Always set io.max proportional to cpu.max |
| Ignoring io.stat Feedback After io.latency | Kernel cannot auto-tune if monitoring is not closed-loop | Implement io.latency → io.max reactive adjustment feedback loop |
| Disabling pids.max in untrusted tenant Cgroups | Fork bombs kill the entire host via kernel slab exhaustion | Set pids.max = 32768 as default; whitelist per-tenant |
12.3 Production Configuration Template
#!/bin/bash
# setup-webserver-cgroup.sh — production-ready cgroup v2 bootstrap
# Usage: ./setup-webserver-cgroup.sh webserver 50 256 1000
CGROUP_NAME=$1
CPU_WEIGHT=$2 # 1-10000
MEMORY_MB=$3
PIDS_MAX=$4
CGROUP_V2="/sys/fs/cgroup/${CGROUP_NAME}"
mkdir -p "$CGROUP_V2"
# Enable controllers on parent
echo "+cpu +memory +io +pids" > /sys/fs/cgroup/cgroup.subtree_control
# CPU: weight proportional to siblings, burst to 2 CPUs
echo "200000 100000" > "$CGROUP_V2/cpu.max"
echo "$CPU_WEIGHT" > "$CGROUP_V2/cpu.weight"
# Memory: hard limit + 20% throttle headroom
echo "$(( MEMORY_MB * 1024 * 1024 ))" > "$CGROUP_V2/memory.max"
echo "$(( MEMORY_MB * 1024 * 1024 * 8 / 10 ))" > "$CGROUP_V2/memory.high"
echo "$(( MEMORY_MB * 1024 * 1024 * 5 / 10 ))" > "$CGROUP_V2/memory.low"
# IO: 10MB/s burst cap per NVMe; 5ms latency target
echo "8:0 rbps=10485760 wbps=10485760" > "$CGROUP_V2/io.max"
echo "8:0 target=5000" > "$CGROUP_V2/io.latency"
# Pids: hard process count limit
echo "$PIDS_MAX" > "$CGROUP_V2/pids.max"
# Optional: freeze support for live migration
echo 1 > "$CGROUP_V2/cgroup.freeze" # test freeze workflow
echo "Cgroup ${CGROUP_NAME} provisioned:"
echo " cpu.weight = $(cat $CGROUP_V2/cpu.weight)"
echo " memory.max = $(cat $CGROUP_V2/memory.max | numfmt --to=iec) bytes"
echo " pids.max = $(cat $CGROUP_V2/pids.max)"
echo " subtree_ctrl = $(cat $CGROUP_V2/cgroup.subtree_control)"
13. Conclusion
Linux cgroups v2 represents a fundamental maturation of the kernel resource isolation stack — moving from v1's fragmented and unpredictable model to a unified, weight-based, feedback-driven architecture. Its adoption across Docker, containerd, Kubernetes, and systemd has cemented it as the universal container substrate for the 2020s.
The key engineering insights are:
- Unified hierarchy eliminates v1's multi-tree ambiguity — a process's resource world is a single path
- Tiered memory protection (min → low → high → max) provides a graceful degradation curve
- PSI integration makes resource pressure observable and actionable
- cgroup-BPF hooks enable programmable, zero-code modification security and networking
- Latency-centric io controller shifts IO management from "rate limiting" to "SLO guarantee"
For engineers building or operating container platforms, deep understanding of cgroups v2 is no longer optional — it is the language in which resource scheduling, QoS guarantees, and isolation policies are expressed. Master the model, tune from PSI feedback, and let the kernel enforce the promises you define.
References
- Linux Kernel Documentation:
Documentation/admin-guide/cgroup-v2.rst - Tejun Heo, "cgroup v2: Design and Implementation", Linux Kernel Documentation (2016)
- Linux Kernel 5.19 Release Notes —
pids.peak,memory.peak,nrulécontrollers - systemd.resource-control(5) man page, FreeDesktop.org
- Kubernetes Enhancement Proposal 127: Pod Priority and Preemption
- bpf-helpers(7) man page — cgroup-BPF function catalog

发表评论 取消回复