Chapter 21: cgroups, systemd and Resource Management
Chapter 21: cgroups, systemd and Resource Management
Control Groups (cgroups) are the foundational Linux kernel mechanism for metering, allocating, limiting, and isolating system resources (CPU, Memory, I/O, PIDs, RDMA) across groups of processes. In modern Linux and container runtimes, cgroups v2 provides a unified hierarchy that coordinates resource limits cleanly with systemd.
This chapter explores the differences between cgroups v1 and v2, CPU CFS quota throttling, memory pressure limits, I/O bandwidth control, systemd unit isolation, and process limits (ulimit).
cgroups v1 vs v2: Architecture Differences
In cgroups v1, every resource controller (cpu, memory, blkio, net_cls) had its own independent hierarchy:
- A process could be in group
/webin the CPU hierarchy, but in group/dbin the memory hierarchy. - This made coordinated resource management impossible: the kernel could not correlate buffered I/O writes with memory page allocation because
blkioandmemorywere in completely separate hierarchies. - cgroups v2 (Unified Hierarchy): Standardized on a single unified process tree mounted at
/sys/fs/cgroup. A process belongs to exactly one cgroup, and controllers operate coherently across CPU, Memory, and I/O simultaneously.
┌────────────────────────────────────────────────────────────────────────┐
│ CGROUPS v2 UNIFIED TREE │
│ │
│ /sys/fs/cgroup (Root) │
│ ├── system.slice │
│ │ ├── nginx.service (cpu.max, memory.max, io.max) │
│ │ └── postgresql.service │
│ ├── user.slice │
│ │ └── user-1000.slice │
│ └── kubepods.slice (Container Pods) │
└────────────────────────────────────────────────────────────────────────┘
CPU Controllers and Throttling
Under cgroups v2, CPU allocation is governed by weights and quotas:
Proportional Sharing (
cpu.weight): Priority weight from 1 to 10000 (default 100). When CPU is saturated, cores are divided proportionally to weights. When idle, any cgroup can consume 100% of available CPU.Hard Quotas (
cpu.max): Restricts maximum CPU consumption over a period. Format:$QUOTA $PERIODin microseconds:# Limit cgroup to 2.5 CPU cores (250,000µs quota per 100,000µs period) echo "250000 100000" | sudo tee /sys/fs/cgroup/workload/cpu.max
The CPU Throttling Latency Trap
When a multi-threaded application exceeds its quota within the first 20ms of a 100ms period, the kernel immediately deschedules all its threads for the remaining 80ms! This causes catastrophic p99 tail latency spikes:
# Check how many periods a cgroup has been throttled
grep nr_throttled /sys/fs/cgroup/system.slice/nginx.service/cpu.statModern kernels (5.14+) include CPU Bursting (cpu.max.burst), allowing tasks to borrow unused quota from previous periods to absorb micro-bursts without triggering throttling stalls.
Memory Controllers: Limits and Pressure
cgroups v2 provides granular memory boundaries:
memory.min: Hard memory protection. If memory drops below this threshold, the kernel will never reclaim memory from this cgroup.memory.low: Soft memory protection. Reclaim occurs only if memory cannot be reclaimed from other unprotected cgroups.memory.high: Throttle limit. When exceeded, the processes in the cgroup are throttled and forced into asynchronous direct reclaim to drop pages.memory.max: Hard maximum. If exceeded and reclaim fails, the Out-Of-Memory (OOM) killer terminates processes in the cgroup.
# Set soft limit to 4GB and hard maximum to 8GB
echo 4294967296 | sudo tee /sys/fs/cgroup/workload/memory.high
echo 8589934592 | sudo tee /sys/fs/cgroup/workload/memory.maxI/O Controllers: Throttling and Prioritization
cgroups v2 coordinates page cache writeback with block device throttling:
# Throttle device 259:0 (NVMe) to 5000 read IOPS and 50 MB/s write bandwidth
echo "259:0 riops=5000 wbps=52428800" | sudo tee /sys/fs/cgroup/workload/io.maxBecause cgroups v2 unifies memory and I/O, dirty pages generated by this cgroup are attributed to its I/O budget during writeback, preventing rogue background tasks from swamping storage.
systemd Resource Controls and Service Isolation
In production, avoid interacting with /sys/fs/cgroup directly; configure resource limits via systemd unit files or drop-in overrides:
# /etc/systemd/system/api-service.service.d/override.conf
[Service]
# CPU Limits: allocate 4 cores with burst capability
CPUAccounting=true
CPUQuota=400%
CPUWeight=500
# Memory Limits: throttle at 14GB, hard OOM limit at 16GB
MemoryAccounting=true
MemoryHigh=14G
MemoryMax=16G
# I/O Limits: prioritize storage requests
IOAccounting=true
IOWeight=800
IOReadIOPSMax=/dev/nvme0n1 10000
# Tasks Limit: prevent fork bombs
TasksMax=4096Reload and apply:
sudo systemctl daemon-reload
sudo systemctl restart api-serviceulimits and Resource Limits
Before cgroup limits are reached, the kernel enforces per-process resource limits (ulimit):
| Limit | Description | Production Recommendation |
|---|---|---|
nofile |
Max open file descriptors (sockets, files) | Set to at least 65536 or 1048576 for servers. |
nproc |
Max process/thread count for user | Set to unlimited or generous limits for services. |
memlock |
Max memory locked into RAM (mlock()) |
Set to unlimited for databases using HugePages or DPDK. |
In systemd services, set these directly in unit files (LimitNOFILE=1048576, LimitMEMLOCK=infinity), bypassing legacy /etc/security/limits.conf limits entirely.