Split brain, and what sitePriorities really buys you

Two writable sites is a three-tier problem: prior failover history, a priority list, or nothing but an alert. The field is sitePriorities, the published docs name a field that does not exist, and the resolution is a policy that discards writes.

By the end of this topic you can

  1. Walk the three tiers of split-brain response and say which one your group is on today
  2. Explain why sitePriorities is a policy, not a safety feature, and what it silently discards
  3. Recover a split brain: pick a winner, fence, audit, reclone

playground is telling you something new. The counter application’s writes are still landing, both iad and pdx are answering, and the group’s condition reads SPLIT BRAIN: 2 sites are writable (iad, pdx) with reason SplitBrain. The matrix emits that the moment more than one core site reports read_only=0 — strictly len(writable) > 1, with the read-only reader site excluded from the tally. Two writable primary-candidates, then. What does the operator do about it?

That depends entirely on one field, and there is a habit to install before you go looking for it.

Ask the cluster, not the page

The real field is spec.splitBrainPolicy.sitePriorities: an ordered list of site names. The operator promotes the first entry that is currently writable.

The reason to be emphatic about the name is a failure mode with no error message anywhere in it. A MysqlFailoverGroup that names a field the CRD schema does not define is not rejected — the API server prunes the unknown field and admits the object. No admission error, no Kubernetes event, no line in the operator log, and kubectl get -o yaml comes back without it because it was never stored. Your group silently keeps whatever behaviour it had, and you find out at 3am when a split brain sits there alerting and nothing fences.

That is not a hypothetical: for part of Bloodraven’s history the published failover page described a field called preferSite — in prose, and in a copy-pasteable YAML block — that no shipped CRD has ever defined. The page has since been corrected; the dated record is in the version appendix, row B1. The habit outlives the instance.

Two commands make it impossible to be caught by this class of error. kubectl explain mysqlfailovergroup.spec.splitBrainPolicy renders the schema the API server actually validates against, which is the only schema that matters. And grep -rn preferSite config/crd/bases/ charts/bloodraven/crds/ returns nothing, which is the whole story in one line. Documentation drifts from CRDs in both directions and at unpredictable speed. The CRD is the contract.

AnatomyThe field that actually ships
optional blocksplitBrainPolicy:
Omit it entirely and you are on tier 3 — alert only.
the real field sitePriorities:
Not preferSite. An ordered list, not a single name. MaxItems 16, matching spec.sites.
first choice - iad
Wins if it is currently writable AND role: primary-candidate.
fallback - pdx
Wins only if iad is not currently writable. Order is the whole policy.

The three tiers, in evaluation order

The response is tiered, evaluated in this order every time the operator sees SplitBrain.

TierTriggerWhat the operator does
1 — historylastFailoverTarget names a site that is live, writable and promotableFences every other site immediately, regardless of policy
2 — policyNo usable history and sitePriorities is non-emptyResolveSplitBrain picks the winner, losers are fenced, winner is re-promoted through the standard path
3 — neitherNo usable history and no prioritiesAlert only. Manual resolution, by design

Tier 1’s three conditions are a conjunction and all three matter: keepSite != nil && keepSite.state == StateWritable && keepSite.isPromotable(). A recorded target that has since gone unreachable, been demoted to read-only, or had its role changed away from primary-candidate does not win by memory alone — the operator falls through to tier 2. Authority has to be currently true, not merely once recorded.

Tier 2 runs state.ResolveSplitBrain(writable, sitePriorities). It walks your list in order and takes the earliest entry that is both currently writable and primary-candidate; every other writable site becomes a loser to fence. Two refusals are worth memorising. An empty list returns ("", nil) — it will not guess. So does a list whose entries name nothing currently writable and promotable. It never falls back to the order sites were declared in under spec.sites. No policy means no automated resolution.

The selector that is deliberately missing

In Unit 2 you met normal promotion, where GTID freshness is the primary selector and the priority list is only a tiebreaker — pickFreshestCandidate takes the most caught-up replica precisely to minimise data loss.

Split-brain winner selection does not consult GTID at all. The code says why: “GTID freshness is intentionally not consulted here — split-brain winner selection is policy-driven because every writable side may carry unique writes.” Normal promotion compares replicas against a dead primary, where “freshest” genuinely means “loses least”. In a split brain both sides have been accepting original writes, and neither GTID set contains the other. There is no freshest that is safe, so the operator stops pretending and asks you instead. Same system, opposite selector, for a good reason.

What shows up in logs and manifests

CEL rejects priority entries that do not name a primary-candidate site: “splitBrainPolicy.sitePriorities entries must match the names of sites with role ‘primary-candidate’”. Naming reader fails at kubectl apply, not silently at 3am. The resolution then emits a Warn naming the correct field and carrying two keys you can alert on — winner and fencedSite.

Try itWhat a tier-2 resolution looks like in the operator log
kubectl -n bloodraven-playground logs deploy/bloodraven | grep -E 'ALERT|split-brain'
{"level":"WARN","msg":"ALERT","message":"SPLIT BRAIN: 2 sites are writable (iad, pdx)"}
{"level":"WARN","msg":"split-brain auto-resolve: fencing non-preferred site per spec.splitBrainPolicy.sitePriorities","winner":"iad","fencedSite":"pdx"}
Recorded output. Run reveals what is already on the page — nothing executes, and no cluster is contacted.

Fencing is also retried on non-transition polls, and the retry gate counts writable promotable sites directly rather than trusting action.SplitBrain — the matrix reports SplitBrain=false whenever a writable non-promotable site needs fencing first. A stable split brain produces no further transitions, so a fence that failed once would otherwise never get a second attempt. Counting the actual hazard means it does.

The honest paragraph

The project’s own docs flag this with a danger admonition. Carry the sentence out of this topic: priority-based resolution “makes split-brain resolution fast and deterministic at the cost of silently losing the loser’s unreplicated writes. The loss is surfaced loudly but not prevented.”

sitePriorities does not prevent split brain, and merges nothing. It is a standing decision about which writes you are willing to discard, made in advance so the operator does not have to wake you. Policy, not safety.

Where your split brains will actually come from

Not from exotic partitions. From restarting a pod. A freshly created or freshly cloned MySQL pod comes up writable for several seconds before anything fences it. A recorded run has the new pdx pod Running and writable at T+22s and ALERT: SPLIT BRAIN at T+33s — eleven seconds with two sites taking writes. Restart a pod on playground and you reproduce that today.

The wider world says the same in bigger numbers. GitHub’s October 2018 incident: a 43-second partition left East and West each holding writes the other never saw, and recovery took over 24 hours. The partition was seconds; the cleanup was a day. Orchestrator issue #854 shows a graceful takeover split-brained anyway, because the new master was made writable before the old one was set read-only, leaving the demoted master holding transactions the cluster never got. And Pacemaker on quorum: its loss “can take an unbounded amount of time to detect and react to… The ultimate cure is to use fencing and lock the other side out.” Bloodraven agrees. The fencing layer is that agreement made executable.

Recovering playground from a split brain

Four steps, in order:

  1. Pick a winner. By policy if sitePriorities is set, by hand otherwise. There is no freshest — decide whose writes are authoritative.
  2. Fence the loser. SET GLOBAL super_read_only = ON. It blocks rather than cuts off; surviving sessions can still serve stale reads until the site is next demoted.
  3. Audit the divergence. status.sites[].divergentGtid holds the exact set the loser has and the winner never saw; the bloodraven_divergent_transactions gauge holds the count. The condition reason is DivergentTransactions and its message names the annotation you need.
  4. Reclone. The bloodraven.shipstream.io/reclone-site annotation you already met — <siteName>:<divergentGtidPrefix>, at least 8 characters, matched against the observed divergentGtid so you cannot fat-finger it.

You can now state which tier playground is on, justify or change its sitePriorities, and run pick-fence-audit-reclone without guessing. What you cannot yet say is what happens to any of it when the operator itself is not there to run it.

Flashcards

spec.splitBrainPolicy.sitePriorities

An ordered list of site names. During split-brain auto-resolution the operator promotes the first entry that is currently writable and primary-candidate, and fences every other writable site.

1 / 10

Quiz

Question 1 of 5

playground is split-brained — iad and pdx are both writable — and spec.splitBrainPolicy.sitePriorities is [iad]. Dump A has status.lastFailoverTarget empty. Dump B has status.lastFailoverTarget: pdx, with pdx writable and still primary-candidate. Which site ends up writable in each?

Show answer

Answer: iad in A, pdx in B — usable history is tier 1 and pre-empts the policy list entirely

The tiers are evaluated in order. In B the recorded target is live, writable and promotable, so tier 1 fires and fences everything except pdxsitePriorities is never consulted, which is exactly the case where policy and history give different answers and history wins. In A there is no usable history, so tier 2 runs ResolveSplitBrain and iad, the earliest currently-writable primary-candidate in the list, wins. Option 1 is the common misreading that policy is the top-level control; it is the fallback. Option 3 invents a conflict-detection behaviour the operator does not have — it does not compare the two and give up. Option 4 gets A wrong: a non-empty sitePriorities is precisely what stops the no-history case from being alert-only. (objective 4)

Question 2 of 5

playground is split-brained with no usable failover history, and spec.splitBrainPolicy is omitted entirely. spec.sites declares iad first and pdx second. What does the operator do?

Show answer

Answer: Alerts only — ResolveSplitBrain returns no winner and no automated action is taken

An empty sitePriorities makes ResolveSplitBrain return ("", nil) immediately: tier 3, alert only, manual resolution by design. Option 1 is the single most dangerous assumption here — the function documents explicitly that it never falls back to declared order, precisely so a winner is never picked by chance. Option 2 invents a heuristic the operator does not have; nothing tracks how long a site has been writable for this purpose. Option 4 sounds safe but is not what happens — with no policy the operator takes no automated action at all, so both sites keep accepting writes and the divergence keeps growing while you decide. (objective 4)

Question 3 of 5

When two sites are writable, Bloodraven promotes the one with the freshest GTID set, exactly as it does when choosing a replica to promote after a primary dies.

Show answer

Answer: False

The opposite is true, and the reversal is the point of the topic. Normal promotion is GTID-freshest — pickFreshestCandidate takes the most caught-up replica to minimise loss, with the priority list acting only as a tiebreaker. Split-brain winner selection deliberately does not consult GTID at all, because every writable side may carry unique writes: neither GTID set contains the other, so there is no "freshest" that is safe. Same system, opposite selector, for a good reason. (objective 5)

Question 4 of 5

You copy the splitBrainPolicy: preferSite: iad block from the published failover documentation into playground and kubectl apply it. The command reports the group configured. What have you actually changed?

Show answer

Answer: Nothing — the field is not in the CRD schema, so it was pruned and the group stays on tier 3

preferSite does not exist in the CRD. The API server prunes unknown fields, so the apply succeeds and changes nothing — the group keeps whatever tier it had, which with no sitePriorities is tier 3, alert only. Option 2 is the belief the documentation invites; there is no alias, and no code outside the docs reads that name. Option 3 gets the outcome half right but the mechanism wrong, and the difference matters operationally: an admission rejection would have told you at apply time, whereas silent pruning is what costs you an hour at 3am. Option 4 confuses policy with history — nothing writes lastFailoverTarget except a recorded failover. Verify with kubectl explain or by grepping the shipped CRD. (objective 5)

Question 5 of 5

Your colleague proposes setting sitePriorities: [iad, pdx] on playground and describes it in the change ticket as "prevents data loss from split brain". Correct the framing, and describe what you would actually do to the loser after a resolution fires.

Show answer

Answer:

sitePriorities does not prevent data loss — it decides in advance which data you are willing to lose. It makes resolution fast and deterministic at the cost of silently discarding the loser's unreplicated writes; the loss is surfaced loudly but not prevented, and it does not stop split brain from occurring in the first place. It is a policy, not a safety feature. After a resolution fires on playground, the winner (iad) keeps authority and pdx is fenced with SET GLOBAL super_read_only = ON. I would then audit the damage before touching anything: read status.sites[].divergentGtid for the exact set of transactions pdx holds that iad never saw, and the bloodraven_divergent_transactions gauge for the count — the condition reason will be DivergentTransactions. Only once that is captured would I recover pdx with the bloodraven.shipstream.io/reclone-site annotation, giving it pdx:<divergentGtidPrefix> of at least 8 characters so the request is matched against the observed divergence rather than applied blind.

A full-credit answer shows: A strong answer covers: (1) the correction — policy, not safety; it chooses which writes to discard rather than preventing loss, and does not prevent split brain; (2) the loss is surfaced but not prevented; (3) the recovery order — pick a winner, fence the loser, audit, then reclone; (4) the audit surfaces by name: status.sites[].divergentGtid and the bloodraven_divergent_transactions gauge; (5) reclone via the bloodraven.shipstream.io/reclone-site annotation. An answer that jumps straight to recloning without auditing first has missed the step where the lost transactions are still recoverable as evidence.

The project's own docs carry a danger admonition on exactly this point: priority-based resolution "makes split-brain resolution fast and deterministic at the cost of silently losing the loser's unreplicated writes. The loss is surfaced loudly but not prevented." The recovery procedure is pick-fence-audit-reclone in that order, and the audit step is the one people skip — once the site is recloned, the divergent transactions are gone for good. (objectives 5, 6)

Erase saved progress?

This erases all quiz scores, reading progress, project checklists, and your name on the certificate. It cannot be undone, and it affects only this course in this browser.