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
- Walk the three tiers of split-brain response and say which one your group is on today
- Explain why
sitePrioritiesis a policy, not a safety feature, and what it silently discards - 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.
splitBrainPolicy: sitePriorities: - iad - pdxThe three tiers, in evaluation order
The response is tiered, evaluated in this order every time the operator sees SplitBrain.
| Tier | Trigger | What the operator does |
|---|---|---|
| 1 — history | lastFailoverTarget names a site that is live, writable and promotable | Fences every other site immediately, regardless of policy |
| 2 — policy | No usable history and sitePriorities is non-empty | ResolveSplitBrain picks the winner, losers are fenced, winner is re-promoted through the standard path |
| 3 — neither | No usable history and no priorities | Alert 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.
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"}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:
- Pick a winner. By policy if
sitePrioritiesis set, by hand otherwise. There is no freshest — decide whose writes are authoritative. - 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. - Audit the divergence.
status.sites[].divergentGtidholds the exact set the loser has and the winner never saw; thebloodraven_divergent_transactionsgauge holds the count. The condition reason isDivergentTransactionsand its message names the annotation you need. - Reclone. The
bloodraven.shipstream.io/reclone-siteannotation you already met —<siteName>:<divergentGtidPrefix>, at least 8 characters, matched against the observeddivergentGtidso 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.
spec.splitBrainPolicy.preferSite
A field that exists only in the documentation. It is not in the CRD schema, so the API server prunes it on apply — no error, no event, no effect.
You want to confirm that a field the docs describe actually exists on this cluster. What do you run?
kubectl explain mysqlfailovergroup.spec.splitBrainPolicy to read the live schema, or grep the shipped CRD under config/crd/bases/ and charts/bloodraven/crds/.
Tier 1's guard — the three conditions the recorded lastFailoverTarget must satisfy
The site must exist (keepSite != nil), be currently StateWritable, and be isPromotable(). All three, as a conjunction.
state.ResolveSplitBrain(writable, sitePriorities)
Walks sitePriorities in order and returns the earliest entry that is currently writable and primary-candidate as winner; every other writable site is returned as a loser for the caller to fence.
Why is GTID freshness deliberately not consulted when picking a split-brain winner?
Because every writable side may carry unique writes, so no GTID set contains the other and there is no "freshest" that is safe to promote. Winner selection is therefore policy-driven.
The CEL rule guarding sitePriorities
Every entry must match the name of a site whose role is primary-candidate; the group is rejected at admission otherwise.
The two log keys carried by the split-brain auto-resolve Warn
winner and fencedSite — on the message "split-brain auto-resolve: fencing non-preferred site per spec.splitBrainPolicy.sitePriorities".
Why does the split-brain fence retry count writable candidates directly instead of trusting action.SplitBrain?
Because the matrix reports SplitBrain=false whenever a writable non-promotable site needs fencing first, and a stable split brain emits no further transitions — so a fence that failed once would never be retried.
What becomes of the loser's unreplicated writes after a priority-based resolution?
They are isolated on the loser's PVC — reported as status.sites[].divergentGtid and the bloodraven_divergent_transactions gauge, and recoverable only by recloning that site.
Quiz
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 pdx — sitePriorities 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)
The tiers are evaluated in order. In B the recorded target is live, writable and promotable, so tier 1 fires and fences everything except pdx — sitePriorities 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)
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)
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)
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)
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)
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)
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)
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)
Sample 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)