Recover a Cluster from a lost primary majority - #35
Merged
Conversation
A Cluster that loses more than half its primaries at once cannot heal
itself — voting a replica in needs a master quorum that no longer exists,
so it sits in cluster_state:fail with unserved slots indefinitely while its
healthy, current replicas wait for votes that never come.
The operator now drives recovery. Once the survivors are provably below a
voting quorum (healthy*2 <= total) and each dead primary is fenced from two
independent perspectives — the k8s API view of pod/node liveness AND a
direct data-path check — it waits out a 45s debounce so it never races a
failover gossip could still perform, then issues CLUSTER FAILOVER TAKEOVER
on the most up-to-date surviving replica of each shard. Re-created primaries
rejoin as replicas via their retained PVCs and the cluster self-heals.
Automatic for the Cache profile; opt-in for Durable via the
valkey.wellcake.io/quorum-takeover annotation, since a forced takeover can
drop acknowledged writes. Write safety rests on Valkey's own
minority-write-block (a primary partitioned from the majority stops serving
after cluster-node-timeout), not on proving process death — see ADR 0006.
Surfaced via status.quorumDownSince and failover_total{reason=
"cluster-takeover"}. Validated live on k3d: majority of primaries fenced,
recovered to cluster_state:ok with 0 data loss and the fenced primaries
rejoining as replicas.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Cluster fails a single primary over by gossip vote, which needs a majority of masters. Lose more than half the primaries at once (an AZ outage, a bad drain, a correlated crash) and the survivors aren't a quorum, so gossip can't vote any replica in. The cluster then sits in
cluster_state:failwith unserved slots for as long as the outage lasts, even though the dead primaries' replicas are healthy and current. That is a real single point of failure for the Cluster topology: the data is right there, but nothing brings it back.This adds an operator-driven recovery path for that case.
How it decides to act
The operator only steps in when all of these hold, so it never races a failover gossip could still do and never overrules a primary that might still be serving:
valkey.wellcake.io/quorum-takeover: "true"annotation.CLUSTER NODES,healthyMasters*2 <= totalMasters. A minority of primaries down is left to gossip.cluster_state:fail). A primary that is reachable and reportscluster_state:okis genuinely serving, and is never taken over.It then deletes the dead primary's pod and runs
CLUSTER FAILOVER TAKEOVERon the shard's most up-to-date reachable replica. The re-created primary rejoins through its retained PVC, sees the higherconfigEpoch, and demotes itself, so full redundancy comes back without any manual step.Why it isn't split-brain
Write safety doesn't depend on proving the old primary's process is dead. No operator can do that without STONITH. It depends on a guarantee Valkey already makes: a primary partitioned from the majority for longer than
cluster-node-timeoutstops accepting writes. The 45s debounce is far longer than the 5s node-timeout, so by the time a takeover fires, a still-running but isolated old primary has already stopped serving writes. The k8s fence and the data-path check sit on top of that as defence in depth. A minority primary can still answer stale reads until it rejoins, which the Cache profile accepts and Durable opts into. The full argument is in ADR 0006.A cross-model review shaped the safety design
An adversarial review pass went after the split-brain edge cases and changed the design. It led to the two-perspective fence instead of trusting a NotReady node on its own; the majority gate, so a minority failure is left to gossip and the Durable validity-factor refusal is never overruled; the Durable opt-in, because a forced takeover can drop acknowledged writes; validating
CLUSTER NODESnames against real pods, so a bare IP with no announce hostname can't look fenced; and clearing the debounce marker only oncluster_state:ok.Validation
cluster_state:ok, 16384 slots served, 500/500 keys intact. After uncordoning, the fenced primaries rescheduled and rejoined as replicas, andstatus.quorumDownSincecleared.Closes the AR1 Cluster SPOF.