Skip to content

Cluster Lifecycle

How a cluster is born, grows, and survives violence. The second half of this page is effectively a post-mortem anthology — Narad's crash-recovery design wasn't written down first and tested later; it was forged by killing live nodes under traffic until nothing broke anymore.

Bootstrap

The first nodes (the initial_members, typically 3) each start with an empty disk and bootstrap the same Raft configuration — identical peer lists make that a legal, convergent bootstrap. One wins the first election; the controller on it seeds the root admin and starts assigning partitions.

Scale-out: joining an existing cluster

A node not listed in initial_members must never bootstrap — it would create a phantom cluster that the real one will never contact. Instead it starts join-only:

sequenceDiagram
    participant N as new node (narad-3)
    participant P as peer (follower)
    participant L as leader
    Note over N: empty raft config, /readyz = false
    N->>P: JoinCluster RPC
    P-->>N: 421 not the leader
    N->>L: JoinCluster RPC
    L->>L: raft AddVoter(narad-3)
    L-->>N: 200
    L->>N: AppendEntries (replication begins)
    Note over N: sees leader → admitted,<br/>catches up FSM → /readyz = true

The join loop walks configured peers every 2s until its own Raft sees a leader (proof of admission). Readiness is held until then, so an unadmitted node never receives traffic — the failure mode where a fresh node serves an empty metastore behind the load balancer is structurally impossible. AddVoter is idempotent; joiner restarts and lost replies are safe. Scaling out is literally replicaCount: 5 in Helm.

New nodes receive partition assignments for topics created after they join; existing partitions never move (single-owner data). Rebalancing and scale-down/decommission are deliberately future work.

Steady state

  • Every node heartbeats its membership to the leader every 5s; 30s of silence marks it dead.
  • Dead nodes' partitions are not reassigned (their data is on that disk); produce reroutes around them, consume returns 503 for their partitions until they return.
  • Graceful shutdown transfers Raft leadership first — planned restarts fail over in ~150ms.
  • Rolling restarts under full traffic are routine; the soak rig has been through dozens.

Crash recovery: the four-bug war story

All four bugs share one root cause: a node restored from a Raft snapshot believes it is current while being hours stale — and each bug was a different subsystem trusting that belief with something destructive. All four were found by force-killing pods under live soak traffic with a loss-detecting harness; each fix landed with the next kill proving it.

timeline
    title One root cause, four disguises
    Kill 1 : startup orphan sweep read stale topic set : deleted LIVE topic directories
    Kill 2 : fan-out reconciler read stale attach epochs : tail-anchored cursors, skipped the delay backlog
    Audit : dispatcher trusted local topic absence : could discard 202-acked WAL records
    Kill 3 : fresh LEADER trusted its still-replaying FSM : the node that WON the election lost data

1. The startup orphan sweep compared on-disk topic directories against the (stale) local topic set and reclaimed "orphans" — deleting live topics' data seconds after boot. Fix: deletion requires the leader to confirm the topic is gone; every failure mode keeps the directory.

2. Fan-out cursors spawned from the stale view carried dead attach epochs, mismatched the (correct) offset files, tail-anchored, and overwrote them — so the caught-up cursors re-anchored at the new tail, silently skipping the delay child's pending backlog. Fix: reconcilers wait for a provably-current replica; tail-anchoring requires a leader-confirmed epoch; offset files are only deleted by their own epoch's cursor.

3. The dispatcher's discard path dropped WAL records whose topic was locally absent — sound logic ("replicas only move forward") that snapshot restore breaks: every topic created after the snapshot reads as deleted. Fix: discard requires caught-up + leader-confirmed absence.

4. The subtle one. Fixes 1–3 had a shortcut: "if I am the leader, my local state is authoritative." Then a double-kill made a freshly restarted node win the election — legal, its log was complete — while its FSM was still replaying an old snapshot. It confirmed a dead epoch from its own stale state and tail-anchored. The follower that restarted beside it asked the real leader, was refused, and lost nothing; that asymmetry was the fingerprint. Fix: a self-leader must pass a Raft barrier (FSM fully applied) and re-read before trusting itself. Election proves the log; only the barrier proves the state.

The chaos matrix

The regimen that found the bugs now guards against their return — all runs under 300 msg/s of soak traffic with a Redis ledger that detects any lost message:

Scenario Result
Force-kill a follower owning parent partitions cursors resume from files, zero loss
Force-kill the Raft leader ~1s failover, zero loss
Force-kill a child-partition owner commits retry, zero loss
Force-kill two of three nodes (quorum loss, ×3) reads/produce degrade gracefully, full recovery, zero loss
Force-kill during a rolling restart zero loss
Scale 3→5, then kill leader + new node together quorum held at 5 nodes, zero loss

The recurring numbers: bounded duplicates (the at-least-once seams), OVERDUE = 0 (the harness's loss detector) at the end of every scenario.

Lifecycle constants, for the record

Thing Value
Join attempt cadence / admission proof every 2s / "my own Raft sees a leader"
Graceful leadership transfer ~150ms; crash election ~1s
Heartbeat / dead marking 5s / 30s
Startup reconcile caught-up wait ≤60s (sweep skipped on timeout — data outlives impatience)
Self-leader trust only after a 5s-bounded Raft Barrier + re-read