Skip to content
nayem.talukder
← war stories

The Afternoon the Cluster Ate Itself

Seven workers hit disk pressure, and every standard fix made it worse: restarted agents flapped back NotReady in minutes, kubelets hung silently for 30+ minutes on startup, and Harbor — the registry every recovering pod needed — went down with the cluster it runs on.

Severity
SEV-1
Status
Resolved
Duration
~4 hours
Date
May 12, 2026

systems: RKE2 · Longhorn · Harbor · Argo CD · kubelet 4 min read

Problem

On the afternoon of May 12, seven worker nodes on our self-managed RKE2 QA cluster went under disk pressure at nearly the same time. The kubelet started evicting pods, the evicted pods rescheduled onto the surviving nodes and pushed them toward the same edge, and within the hour the cluster was in a full eviction cascade.

Then the part that turned an incident into a siege: Harbor — the in-cluster registry — lost its scheduling home. Every pod trying to recover needed to pull an image from a registry that was down because of the same incident it was needed to fix.

Context

The cluster ran a rapid-QA tier: 28 namespaces of full product environments, in-cluster CI (Tekton building through Harbor), GitOps via Argo CD, and Longhorn for storage — with Longhorn's data path sharing the root filesystem with the kubelet, no reservation between them, and no ephemeral-storage limits on any workload. The cluster had been drifting toward its capacity edge for weeks; disk pressure was the tripwire, not the disease.

Investigation

The first hours were a catalogue of reasonable fixes that didn't work, and the dead ends are the real story:

  • Pausing Argo CD applications didn't shed a single pod. Sync pause stops Git-driven changes — the Deployment controllers already in the cluster kept respawning everything regardless.
  • Image pruning freed 0 bytes. Every image on the tight nodes was still referenced by a Pending pod waiting to run. The garbage collector was technically correct and completely useless.
  • Cordoning the tightest nodes made things worse. Load concentrated on fewer nodes, container churn spiked, and kubelets started failing PLEG (pod lifecycle event) health checks — which flapped the nodes NotReady and triggered more rescheduling.
  • Restarting rke2-agent couldn't unstick a hung kubelet. Restarted agents came back, went NotReady again within minutes, and on the worst nodes the kubelet hung silently for 30+ minutes on startup, relisting the wreckage before reporting anything.

The signal that finally pointed somewhere useful: crictl showed enormous piles of orphaned pod sandboxes on the affected nodes — dead pods the kubelet had lost track of, still holding their writable layers.

Root Cause

Two conditions had to be true at once. First, no ephemeral-storage limits anywhere: any pod could write unbounded data to the node disk, and across 28 busy namespaces, many did. Second, the cluster was already at its capacity edge, so the eviction wave had nowhere to land — recovery required free pod slots and free disk, and the incident consumed both. Harbor going down was the multiplier: the recovery path itself depended on a component inside the blast radius.

Solution

What actually worked, in order:

  1. crictl rmp on orphaned sandboxes across the affected nodes reclaimed 33–47GB per node — the single biggest source of relief.
  2. Reboots instead of service restarts. A hung kubelet doesn't get better by restarting the agent above it; a clean reboot took wedged nodes from a 30-minute silent hang to a normal startup. We rebooted the stuck workers in parallel rather than one cautious node at a time.
  3. Scaling five dev namespaces to zero freed roughly 390 pod slots in one move. With actual headroom available, Harbor scheduled within seconds — and once the registry was back, everything else could finally pull images and recover on its own.

Total time from first eviction to a stable cluster: about four hours.

Technical Decisions

  • Load shedding over node surgery. The instinct is to fix nodes one by one; the win came from reducing what the cluster was being asked to run. Dev namespaces were the designated sacrifice — we made that an explicit, pre-agreed lever afterward.
  • Parallel reboots were the right risk. With Longhorn replicas spread across nodes, rebooting several workers at once risked volume availability — but the volumes were already degraded, and serial reboots would have stretched a 4-hour incident into a full day.
  • We stopped trusting "restart the agent" as a remediation for anything kubelet-side and wrote it down: hung kubelet → reboot, no intermediate steps.

Lessons Learned

  • Your recovery path can't depend on the thing that's down. An in-cluster registry is fine — until the incident that takes it down is the one where every recovering pod needs it. We documented which cluster components are recovery dependencies and what the fallback is for each.
  • Unbounded ephemeral storage is a cluster-wide contract violation waiting to happen. The aftermath: ephemeral-storage LimitRanges on every tenant namespace, so one workload's disk appetite becomes that pod's problem, not the node's.
  • Orphaned sandboxes are silent disk debt. A weekly cleanup CronJob now handles what crictl found the hard way.
  • A cluster at its capacity edge has no recovery capacity. Headroom isn't waste — it's the resource incidents run on.