The control plane makes decisions. Worker nodes execute them. Every container you run lives on a worker node, managed by three components working in concert: the kubelet, kube-proxy, and the container runtime.
1. kubelet — The Node Agent
The kubelet is a systemd service (not a container) running on every worker node. It is the bridge between the Kubernetes API and the container runtime on that machine.
What the kubelet Does
- Watches the API server for Pods assigned to its node (via
spec.nodeName) - Translates Pod specs into container runtime calls (via CRI)
- Manages Pod lifecycle — starts, stops, restarts containers
- Runs probes — liveness, readiness, startup
- Reports status — Pod conditions and node conditions back to the API server
- Manages volumes — mounts/unmounts as Pods start/stop
- Enforces resources — sets cgroup limits for CPU/memory
- Evicts Pods — when node is under resource pressure
PLEG — Pod Lifecycle Event Generator
The kubelet doesn't poll the container runtime constantly. Instead, PLEG periodically (every 1 second) queries the runtime for the list of all containers and compares it to the last known state. Changes generate events that trigger the kubelet's sync loop.
# A common production issue: PLEG is not healthy # This means PLEG took >3min to relist containers. # Causes: overloaded node, slow container runtime, too many containers on one node.
kubelet Pod Admission
Before starting a Pod, the kubelet runs its own admission checks (separate from API server admission):
- Resource check: Does the node have enough allocatable CPU/memory?
- PID pressure: Is the node running out of process IDs?
- Node affinity: Double-check (in case of race conditions)
- Tolerations: Can the Pod tolerate the node's taints?
If admission fails, the Pod stays Pending with a reason like OutOfCpu or OutOfMemory.
Node Registration
When the kubelet starts, it self-registers with the API server — creating or updating its Node object with:
- Capacity (total CPU, memory, max pods)
- Allocatable (capacity minus system-reserved and kube-reserved)
- Node conditions (Ready, MemoryPressure, DiskPressure, PIDPressure)
- Node labels and annotations
# Check allocatable vs capacity: kubectl describe node worker-1 | grep -A5 "Allocatable" # Allocatable: # cpu: 3800m (4 cores - 200m reserved) # memory: 7400Mi (8Gi - 600Mi reserved) # pods: 110 (default max)
Allocatable ≠ Capacity. The kubelet reserves resources for system daemons (--system-reserved) and for itself (--kube-reserved). The scheduler uses allocatable when deciding if a Pod fits.
2. kube-proxy — Service Networking
kube-proxy makes Service objects work. When you create a Service with a ClusterIP, kube-proxy ensures that any traffic sent to that virtual IP gets routed to one of the backing Pods.
Three Modes
| Mode | Mechanism | When to Use |
|---|---|---|
| iptables (default) | Programs iptables rules for NAT | Most clusters < 5000 Services |
| IPVS | Uses Linux IPVS kernel module (L4 load balancer) | Large clusters, need advanced LB algorithms |
| nftables (new, v1.29+) | Uses nftables instead of iptables | Modern kernels, replacing iptables |
How iptables Mode Works
For each Service, kube-proxy creates iptables rules that:
- DNAT packets destined for the ClusterIP to a randomly-selected Pod IP
- Apply probability-based load balancing (each Pod gets 1/N chance)
- Handle session affinity if configured
# Example: Service "web" (10.96.0.100:80) → 3 Pods # kube-proxy generates rules like: -A KUBE-SERVICES -d 10.96.0.100/32 -p tcp --dport 80 -j KUBE-SVC-XXXXX -A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.333 -j KUBE-SEP-AAA -A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.500 -j KUBE-SEP-BBB -A KUBE-SVC-XXXXX -j KUBE-SEP-CCC -A KUBE-SEP-AAA -p tcp -j DNAT --to-destination 10.244.1.5:8080 -A KUBE-SEP-BBB -p tcp -j DNAT --to-destination 10.244.2.3:8080 -A KUBE-SEP-CCC -p tcp -j DNAT --to-destination 10.244.3.7:8080
Why IPVS at Scale?
iptables rules are O(n) — every packet traverses the chain linearly. With 10,000 Services × 10 endpoints = 100,000 rules, this gets slow. IPVS uses hash tables (O(1) lookup) and supports multiple load-balancing algorithms: round-robin, least-connections, weighted, etc.
iptables-save output is hundreds of thousands of lines, kube-proxy takes minutes to sync rules after endpoint changes. Switch with: kube-proxy --proxy-mode=ipvs.
The "No kube-proxy" Option
Modern CNI plugins like Cilium can replace kube-proxy entirely using eBPF. Instead of iptables/IPVS rules, Cilium programs eBPF maps in the kernel — faster lookups, more features (topology-aware routing, Maglev hashing), and easier debugging.
3. Container Runtime & CRI
The kubelet doesn't run containers directly. It speaks the Container Runtime Interface (CRI) — a gRPC API — to a container runtime that does the actual work.
CRI Architecture
CRI API — Two Services
| Service | Methods | Purpose |
|---|---|---|
| RuntimeService | RunPodSandbox, CreateContainer, StartContainer, StopContainer, RemoveContainer | Pod & container lifecycle |
| ImageService | PullImage, ListImages, RemoveImage, ImageStatus | Image management |
containerd vs CRI-O
| Feature | containerd | CRI-O |
|---|---|---|
| Origin | Docker (extracted as standalone) | Red Hat / Kubernetes SIG |
| Scope | General-purpose (also used outside K8s) | Kubernetes-only |
| Used by | EKS, GKE, Docker Desktop, k3s | OpenShift, some kubeadm setups |
| OCI runtime | runc (default), kata, gVisor | runc (default), kata, gVisor |
| Footprint | Slightly larger (more features) | Minimal, K8s-focused |
dockershim. Docker was never a CRI-compliant runtime — the kubelet used a shim layer. Now it speaks CRI directly to containerd or CRI-O. Docker-built images still work perfectly (they're OCI images).
4. Pod Sandbox Lifecycle
A Pod is not just "a group of containers." At the OS level, a Pod is a sandbox — a set of shared Linux namespaces that containers join.
The Pause Container
Every Pod has a hidden "pause" container (also called the "infra container"). It's a tiny process that:
- Creates and holds the Pod's network namespace
- Acts as PID 1 (reaps zombie processes)
- Stays running for the Pod's entire lifetime
All other containers in the Pod join the pause container's namespaces. This is why containers in the same Pod share:
| Shared | Implication |
|---|---|
| Network namespace | Same IP, same ports, localhost communication |
| IPC namespace | Shared memory, semaphores between containers |
| UTS namespace (optional) | Same hostname |
NOT shared: PID namespace (unless shareProcessNamespace: true) | By default, each container has its own PID 1 |
| NOT shared: Filesystem | Must use emptyDir volumes to share files |
Full Pod Startup Sequence
Pod Shutdown Sequence
When a Pod is deleted, the kubelet executes a graceful shutdown:
- Pod enters
Terminatingstate - Pod is removed from Service endpoints (stops receiving traffic)
preStophook runs (if defined)SIGTERMsent to PID 1 in each container- Wait up to
terminationGracePeriodSeconds(default: 30s) - If still running:
SIGKILL - CNI: detach network
- Remove sandbox
preStop hook: lifecycle.preStop.exec.command: ["sleep", "5"] to give in-flight requests time to complete.
preStop: sleep 5 pattern exists — it gives time for endpoint propagation.
5. Eviction Manager — When the Node Runs Out
The kubelet monitors node resources and evicts Pods when thresholds are breached:
| Signal | Default Threshold | Condition Set |
|---|---|---|
memory.available | < 100Mi | MemoryPressure |
nodefs.available | < 10% | DiskPressure |
imagefs.available | < 15% | DiskPressure |
pid.available | < varies | PIDPressure |
Eviction Order (QoS-based)
- BestEffort Pods (no requests/limits) — evicted first
- Burstable Pods exceeding their requests
- Guaranteed Pods (requests = limits) — evicted last, only in extreme pressure
Node Status — What the Control Plane Sees
kubectl describe node worker-1 Conditions: Type Status Reason ---- ------ ------ Ready True KubeletReady MemoryPressure False KubeletHasSufficientMemory DiskPressure False KubeletHasNoDiskPressure PIDPressure False KubeletHasSufficientPID # If Ready becomes Unknown (kubelet stops reporting): # → Node controller waits 40s, then marks NotReady # → After pod-eviction-timeout (5min), Pods are evicted
Summary
| Component | Runs As | Primary Job | Failure Impact |
|---|---|---|---|
| kubelet | systemd service | Pod lifecycle, reporting | Node goes NotReady, no new Pods |
| kube-proxy | DaemonSet (usually) | Service → Pod routing rules | Services stop working on that node |
| containerd | systemd service | Pull images, run containers | No containers start, PLEG fails |
| pause container | Per-Pod infra container | Hold namespaces | Pod's network dies |
📝 Quiz: Worker Node Internals
Q1: What is the pause container and why does every Pod have one?
Q2: A Pod has containers A and B. Container A listens on port 8080. How does container B reach it?
localhost:8080. Since both containers share the same network namespace (via the pause container), they share the same network stack and can communicate over localhost.Q3: You see "PLEG is not healthy" in kubelet logs. What does this mean and what do you check?
crictl ps). (2) Node I/O and CPU pressure. (3) Number of Pods/containers on that node — may be too many.Q4: During a rolling deployment, users see brief connection resets. What's likely happening and how do you fix it?
preStop: sleep 5 hook to delay shutdown, giving endpoint propagation time to complete.Q5: Why does Kubernetes use CRI (an abstraction) instead of calling containerd directly?
Q6: A node has 4 CPU cores. --kube-reserved=500m and --system-reserved=500m are set. How much CPU is allocatable for Pods?