Skip to main content
RunBook Academy

KubernetesI · Container and Distributed Systems FoundationsContainer and distributed systems foundations

Linux namespaces and cgroups — the kernel primitives Pods inherit

Foundation⏱ ~18 minkubectlunsharesystemd-cgls

What you'll learn

  • Identify the seven Linux namespace types and what each one isolates
  • Explain how cgroups v2 control CPU, memory, PIDs and I/O, and how kubelet wires them to Pods
  • Reason about what Pods share (kernel, network namespace, optionally PID) vs what each container gets privately
  • Identify the cases where Kubernetes abstractions leak through the underlying kernel primitives

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

Kubernetes did not invent container isolation. It composes Linux namespaces and cgroups into a unit called a Pod. Every Pod behaviour that surprises an operator — shared loopback, shared /etc/hosts, shared process IDs — is downstream of a namespace being shared or private. Every resource limit that works — CPU throttling, memory OOM, PIDs cap — is downstream of a cgroup controller. This lesson covers the kernel layer Kubernetes sits on.

The seven namespace types

Linux has seven namespace types. A process can hold one of each:

NamespaceFlag to unshare / cloneWhat it isolates
MountCLONE_NEWNSFilesystem mount points
PIDCLONE_NEWPIDProcess IDs (visible PIDs start at 1)
NetworkCLONE_NEWNETNetwork interfaces, routing table, sockets
IPCCLONE_NEWIPCSystem V IPC, POSIX message queues
UTSCLONE_NEWUTSHostname, NIS domain name
UserCLONE_NEWUSERUIDs and GIDs (UID 0 inside may be UID 100000 outside)
CgroupCLONE_NEWCGROUPcgroup root view (added in kernel 6.7; previous kernels hide the cgroup view)

When runc (or crun) starts a container, it creates the namespace set requested by the OCI configuration. By default, the runtime creates a fresh namespace for each container. The Pod is the unit where the orchestrator decides which namespaces are shared.

flowchart TB
    subgraph Host["Linux host (kubelet)"]
        subgraph Pod["Pod sandbox (pause container)"]
            N["Network namespace ns1"]
            M["Mount namespace ns1"]
            U["UTS namespace ns1"]
            I["IPC namespace ns1"]
            P1["PID namespace ns1"]
            C1["Container 1 (app)"]
            C2["Container 2 (sidecar)"]
            C3["Container 3 (init)"]
            C1 --> N
            C2 --> N
            C3 --> N
            C1 --> M
            C2 --> M
            C1 --> P1
            C2 --> P1
        end
        Cg["cgroup slice: kubepods/pod-xxx/container-yyy"]
    end

In the diagram:

  • The Pod sandbox is implemented as the pause container, a tiny process whose only job is to hold the shared namespaces open. Containers in the Pod join the sandbox’s namespaces via the runtime.
  • All containers in the Pod share the network namespace by default: same loopback, same eth0, same IP, same ports. Two containers cannot bind to the same port.
  • All containers share the mount namespace only when they use volumeMounts from the same volume, or when they explicitly declare shareProcessNamespace: true in the Pod spec.
  • Containers have private PID namespaces by default since Kubernetes 1.14+. With shareProcessNamespace: true, all containers see each other’s processes (and PID 1 is the sandbox).

What is shared in a Pod, by default

A standard Pod (no shareProcessNamespace, no hostNetwork, no hostPID) shares these namespaces:

  • Network (so containers can talk over localhost)
  • IPC (so they can share POSIX / SysV IPC primitives)
  • UTS (so they have the same hostname)
  • PID with shareProcessNamespace: true only — by default each container has a private PID namespace and PID 1 is the container’s main process
  • Cgroup (added in kernel 6.7; previously hidden)

Containers always have their own:

  • Mount (the root filesystem is private; volumes are the bridge)
  • User (UID 0 inside the container is mapped to a non-zero UID on the host via user namespaces, when configured)
  • PID by default
sequenceDiagram
    autonumber
    participant Kub as kubelet
    participant Pause as pause (sandbox)
    participant Net as netns
    participant Runc as runc
    participant C1 as Container 1
    participant C2 as Container 2

    Kub->>Pause: Start with empty namespaces (net/mount/UTS/IPC)
    Kub->>Runc: Spawn container 1
    Runc->>Net: Join sandbox's netns
    Runc->>C1: Create container 1 with its own mount/PID
    Kub->>Runc: Spawn container 2
    Runc->>Net: Join sandbox's netns (same IP, same ports)
    Runc->>C2: Create container 2 with its own mount/PID
    Note over C1,C2: Both see same loopback, same eth0 IP, same hostname
    Note over C1,C2: Both have private /, private PIDs

The pod shareProcessNamespace toggle

shareProcessNamespace: true in the Pod spec makes all containers share one PID namespace. With it enabled:

  • ps aux inside any container shows every other container’s processes.
  • PID 1 is the pause process. Sending SIGTERM to PID 1 sends SIGTERM to the Pod — a classic source of “why did my whole Pod restart when I only asked for one container to stop?” surprises.
  • A container with no /proc mount cannot enumerate processes even when the namespace is shared. The containerd shim and /proc/$pid/root give debug access, but only if the debug container is privileged enough.

cgroups v2: where limits actually live

Namespaces give isolation; cgroups give resource control. Kubernetes 1.34 supports cgroups v2 (unified hierarchy) on Linux hosts running kernel 5.8+ and kubelet configured with --cgroup-driver=systemd (the modern recommendation).

A Pod’s containers are placed under a cgroup slice:

kubepods.slice/
  kubepods-pod<UID>.slice/
    kubepods-pod<UID>-cri-containerd-<containerID>.scope
      cpu.max        = 500m 1000m      # 0.5 CPU quota, 1 CPU period
      memory.max     = 536870912        # 512 MiB hard limit
      memory.high    = 402653184        # 384 MiB soft limit (throttle first)
      pids.max       = 1024             # process count cap
      io.max         = ...              # block I/O bps/iops (per device)

The kubelet translates the Pod’s resources.requests and resources.limits into these files:

Pod fieldCgroup fileBehaviour
resources.requests.cpucpu.weight (proportional)Used by scheduler for placement; weighted share
resources.limits.cpucpu.max (quota, period)Hard cap; container is throttled when exceeded
resources.requests.memory(advisory)Used by scheduler for placement
resources.limits.memorymemory.maxHard cap; OOMKill when exceeded

How kubelet wires Pod spec to kernel

When kubelet gets a new Pod from the API server, it walks the spec and asks the runtime to:

  1. Create the sandbox (pause) with empty mount/network/IPC/UTS namespaces.
  2. Apply the Pod’s cgroupfs configuration: create kubepods.slice/kubepods-pod<UID>.slice with the sum of all container requests as the floor.
  3. For each container:
    • Pull the image (via CRI to containerd).
    • Join the sandbox’s netns, ipcns, utsns.
    • Apply the container’s resources.limits to the container’s own cgroup slice.
    • Start the container’s entrypoint.

The container’s cgroup is under the Pod’s cgroup, so a noisy container can be throttled without starving its siblings — but the Pod as a whole can still be OOMKilled if its memory sum exceeds the node’s available memory and the Pod’s memory.max is set.

Where the abstractions leak

The Pod is a tidy abstraction. The kernel underneath is not.

  • PID 1 is real. When a container’s PID 1 process dies, the kernel sends SIGKILL to every other process in the container’s PID namespace and tears down the container. Without an init process that reaps zombies, the container fills with zombies and the runtime may report the container as unhealthy. The Linux course covers PID 1 reaping in depth.
  • Sharing hostNetwork: true is dangerous. The container joins the host’s network namespace, not the Pod’s. It sees every interface, every listening socket, every iptables rule. Production should never set hostNetwork: true except for network agents that explicitly require it (CNI, kube-proxy in some configurations).
  • hostPID: true lets the container see every process on the host. Even more dangerous than hostNetwork. Use only for debugging.
  • User namespace remapping (hostUsers: false in 1.34+) gives the container a UID range that maps to non-zero host UIDs. If the container breaks out, it does not get root. Production clusters should enable this wherever the runtime supports it.
  • cgroup limits are not promises. The kernel enforces them, but the kernel can also OOMKill a container whose memory.max was set higher than its actual usage pattern triggered (e.g., a sudden spike from an upstream service). Production requires both limits and observability of memory.usage, memory.events, and cpu.stat.

Inspecting namespaces from inside a Pod

A debugging pattern that catches many “why is this Pod misbehaving” tickets is to use an ephemeralContainer (debug pod) that joins the Pod’s namespaces:

# Substitute your own values before running:
POD=web-5f9c7d8b6c-2xk9p
CONTAINER=nginx          # the container in that Pod to join

kubectl debug -it "pod/$POD" --image=busybox --target="$CONTAINER"

Inside, you can read /proc/1/cgroup, ls -la /proc/1/ns/, and confirm which namespaces are shared and which are private. A second pattern is nsenter from a privileged debug Pod on the node:

# Container ID from `crictl ps` on this node:
CONTAINER_ID=a3f1c9e2b7d84f0a9c6e5b3d1f2a8c7e4b6d9f0a1c3e5b7d9f2a4c6e8b0d1f3a

PID=$(crictl inspect "$CONTAINER_ID" | jq .info.pid)
nsenter -t "$PID" -n ip addr
nsenter -t "$PID" -m ls /
nsenter -t "$PID" -p ps -ef

These tools are operator-grade. Production runbooks for “container cannot reach service X” should default to nsenter -n from a debug Pod on the same node, not to speculating about NetworkPolicy.

Cross-course references

  • The Linux course part LXXVIII-Linux-Containers covers namespaces and cgroups from the kernel side; this lesson shows how Kubernetes composes them.
  • The Linux course part XXX-Linux-Capabilities covers the capability model; Pods strip capabilities by default and re-add only what the workload declares — this is the same model Kubernetes inherits.
  • The Docker course part XXXIX-Docker-Resources covers cgroups v2 resource controls for Docker; the Kubernetes cgroup model is the same primitives with the orchestrator writing the values.
  • The Observability course part IX-Observability-Exporters covers node_exporter metrics that surface cgroup usage (container_cpu_*, container_memory_*) into the monitoring stack.

Quiz

Knowledge check · 4 questions

  1. Q1. In a Pod with two containers and no special Pod-level toggles, which namespaces do the two containers share by default?

  2. Q2. The `pause` container in a Pod can be safely omitted or replaced with a custom image because its only role is to hold the shared namespaces open.

  3. Q3. An application team reports that their Pod is slow under load. CPU `throttling` metrics show non-zero `throttled_periods`, and `cpu.stat` reports `nr_throttled` increasing. They are convinced the limit is wrong. Walk through what the kernel is doing and what the right production response is.

    Pod spec excerpt: ```yaml resources: requests: cpu: 250m memory: 256Mi limits: cpu: 500m memory: 512Mi ``` Node: 32 cores, kubelet systemd cgroup driver, kernel 6.6. Metrics from node_exporter / cAdvisor: ``` container_cpu_cfs_throttled_periods_total{container="app"} 18204 container_cpu_cfs_periods_total{container="app"} 20000 container_cpu_usage_seconds_total{container="app"} 9400 # Effective usage ~0.47 cores, but limit is 500m — saturation is real ``` Application logs: ``` 2026-08-15T12:01:01Z request latency p99 = 480ms (target: 200ms) 2026-08-15T12:01:01Z worker pool size: 8 2026-08-15T12:01:01Z queue depth: 240 ```

  4. Q4. Explain the relationship between a Pod's `resources.requests.cpu` and the cgroup file `cpu.weight`, and between `resources.limits.cpu` and `cpu.max`. What does the kernel enforce, and what does the scheduler use?

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Treat the kernel primitives as the source of truth; Kubernetes abstractions are conveniences on top.
  • Always set both requests and limits for CPU and memory. Requests without limits let a Pod starve its neighbours; limits without requests give the scheduler no signal for placement.
  • Default shareProcessNamespace: false unless a sidecar pattern truly requires process visibility.
  • Use ephemeralContainer and nsenter from a debug Pod for namespace-level diagnosis; do not redeploy with hostNetwork or hostPID to “fix” a debugging problem.
  • Run a kernel with cgroups v2 and the systemd driver on every node; mixed-mode clusters are operationally fragile.