What it cost you

The RPO contract in one sentence, the two durability settings a tenant can silently switch off, and the GTID arithmetic that turns an outage into an exact number of lost transactions.

By the end of this topic you can

  1. State the RPO contract in one sentence and say which settings are guarantees and which are merely defaults
  2. Read promotionGtidExecuted and divergentGtid to get an exact lost-transaction count
  3. Pick the right row of the per-failure-mode RPO matrix for an outage in front of you

playground is back. pdx took writes 12.0 seconds after you took iad down, and the counter application is incrementing again. The incident review will not ask how fast that was. It will ask how many writes you lost — and “some” is not an answer you can put in a ticket.

The contract

Memorise this sentence, because everything else in the topic is either an amplifier or a mitigation of it:

An emergency failover can lose every transaction that committed on the dying primary but had not yet replicated to the surviving site.

Replication between sites is asynchronous. Nothing holds your commit open on iad until pdx has acknowledged it. The window is real, it is bounded only by how far behind pdx happened to be, and no setting in the CRD closes it on the emergency path.

Guarantees versus defaults

The mitigation people reach for is durability settings, and this is where the documentation flattens a distinction that decides whether your RPO story survives contact with a tenant. Bloodraven renders each site’s config in three passes. First a base map of sensible settings. Then your overrides — spec.mysqlConf, and then a site’s own mysqlConf, which beats the group’s. Then a short block of operator-owned invariants, written after your overrides, which simply overwrite whatever you put there. Finally skip-log-bin and disable-log-bin are deleted outright, because MySQL honours the last occurrence and a sorted render would place either alias after log-bin.

That ordering is the whole test. Written after the overrides, it is a guarantee. Written before them, it is a default.

CompareTwo layers of the same file
Overridable defaults (written first)Un-weakenable invariants (written last)
Which settings?sync-binlog=1, innodb-flush-log-at-trx-commit=2, binlog-expire-logs-seconds=1209600 (14 days)gtid-mode=ON, enforce-gtid-consistency=ON, log-replica-updates=ON, log-bin, skip-replica-start=ON, plugin-load-add=mysql_clone.so
Can spec.mysqlConf beat it?Yes — silently, with no warning and no admission rejectionNo — the invariant block overwrites your key after your override is applied
What does losing it cost?The durability and retention story the RPO documentation tellsGTID identity, binlog continuity, and the ability to clone or replicate at all
Where do you check?The rendered per-site ConfigMap, not the layer diagramThe same place — the invariant should be present regardless of what you set

So: a tenant who sets sync_binlog=0 in spec.mysqlConf gets sync_binlog=0, and your written RPO promise quietly becomes fiction. A tenant who sets gtid_mode=OFF gets gtid_mode=ON anyway. Do not trust the diagram — read the file playground actually rendered:

$ kubectl -n bloodraven-playground get configmap mysql-playground-iad-config \
    -o jsonpath='{.data.bloodraven\.cnf}' | grep -E 'sync-binlog|gtid-mode|flush-log'
gtid-mode=ON
innodb-flush-log-at-trx-commit=2
sync-binlog=1

Sharpen the middle line, because the MySQL manual is harsher than Bloodraven’s docs. With innodb_flush_log_at_trx_commit=2, logs are written at commit but flushed once per second, and the manual attributes the loss to any unexpected mysqld process exit — not just power loss — saying plainly that it “can erase up to N seconds of transactions”. Its own recommendation is innodb_flush_log_at_trx_commit=1 alongside sync_binlog=1, the setting it calls the safest, which guarantees no transaction is lost from the binary log. Bloodraven ships 2 for throughput. That is a defensible trade, and it is one spec.mysqlConf line from the stricter one. Just know the price: up to a second of committed transactions, on the site that has just crashed, on top of the replication window.

The arithmetic

Now count what you actually lost. At step 6 of the failover sequence the operator runs SELECT @@global.gtid_executed on the candidate — before it accepts a single write — and records the answer in status.promotionGtidExecuted. That is the high-water mark of everything pdx had received. When the old primary comes back and is compared against it, the operator subtracts one set from the other and publishes the difference as status.sites[].divergentGtid, with its cardinality on the bloodraven_divergent_transactions gauge.

Two MySQL functions let you do that arithmetic by hand on any pair of sets. GTID_SUBSET(set1, set2) returns true when every GTID in set1 is also in set2 — that is the “did it catch up” question. GTID_SUBTRACT(set1, set2) returns only those GTIDs from set1 that are not in set2. The subtraction is the divergence primitive; its cardinality is your lost-transaction count.

One wrinkle before you eyeball a set. MySQL 9.x GTID sets can carry user-defined tags, written uuid:tag:interval, and a tag is treated as part of the UUID’s identity. uuid:Domain_1:1-3 and uuid:Domain_2:1-3 are six different transactions, not three. Let MySQL do the subtraction.

The measurement

Two GTID bars. IAD holds transactions 1 through 23. PDX holds 1 through 19. The range 20-23 is highlighted amber and labelled 4 lost transactions.
What the failover cost. 20-23 is four transactions, not an estimate.

Here is a real playground status after an emergency promotion to pdx, with the two fields that matter and everything else elided:

status:
  activeSite: pdx                                     # was iad
  lastFailoverTarget: pdx
  promotionGtidExecuted: |-                           # new since topic 1: pdx's set at promotion
    a2cc879c-5f9d-11f1-9fae-8e47bc2a4544:1-19,
    a3c3f9e8-5f9d-11f1-bf37-568bfb8d0365:1-7
  sites:
  - name: iad
    divergentGtid: a2cc879c-5f9d-11f1-9fae-8e47bc2a4544:20-23
    divergentTransactionCount: 4

Read it off. a2cc879c… is iad’s own server UUID — the transactions it originated as primary. pdx had 1-19 of them at the moment of promotion. The difference is 20-23, so iad had run to 1-23 and four transactions committed on iad never reached pdx. The count is 23 − 20 + 1 = 4, which is exactly what the gauge reports (bloodraven_divergent_transactions{site="iad"} 4) and what the status condition says in words: Old primary iad has 4 divergent transactions. Four counter increments. Not an estimate.

Try itDoing the subtraction yourself
kubectl -n bloodraven-playground get mysqlfailovergroup playground -o jsonpath='{.status.sites[?(@.name=="iad")].divergentGtid}'
a2cc879c-5f9d-11f1-9fae-8e47bc2a4544:20-23
mysql -N -e "SELECT GTID_SUBTRACT('a2cc879c-5f9d-11f1-9fae-8e47bc2a4544:1-23', 'a2cc879c-5f9d-11f1-9fae-8e47bc2a4544:1-19,a3c3f9e8-5f9d-11f1-bf37-568bfb8d0365:1-7')"
a2cc879c-5f9d-11f1-9fae-8e47bc2a4544:20-23
Recorded output. Run reveals what is already on the page — nothing executes, and no cluster is contacted.

The distractor: maxLagSeconds

spec.replication.maxLagSeconds defaults to 300, and the playground manifest for playground sets 30. It drives exactly one thing: a ReplicationLagging reason on the Degraded condition when a site’s reported lag exceeds it. It is not a promotion gate. Nothing in candidate selection consults it. If iad dies while pdx is 400 seconds behind, Bloodraven promotes pdx anyway — because no writable site at all is almost always worse. If you believe maxLagSeconds bounds your RPO, your RPO is whatever the lag happened to be at the moment of the crash. What does bound it is a true GTID-superset test, which is why a planned switchover is RPO 0 by construction; that path is Unit 4.

Pick your row

Failure modeRPOWhy
Container/pod crash, PVC intact0 — no failover at allThe primary returns writable and the operator keeps it
Clean primary kill, replica caught upNear zero, not guaranteed zeroThe contract still applies to anything in flight
Primary kill with unapplied relay logsWhatever was in flightThe 30 s drain applies what it can reach; the rest is gone
PVC destroyed with the primaryWorst rowThe previously-active binlog lived on the destroyed PVC — PITR cannot replay a tail that was never shipped

Given an outage, find the row before you quote a number.

You can now state the RPO contract, tell a durability guarantee from a durability default by reading a rendered ConfigMap, and produce an exact lost-transaction count for the failover you just ran. Which leaves the four transactions sitting on iad. iad is back, it is read-only, and it is holding writes pdx has never seen. What happens when it tries to rejoin is the next topic.

Flashcards

State the RPO contract for an emergency failover in one sentence.

An emergency failover can lose every transaction that committed on the dying primary but had not yet replicated to the surviving site.

1 / 12

Quiz

Question 1 of 5

A tenant adds both sync_binlog: "0" and gtid_mode: "OFF" to spec.mysqlConf on the playground group. What does the rendered per-site config contain after the next reconcile?

Show answer

Answer: sync-binlog=0 and gtid-mode=ON

The two settings sit in different layers. sync-binlog=1 is in the base map, written before spec.mysqlConf, so the tenant's 0 wins and the durability story quietly changes. gtid-mode=ON is in the invariant block written after user overrides, so it is stamped back to ON. Option 0 is the common flattening of the model — it is right about sync_binlog and wrong about gtid_mode. Option 2 assumes the operator classifies settings by what they mean rather than by write order; it does not, which is exactly why sync_binlog is beatable. Option 3 imagines admission-time protection that does not exist: the override is accepted and silently overwritten at render time, with no rejection and no event. (objective 4)

Question 2 of 5

With spec.replication.maxLagSeconds set to 300, a replica reporting 400 seconds of lag is excluded from promotion when the primary dies.

Show answer

Answer: False

The reversal: Bloodraven promotes it anyway. maxLagSeconds drives exactly one thing — the ReplicationLagging reason on the Degraded condition — and nothing in candidate selection consults it, because no writable site at all is almost always worse than a stale one. The practical consequence is the point: if you believe maxLagSeconds bounds your RPO, your RPO is whatever the lag happened to be when the primary died. A true GTID-superset test is what actually bounds loss, and that gate belongs to the planned path. (objective 4)

Question 3 of 5

You hold the old primary's gtid_executed set (O) and the value recorded in status.promotionGtidExecuted for the new primary (N). Which expression gives the number of transactions lost?

Show answer

Answer: The cardinality of GTID_SUBTRACT(O, N)

GTID_SUBTRACT(O, N) returns only those GTIDs from O that are not in N — the transactions the dying primary committed and never shipped — and counting them gives the loss. Option 0 returns a boolean: it tells you whether divergence exists, not how much. Option 2 reverses the operands and yields what the new primary has that the old one lacks, which is normal post-promotion drift, not loss. Option 3 is the eyeball heuristic that breaks on real sets: intervals are sparse, several UUIDs can appear, and a MySQL 9.x tag makes uuid:Domain_1 and uuid:Domain_2 distinct identities, so comparing top sequence numbers can be wrong in either direction. (objective 5)

Question 4 of 5

Bloodraven's base config sets innodb-flush-log-at-trx-commit=2. According to the MySQL manual, what can that cost you?

Show answer

Answer: Up to a second of transactions on any unexpected mysqld process exit

The manual is blunter than the operator's docs: with a setting of 2 logs are written at commit but flushed once per second, and any unexpected mysqld process exit can erase up to N seconds of transactions. It recommends 1 alongside sync_binlog=1. Option 0 confuses 'written' with 'flushed' — that gap is the whole loss window. Option 1 is the softer framing worth unlearning: an OOM kill or a crashing mysqld costs you the same second as a power cut, and it is the site that just crashed. Option 3 describes the replication window, which is a separate loss window that adds to this one rather than replacing it. (objective 4)

Question 5 of 5

The node hosting the active primary iad suffers a disk failure: the pod and its PVC are destroyed together. Nightly backups and PITR binlog archival were enabled. Which row of the per-failure-mode RPO matrix is this, and what specifically cannot be recovered?

Show answer

Answer:

This is the worst row — PVC destruction, not a pod crash. It is not the RPO-0 row, because that one requires the PVC to survive so the same primary comes back writable and no failover happens at all. Here the loss is everything iad committed but had not replicated to pdx: those transactions were only ever in the binlog on the destroyed PVC, they were never shipped, so they are not in the replica's binlog stream and therefore not in PITR's replay material. Backups plus PITR get you back to the last archived event, not to the tail. The exact count is read from divergentGtid — except that here the site holding it is gone, so quote the replication window, not a measured number.

A full-credit answer shows: A strong answer covers: (1) identifying the PVC-destruction row rather than the pod-crash row, and saying why the pod-crash row is RPO 0 (PVC intact, primary returns writable, no failover); (2) naming the previously-active binlog on the destroyed PVC as the thing that is gone; (3) the reason PITR cannot cover it — unshipped transactions are not in the replica's binlog stream and so are not in the replay material; (4) bonus credit for noticing that the usual divergentGtid measurement is unavailable because the diverged site no longer exists.

The discrimination is between two rows that both start with 'the primary died': with the PVC intact there is no failover and no loss, while with the PVC destroyed you lose the unreplicated tail permanently. The tempting wrong answer is that PITR closes the gap — it does not, because PITR can only replay events that were actually archived, and the tail never left the dead primary. (objective 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.