DevOpsIndex

Node Shutdown and Eviction


1. Graceful Node Shutdown (kubelet-initiated)

When a node receives a shutdown signal (systemd shutdown.target), kubelet intercepts it via a systemd inhibitor lock and evicts pods before allowing the OS to proceed.

sequenceDiagram
    participant SD as systemd
    participant K as kubelet
    participant A as API Server
    participant P as Pods

    Note over K: kubelet registers inhibitor lock on startup
    SD->>K: Shutdown signal (reboot/poweroff)
    K->>K: Acquire inhibitor lock (blocks actual shutdown)
    K->>A: Taint node: node.kubernetes.io/not-ready:NoExecute
    Note over K: Phase 1: evict non-critical pods
    K->>P: SIGTERM --> wait grace period --> SIGKILL
    Note over K: Phase 2: evict critical pods
    K->>P: SIGTERM --> wait critical grace period --> SIGKILL
    K->>K: Release inhibitor lock
    SD->>SD: Shutdown proceeds

kubelet configuration:

# /var/lib/kubelet/config.yaml
shutdownGracePeriod: 30s              # total time for all pods to terminate
shutdownGracePeriodCriticalPods: 10s  # of the above, reserved for critical pods

Timeline with defaults:

t=0s:  Shutdown signal received
t=0s:  kubelet acquires inhibitor, begins eviction
t=0s:  Non-critical pods get SIGTERM (20s window = 30s - 10s)
t=20s: Non-critical pods get SIGKILL (if still running)
t=20s: Critical pods get SIGTERM (10s window)
t=30s: Critical pods get SIGKILL
t=30s: Inhibitor released, OS shuts down
# Check kubelet config for shutdown settings
ssh <node>
cat /var/lib/kubelet/config.yaml | grep -i shutdown

# Verify inhibitor lock is registered
systemd-inhibit --list | grep kubelet

# Watch eviction events during shutdown
kubectl get events --field-selector reason=Evicted -A -w

2. Pod Eviction Ordering

During node shutdown (and kubelet-initiated eviction), pods are evicted in priority order:

flowchart TD
    BE["BestEffort pods<br/>(no requests/limits)<br/>evicted first"] --> BURST
    BURST["Burstable pods<br/>(requests set, usage > requests)<br/>evicted second"] --> GUAR
    GUAR["Guaranteed pods<br/>(requests == limits)<br/>evicted third"] --> CRIT
    CRIT["Critical pods<br/>(PriorityClass >= 2000000000)<br/>evicted last"]

    style BE fill:#e74c3c,color:#fff
    style BURST fill:#e67e22,color:#fff
    style GUAR fill:#27ae60,color:#fff
    style CRIT fill:#2980b9,color:#fff

PriorityClass determines "critical":

PriorityClass Value Examples
system-node-critical 2000001000 kubelet, kube-proxy, CNI
system-cluster-critical 2000000000 CoreDNS, metrics-server
(user pods) < 1000000000 Your workloads
# Protect your pod from early eviction
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
---
spec:
  priorityClassName: high-priority

3. Node Drain (Voluntary Eviction)

kubectl drain is the controlled, human-initiated version of node shutdown. Used for maintenance, node upgrades, and cluster autoscaler scale-down.

flowchart TD
    DRAIN["kubectl drain NODE_NAME"] --> CORDON
    CORDON["kubectl cordon NODE_NAME<br/>node.spec.unschedulable = true<br/>No new pods scheduled here"] --> EVICT
    EVICT["Evict each pod via Eviction API<br/>(respects PodDisruptionBudget)"] --> WAIT
    WAIT["Wait for each pod to terminate<br/>(terminationGracePeriodSeconds)"] --> DONE
    DONE["Node ready for maintenance"]

    EVICT -->|"PDB would be violated"| BLOCK["Drain blocked<br/>until PDB allows eviction"]
    BLOCK --> EVICT
# Full drain command
kubectl drain <node-name> \
  --ignore-daemonsets \        # DaemonSet pods can't be evicted (managed by DS controller)
  --delete-emptydir-data \     # evict pods using emptyDir (data will be lost)
  --timeout=5m \               # fail if drain takes longer than 5 min
  --grace-period=30            # override pod terminationGracePeriodSeconds

# Uncordon after maintenance
kubectl uncordon <node-name>

# Check what's blocking drain
kubectl get pdb -A             # list PodDisruptionBudgets
kubectl describe pdb <name>    # check minAvailable vs current ready pods

Why drain blocks:

PDB: minAvailable=2, current ready=2 → drain would take 1 → violates PDB → blocked
Fix: wait for replacement pod to come up on another node, then drain proceeds

4. Non-Graceful Node Shutdown

If a node dies abruptly (power cut, kernel panic, kill -9 kubelet), the node object stays in the cluster but kubelet is gone. Pods on that node stay in Terminating forever — the API server is waiting for kubelet to confirm deletion, which never comes.

# Pods stuck Terminating on dead node
kubectl get pods --field-selector spec.nodeName=<dead-node>

# Old manual fix (still works)
kubectl delete pod <pod> --grace-period=0 --force

# Better: delete the node object
kubectl delete node <dead-node>
# → node lifecycle controller adds not-ready/unreachable taint
# → pods get evicted after tolerationSeconds

Kubernetes 1.28+ — out-of-service taint (GA):

# Manually taint the dead node
kubectl taint node <dead-node> \
  node.kubernetes.io/out-of-service=nodeshutdown:NoExecute

# Effect: StatefulSet pods and pods with PVCs are immediately force-deleted
# and rescheduled on healthy nodes — even without kubelet confirmation
# This was the #1 StatefulSet recovery pain point before this feature

5. Node Lifecycle Taints

The node lifecycle controller automatically adds taints when conditions change. These taints trigger pod eviction via tolerationSeconds.

Condition Taint key Auto-tolerationSeconds on pods
NotReady node.kubernetes.io/not-ready 300s (default)
Unreachable node.kubernetes.io/unreachable 300s (default)
MemoryPressure node.kubernetes.io/memory-pressure
DiskPressure node.kubernetes.io/disk-pressure
PIDPressure node.kubernetes.io/pid-pressure
NetworkUnavailable node.kubernetes.io/network-unavailable

tolerationSeconds controls how long a pod tolerates a taint before eviction:

# Default toleration on all pods (injected by admission controller):
tolerations:
- key: node.kubernetes.io/not-ready
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 300   # stay on not-ready node for 5 min before evicted

# For fast failover (stateless services):
tolerations:
- key: node.kubernetes.io/not-ready
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 30    # evict in 30s

# For sticky workloads (StatefulSets, cache):
tolerations:
- key: node.kubernetes.io/not-ready
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 600   # give node 10 min to recover before moving pod
# Check node conditions and taints
kubectl describe node <node> | grep -A10 Taints
kubectl describe node <node> | grep -A20 Conditions

# Watch node status changes
kubectl get nodes -w

# See eviction events
kubectl get events -A --field-selector reason=Evicted
kubectl get events -A | grep "Evicting\|Evicted\|Killing"

6. Kubelet Eviction Thresholds

kubelet watches node resources and evicts pods before the node itself runs out.

# kubelet config: /var/lib/kubelet/config.yaml
evictionHard:
  memory.available: "200Mi"     # evict when < 200Mi free
  nodefs.available: "10%"       # evict when < 10% disk
  nodefs.inodesFree: "5%"       # evict when < 5% inodes
  imagefs.available: "15%"      # evict when < 15% image filesystem

evictionSoft:
  memory.available: "500Mi"     # warn when < 500Mi
evictionSoftGracePeriod:
  memory.available: "1m30s"     # only evict if soft threshold exceeded for 90s

evictionMinimumReclaim:
  memory.available: "100Mi"     # reclaim at least 100Mi per eviction round

Eviction order within a QoS class: pods using the most resources above their requests are evicted first.