arthrop0d

Brief articles on varied topics.

Cgroups v2 Resource Control for Containers

containersoperating systemsperformance

Cgroups v2, short for control groups version 2, are Linux kernel hierarchies that group processes and apply resource accounting and limits to them. A container runtime uses a cgroup to control how much CPU and memory a container can consume and how many processes it may create.

The problem cgroups solve

Without resource controls, processes on the same host compete largely as individuals. A busy container can consume CPU while other workloads wait, fill memory until the host starts killing unrelated processes, or create processes rapidly enough to exhaust a system-wide limit.

A cgroup gives the host an enforceable boundary around a workload. It is not a virtual machine: the processes still use the host kernel and compete for the same physical resources. Instead, the kernel tracks which cgroup each process belongs to and applies controller-specific rules to that group.

This is the mechanism behind container settings such as a CPU limit, a memory limit, or a Kubernetes resources.limits.memory value. The runtime usually creates and configures the cgroup; the application may only encounter the result as throttling, an allocation failure, or a failed fork.

One hierarchy, many groups

In cgroups v2, the host normally exposes one unified hierarchy: a tree of directories representing cgroups. A parent can contain child cgroups, and resource usage normally rolls up from each child into its ancestors.

For example:

/sys/fs/cgroup/
└── containers/
    ├── api/
    └── worker/

The kernel exposes control files in these directories. A runtime might place the container's processes in api/cgroup.procs, then write values to files such as cpu.max or memory.max. The files are a kernel interface, not ordinary configuration files: writing one changes live accounting or enforcement.

A process belongs to one cgroup in this hierarchy. Its descendants inherit the applicable boundaries, so a child cannot escape a parent's effective resource limit. Sibling groups compete according to their settings. The hierarchy also lets an operator account for a complete service group while applying finer limits to individual children.

Controllers are the pieces that implement particular policies. They must be enabled for a subtree through the parent's cgroup.subtree_control; common controllers include cpu, memory, and pids.

A design rule in v2 is the no-internal-process constraint: for normal resource-controlled groups, a cgroup that directly contains processes is not also used as the distribution point for child groups. Processes are placed in leaf groups, while parent groups organize and distribute resources to those children. This avoids ambiguity over whether a parent's own processes should compete with its descendants.

CPU: share time or cap it

The CPU controller operates on CPU time scheduled by Linux. It provides both relative sharing and an absolute ceiling.

A quota is not a reservation. A container with a 20% limit may use more than 20% of one CPU when the configured period allows it, but once its quota is spent, its runnable tasks are throttled—temporarily prevented from using CPU until the next period. On a multi-core host, the limit is still expressed as aggregate CPU time: 200000 100000 is approximately two CPUs, not 20% of every CPU.

cpu.stat reports usage and scheduling outcomes, including user and system CPU time and throttled time. These counters explain why an application can have idle-looking processes yet show latency: it may be waiting for its cgroup's next CPU period.

Memory: reclaim, pressure, and failure

The memory controller accounts for memory charged to a cgroup, including application memory and relevant filesystem cache. Shared pages and kernel-managed objects can have accounting details that make totals differ from an application's own allocator statistics, so a language runtime's heap metric is not the same as cgroup memory usage.

Important files include:

Swap has separate policy, commonly configured with memory.swap.max. Therefore a container can hit its memory limit even if the host has free swap, depending on that setting.

When a hard limit is exceeded, the kernel does not simply reject every allocation immediately. It first attempts reclaim, which can produce latency and filesystem activity. If reclaim cannot satisfy an allocation, an out-of-memory kill may terminate a process in the affected cgroup. The resulting log often says OOMKilled, cgroup out of memory, or reports an allocation failure rather than a clean application-level error.

PIDs: limit process creation

The pids controller limits the number of tasks in a cgroup. Here, a task means a kernel-schedulable process or thread, so a program that creates many threads can consume the limit too.

When a process tries to create another process or thread and doing so would exceed pids.max, the creation operation fails, commonly with EAGAIN. This is useful protection against fork bombs and runaway worker creation. It does not kill existing tasks or measure CPU and memory use; it limits the ability to add more tasks.

Together, these controllers give a container three different kinds of boundary: CPU controls how much execution time it can obtain, memory controls how much charged memory it can retain, and pids controls how many concurrent kernel tasks it can create. The hierarchy makes those boundaries composable, while the accounting files show whether a workload is using resources, being throttled, or failing against a limit.

← All articles