Losing a whole cluster, and the go-live gate
Fence the lost source on two independent signals, bootstrap elsewhere from the bucket, and then decide — on evidence — whether playground is fit to go live.
By the end of this topic you can
- Fence a lost source on two independent signals before bootstrapping a DR group
- Bootstrap a disaster-recovery group for
playgroundfrom object storage and cut DNS over to it - Walk a production hardening checklist and say which items you would block a launch on
playground has survived everything you have thrown at it inside one cluster: a killed primary
promoted in 12.0 s, a split brain resolved by sitePriorities, five shapes of partition, the
operator itself going away, a datadir wiped and restored from a bucket. Every one of those had an
operator watching. Now the cluster hosting playground is gone — the API server does not answer, the
nodes do not answer, and what you still have is an S3 prefix in another region. There is no
failover group left to fail over.
Fence the source first, because that is where the danger is
Your instinct is to restore. Resist it for ten minutes. A restore into a DR cluster while the
source is still accepting writes gives you two writable copies of playground, and Bloodraven v1
does not automatically detect or resolve cross-cluster split brain (objective 13). Inside one
cluster the operator sees every site every 2 s and the decision matrix flags SPLIT BRAIN the
instant more than one core site is writable. Across clusters, nothing holds both halves. No
operator watches both. The sidecar fencing layer only knows the peers in its own group. There is no
component that will catch this mistake for you.
So the fencing decision is the safety mechanism, and it is yours. The checklist demands at least two of three independent signals before you declare the source dead:
|
|
| |
|---|---|---|---|
| What it actually proves | The source operator cannot reach any writable site — or you cannot reach the source operator. | You cannot administer the source cluster right now. | A network location outside both clusters cannot open port 3306 against the source. |
| How it lies on its own | The operator is not on the request path. It can be dead while MySQL serves writes perfectly. | Control plane and data plane fail separately. Kubernetes will not even delete pods on an unreachable node — the containers keep running and keep writing to the PV. | A one-way partition means your loss of reachability is not your application's loss of reachability. |
Two signals is not bureaucracy. Each one alone is a known false positive. If a signal is ambiguous — the API server answers but slowly — you wait. Waiting ten minutes costs you ten minutes. Getting this wrong costs you what it cost GitHub in October 2018: a 43-second partition left East and West each holding writes the other had never seen, and reconciling them took over 24 hours. That was one company, one tooling stack, one partition. You would be doing it by hand, across two clusters, with no shared GTID history to reason from.
MysqlStandbyCluster is a dashboard, not a lifeboat
You will find a CRD called MysqlStandbyCluster and assume it is the DR mechanism. It is not.
Today it is observability only. Its controller re-scans the source bucket on
spec.freshness.discoveryInterval (default 5m) and publishes exactly two conditions:
| Condition | True means |
|---|---|
BucketReadable | The DR cluster listed the source prefix and could read it. |
SourceConfigKnown | The dump metadata parsed — status.discovered now carries dump name, location, GTID set, and the archived binlog window. |
That is the whole of it: no MySQL contact, no restore Jobs, no activation. A standby cluster
sitting at BucketReadable=True has told you that a DR bootstrap would be possible and roughly
how far back it could reach. It has not proven the dump restores, and it will not lift a finger
when the source dies. Treat it as the pre-flight gauge it is.
The bootstrap is the restore path you already have
There is no DR-specific machinery to learn (objective 14). You create a new MysqlFailoverGroup
in the DR cluster, shaped for the DR cluster’s own nodes, IPs and zones, and point
spec.initFromBackup at the same bucket the dead cluster was writing to:
spec:
sites: # DR-cluster topology, not the source's
- name: east-1
role: primary-candidate
lbIP: 10.1.20.11
- name: east-2
role: primary-candidate
lbIP: 10.1.20.12
dns:
hostname: playground-east.example.com
ttl: 60 # shipped default, not the playground's 10
initFromBackup: # the same one-shot restore field from Unit 6
source:
s3:
bucket: shipstream-backups
prefix: playground/west/playground-nightly-20260520
region: us-west-2
credentialsSecret: s3-dr-readonly-creds
decryption:
passphraseSecret:
name: playground-backup-passphrase # mirrored into the DR namespace in advance
pointInTime:
stopDatetime: "2026-05-20T14:32:00Z" # omit to recover to the dump's GTID
initFromBackup is one-shot and gates bootstrap: nothing else proceeds until
status.restore.phase reads Succeeded. Then normal bootstrap — clone, replication, fencing —
runs exactly as it did on day one of this course.
kubectl bloodraven status playground --context=dr -n bloodraven-drMysqlFailoverGroup: bloodraven-dr/playground
Active site: east-1
Ready: True
DNS: playground-east.example.com (TTL 60s)
Sites:
NAME ROLE ZONE STATE REPL LAG RECOVERY LAST-SEEN
east-1 primary-candidate us-east-1a writable no - - 2s
east-2 primary-candidate us-east-1b read-only yes 0s - 2s
Initial restore (initFromBackup):
Phase: Succeeded
Target site: east-1Then DNS, by the same DNSEndpoint path from Unit 4: one object named bloodraven-playground,
server-side-applied every poll, one A record at spec.dns.ttl. The catch is the same catch —
the operator cannot accelerate DNS propagation, and here it is worse, because the operator only
owns the per-cluster record. The global application-facing name is yours to flip, by weight,
CNAME or GSLB. A perfect restore behind a stale CNAME is still an outage.
The day-2 surface you can hand to on-call
kubectl bloodraven has exactly seven subcommands: status, promote, reclone, backup,
verify-backup, version, help. Nothing else. The design property that makes it safe to put in
a runbook is stated in the plugin’s own header: it only writes resources the operator already
reads — annotations on MysqlFailoverGroup, plus MysqlBackup and MysqlBackupVerification
CRs — and it never talks to MySQL directly. There is no back door. kubectl bloodraven promote
behaves identically to the annotation, obeys the cooldown, the reader refusal, the lag gate, every
gate you already know. An on-call engineer cannot use the plugin to do something the operator
would have refused.
The go-live gate
Now commit to a verdict on each of these. Not “noted” — block or accept-with-a-named-owner (objective 15).
| Item | Why it is not what you assumed | My verdict |
|---|---|---|
sync_binlog=1 | An overridable default, written into the base my.cnf before your spec.mysqlConf. An override wins silently. | Block until you have read it off the running instance. |
| Backups on a PVC only | PVC-local backups are not durable; the failure that takes the PVC takes them. | Block. |
| No backup ever verified | An unverified backup is an assumption. Schrödinger backups. | Block. |
| The application-side write gap | No shipped alert covers it. Unowned, it is invisible until an incident. | Block until somebody owns it by name. |
replication.maxLagSeconds (whatever you set) | Drives only the ReplicationLagging condition. It is not a promotion gate — a replica beyond the threshold is still promoted. playground sets 30; a group that omits the field gets 300. Either way it gates nothing. | Accept, owner must know this. |
| Runbook timings from the playground | The playground overrides the shipped defaults: failoverCooldown: 30s (vs 5m), maxLagSeconds: 30 (vs 300), dns.ttl: 10 (vs 60). No timing you measured there transfers. | Accept, owner re-measures on real config. |
A role: read-only reader in the group | It can neither be promoted nor source a backup. It is not a spare. | Accept, owner must know this. |
Disagree with any of my verdicts if you can say why. That is the point of the exercise.
What you can now say about playground
playground began as three sites and a counter application on a laptop. It is now a group whose
failure modes you can enumerate, whose alerts do not lie, whose backups have been restored at least
once, and which you could hand to an on-call rotation tonight — with an honest statement attached:
it promotes unattended in about 12 seconds, it switches over on purpose at RPO 0 by construction,
it reports the exact lost-transaction count in divergentGtid, and it will not save you from a
DNS record you forgot to flip or a second cluster you fenced by guesswork. That statement — not the
failover, not the backup — is what you take away from this course.
Flashcards
The bar for declaring a DR source dead
At least two of three independent signals: the source operator's /active-site returns 5xx, the source cluster's API server is unreachable, and source MySQL is TCP-unreachable from a third vantage point outside both clusters.
Cross-cluster split brain in Bloodraven v1
Nothing detects or resolves it. No operator watches both clusters, so the human fencing decision is the safety mechanism.
BucketReadable=True on a MysqlStandbyCluster
The DR cluster listed the source bucket prefix and could read it.
SourceConfigKnown=True on a MysqlStandbyCluster
The source dump metadata parsed, so status.discovered now carries the dump name, location, GTID set and archived binlog window.
What a MysqlStandbyCluster does when its source cluster dies
Nothing. Phase 1 is observability only: no MySQL contact, no restore Jobs, no activation.
spec.restoreInPlace (as distinct from the DR-bootstrap field)
A re-runnable restore into the already-live active primary, gated by an RFC 3339 confirm token — not the path that stands up a new group in a DR cluster.
The seven kubectl bloodraven subcommands
status, promote, reclone, backup, verify-backup, version, help.
Why kubectl bloodraven promote obeys every gate the annotation obeys
The plugin only writes resources the operator already reads, and never talks to MySQL directly — there is no back door in it.
sync_binlog=1 in a Bloodraven group
An overridable default written into the base my.cnf before spec.mysqlConf is applied, so a user override wins silently — read the value off the running instance.
spec.replication.maxLagSeconds (default 300)
It drives only the ReplicationLagging Degraded condition; a replica beyond the threshold is still promoted.
The three playground values that are not the shipped defaults
failoverCooldown 30s (default 5m), replication.maxLagSeconds 30 (default 300), and dns.ttl 10 (default 60).
The DNS limit during a DR cutover
The operator cannot accelerate DNS propagation, and it owns only the per-cluster record — the global application-facing name is flipped by you.
Quiz
Show answer
Answer: Get a second independent signal before touching the DR cluster, because nothing in Bloodraven will catch a cross-cluster split brain if you are wrong
Two of three signals is the bar because Bloodraven v1 does not automatically detect or resolve cross-cluster split brain — no operator watches both clusters, so your judgement is the only safety mechanism. Option 1 confuses the control plane with the data plane: Kubernetes will not even delete pods on an unreachable node, so the containers keep running and keep writing to the PV. Option 3 misapplies site roles: role-based fencing only acts on sites inside one failover group, and the DR group has no visibility of the source at all. Option 4 expects the decision matrix to work across clusters, but SPLIT BRAIN is raised only when more than one core site of a single group is writable — and in any case you cannot receive an alert from a cluster you cannot reach. (objective 13)
Two of three signals is the bar because Bloodraven v1 does not automatically detect or resolve cross-cluster split brain — no operator watches both clusters, so your judgement is the only safety mechanism. Option 1 confuses the control plane with the data plane: Kubernetes will not even delete pods on an unreachable node, so the containers keep running and keep writing to the PV. Option 3 misapplies site roles: role-based fencing only acts on sites inside one failover group, and the DR group has no visibility of the source at all. Option 4 expects the decision matrix to work across clusters, but SPLIT BRAIN is raised only when more than one core site of a single group is writable — and in any case you cannot receive an alert from a cluster you cannot reach. (objective 13)
Show answer
Answer: False
The reversal: those two conditions prove only that the DR cluster could read the bucket and parse the dump metadata. MysqlStandbyCluster is observability only today — no MySQL contact, no restore Jobs, no activation. It tells you a DR bootstrap would be possible and roughly how far back the dump plus archived binlog window could reach; it has not loaded a byte of that dump into a mysqld. Proving restorability is a separate act, and the only thing that proves it is a verification that actually restored the artifact. (objective 14)
The reversal: those two conditions prove only that the DR cluster could read the bucket and parse the dump metadata. MysqlStandbyCluster is observability only today — no MySQL contact, no restore Jobs, no activation. It tells you a DR bootstrap would be possible and roughly how far back the dump plus archived binlog window could reach; it has not loaded a byte of that dump into a mysqld. Proving restorability is a separate act, and the only thing that proves it is a verification that actually restored the artifact. (objective 14)
Show answer
Answer: spec.initFromBackup, pointed at the source bucket prefix
spec.initFromBackup is the one-shot restore-on-first-boot field: it gates bootstrap until status.restore.phase reads Succeeded, then clone and replication proceed normally. Option 1 names the wrong restore entry point — restoreInPlace is re-runnable and operates against an already-live active primary, which a brand-new DR group does not have. Option 3 is the trap the standby CRD sets: spec.template does hold the shape of the group that a future activation would materialise, but activation is not implemented, so nothing consumes it. Option 4 confuses the archive with the restore — PITR governs whether binlogs were shipped at the source, and it must have been enabled there for a pointInTime request to be accepted at all, but it does not itself restore anything. (objective 14)
spec.initFromBackup is the one-shot restore-on-first-boot field: it gates bootstrap until status.restore.phase reads Succeeded, then clone and replication proceed normally. Option 1 names the wrong restore entry point — restoreInPlace is re-runnable and operates against an already-live active primary, which a brand-new DR group does not have. Option 3 is the trap the standby CRD sets: spec.template does hold the shape of the group that a future activation would materialise, but activation is not implemented, so nothing consumes it. Option 4 confuses the archive with the restore — PITR governs whether binlogs were shipped at the source, and it must have been enabled there for a pointInTime request to be accepted at all, but it does not itself restore anything. (objective 14)
Show answer
Answer: restore
The surface is exactly seven: status, promote, reclone, backup, verify-backup, version, help. There is no restore, and the reason is structural rather than an oversight — restore is not a CR at all, it is two fields on the failover group's spec, and the plugin's design rule is that it only writes resources the operator already reads and never talks to MySQL directly. promote and reclone exist because both are annotation-driven on the group, so the plugin can write them and inherit every gate. verify-backup exists because a verification really is its own CR that the operator reconciles into a throwaway instance. A runbook step that says kubectl bloodraven restore will fail at 3am with unknown command. (objective 15)
The surface is exactly seven: status, promote, reclone, backup, verify-backup, version, help. There is no restore, and the reason is structural rather than an oversight — restore is not a CR at all, it is two fields on the failover group's spec, and the plugin's design rule is that it only writes resources the operator already reads and never talks to MySQL directly. promote and reclone exist because both are annotation-driven on the group, so the plugin can write them and inherit every gate. verify-backup exists because a verification really is its own CR that the operator reconciles into a throwaway instance. A runbook step that says kubectl bloodraven restore will fail at 3am with unknown command. (objective 15)
Show answer
Answer:
Block on (a) and (c). sync_binlog=1 is only an overridable default — it is written into the base my.cnf before spec.mysqlConf is applied, so any override in the group's config wins silently. Until somebody reads the value off a running instance, the durability claim in the runbook is unverified, and it is the claim every RPO statement rests on. (c) is the same failure in a different place: an unverified backup is an assumption, and GitLab's 2017 outage is what an assumption looks like when it is finally tested. Accept (b) and (d) with a named owner. For (b) the owner must know that maxLagSeconds drives only the ReplicationLagging condition and is not a promotion gate — a replica beyond 300 seconds is still promoted, because no writable site is almost always worse. For (d) the owner re-measures every runbook timing against the shipped defaults, since the playground overrides failoverCooldown to 30s (against 5m), maxLagSeconds to 30 (against 300) and dns.ttl to 10 (against 60), so no playground timing transfers unchanged.
A full-credit answer shows: A strong answer blocks on (a) and (c) and accepts (b) and (d) with a named owner, and gives the mechanism in each case: sync_binlog is an overridable default written before spec.mysqlConf so an override wins silently; an unverified backup is an assumption; maxLagSeconds drives only the ReplicationLagging condition and is explicitly not a promotion gate; the playground overrides failoverCooldown, maxLagSeconds and dns.ttl so its timings do not transfer. A different split is acceptable if the reasoning names the mechanism — for example blocking on (d) because a runbook with wrong timings misleads during an incident. An answer that merely labels the items without a mechanism, or that treats maxLagSeconds as a promotion gate, is weak.
The gate is about verdicts, not notes. The two blockers are the ones where an unexamined assumption sits underneath a durability claim: an unread sync_binlog and an untested backup. The two acceptable items are dangerous only through ignorance — a lagging replica really is promoted, and playground timings really are not the shipped defaults — so they are survivable when a named human holds them and fatal when nobody does. (objective 15)
Sample answer
Block on (a) and (c). sync_binlog=1 is only an overridable default — it is written into the base my.cnf before spec.mysqlConf is applied, so any override in the group's config wins silently. Until somebody reads the value off a running instance, the durability claim in the runbook is unverified, and it is the claim every RPO statement rests on. (c) is the same failure in a different place: an unverified backup is an assumption, and GitLab's 2017 outage is what an assumption looks like when it is finally tested. Accept (b) and (d) with a named owner. For (b) the owner must know that maxLagSeconds drives only the ReplicationLagging condition and is not a promotion gate — a replica beyond 300 seconds is still promoted, because no writable site is almost always worse. For (d) the owner re-measures every runbook timing against the shipped defaults, since the playground overrides failoverCooldown to 30s (against 5m), maxLagSeconds to 30 (against 300) and dns.ttl to 10 (against 60), so no playground timing transfers unchanged.
A full-credit answer shows
A strong answer blocks on (a) and (c) and accepts (b) and (d) with a named owner, and gives the mechanism in each case: sync_binlog is an overridable default written before spec.mysqlConf so an override wins silently; an unverified backup is an assumption; maxLagSeconds drives only the ReplicationLagging condition and is explicitly not a promotion gate; the playground overrides failoverCooldown, maxLagSeconds and dns.ttl so its timings do not transfer. A different split is acceptable if the reasoning names the mechanism — for example blocking on (d) because a runbook with wrong timings misleads during an incident. An answer that merely labels the items without a mechanism, or that treats maxLagSeconds as a promotion gate, is weak.
The gate is about verdicts, not notes. The two blockers are the ones where an unexamined assumption sits underneath a durability claim: an unread sync_binlog and an untested backup. The two acceptable items are dangerous only through ignorance — a lagging replica really is promoted, and playground timings really are not the shipped defaults — so they are survivable when a named human holds them and fatal when nobody does. (objective 15)