Stand it up and read its status

Two minutes to a live three-site group with an app reading and writing through it. Then the skill everything else rests on: reading the status and saying what is true right now.

By the end of this topic you can

  1. Bring up a three-site group with ./playground/setup.sh and confirm every site reports Ready
  2. Read a MysqlFailoverGroup status and name the active site, each site's state, and the replica's lag
  3. Watch the counter application write through mysql-playground-primary and find the same row on the replica

This course is about a MySQL that has to survive losing a whole site. The fastest way in is not a vocabulary list. It is a real group on your laptop, and a status dump you can read.

Three MySQL sites — IAD writable, PDX and READER as replicas — with the counter app writing through mysql-playground-primary and a small operator off to the side.
What you are about to start. One site takes writes. The other two copy from it. The operator is not on the request path.

Before you start

You needWhy
docker or podmanBuilds the images locally. Prefer docker — k3d’s podman support is experimental.
kubectlEverything from here is kubectl.
helmInstalls the operator. Chart 1.0.0, appVersion 1.0.0, kubeVersion: ">=1.26.0".
A cluster with at least three worker nodesOne worker per site. The third is dedicated to the reader so storage-loss testing is deterministic.
k3d cluster create bloodraven --agents 3

kind and minikube work too — one server plus three agents either way.

One non-technical check while you are here. Bloodraven is source-available, not open source. Whether your organisation may run it in production is a licensing question, not a git clone. The baseline and the two commands that settle it live in section D of the version appendix. Nothing in this course depends on the answer. Your production plan might.

Bring it up

From the repository root:

./playground/setup.sh

That script labels one worker per site, builds and loads the images, installs the CRDs and the operator, creates the group, and deploys a dashboard and a counter app. About two minutes.

Change nothing. Every command in this course is written against the group exactly as that script creates it — metadata.name: playground in bloodraven-playground. The group name is baked into node labels, Helm flags and a Go constant, so renaming it quietly breaks later tools.

Three MySQL pods. Each runs mysql beside a sidecar container, so a healthy site reads 2/2:

kubectl -n bloodraven-playground get pods -l app.kubernetes.io/name=mysql

Read the status

status is the operator’s report on the group. A handful of fields carry everything you will read for the rest of the course.

status.activeSite is the site the operator currently treats as writable. One name, or empty.

status.sites[] runs parallel to spec.sites. Per site:

There is a trap in that list. The operator probes replication only on sites whose state is read-only, so the writable primary’s entry carries no replicating, no secondsBehindSource and no gtidExecuted at all — those keys are absent, not zero. Absence means “not measured here”, never false.

And .role is not there at all. Role lives in spec.sites[].role. To tell which of two read-only sites is the dedicated reader, read the spec.

Try itRead the healthy group
kubectl -n bloodraven-playground get mysqlfailovergroup playground -o jsonpath='{.status.activeSite}{"\n"}'
iad
kubectl -n bloodraven-playground get mysqlfailovergroup playground -o jsonpath='{range .status.sites[*]}{.name}{"\t"}{.state}{"\t"}{.replicating}{"\t"}{.secondsBehindSource}{"\n"}{end}'
iad	writable		
pdx	read-only	true	0
reader	read-only	true	0
kubectl -n bloodraven-playground get mysqlfailovergroup playground -o jsonpath='{.status.conditions[?(@.type=="Degraded")].reason}{"\n"}'
Healthy
Recorded output. Run reveals what is already on the page — nothing executes, and no cluster is contacted.

Two blank columns on the iad row. That is the point.

Conditions and the five reasons

The group carries a Ready condition — true when a site is writable and replication is healthy — and a Degraded condition with a reason string. Five reasons come from the decision table. Each describes a shape, not an action:

ReasonThe shape it describes
HealthyExactly one core site writable, none unreachable. Degraded is False.
DegradedAnything short of healthy that is not one of the three below — a live primary with an unreachable peer, or a writable non-promotable site awaiting fencing.
SplitBrainMore than one core site is writable at the same time.
NoPrimaryNo core site is writable and none is unreachable — every one of them is read-only.
TotalLossEvery core site is unreachable.

Two surprises. There is no Failover reason: the topology one promotion away from recovery reports Degraded. And role: read-only sites are excluded from those tallies — a dead reader never reads as TotalLoss.

What the operator does about each shape is Unit 2. Here, you only read them.

Watch a write land on both sides

kubectl -n bloodraven-playground port-forward svc/dashboard 8091:8091
kubectl -n bloodraven-playground port-forward svc/counter-app 8090:8090

The counter app at localhost:8090 connects through mysql-playground-primary. It has exactly two code paths:

PathWhat it runsWhen
readGET /api/counterSELECT value, updated_at …, then SELECT @@global.read_only and SELECT @@hostname on the same connectionthe page polls it every two seconds
writePOST /api/incrementUPDATE counter_db.counters SET value = value + 1 WHERE id = 1only when you press + Increment

Nothing writes on a timer. You decide when a write happens. And because the read path asks the same connection who it is talking to, the JSON carries dbHost and readOnly beside value. In Unit 3 those three fields will disagree with the cluster in a way you can see.

Press + Increment a few times, then go looking for that row on the replica — by name, not through the primary Service:

kubectl -n bloodraven-playground exec deploy/mysql-playground-pdx -c mysql -- \
  env MYSQL_PWD=playground-root-pw mysql -h127.0.0.1 -uroot -Nse \
  "SELECT value, updated_at FROM counter_db.counters WHERE id = 1"

Same value you just clicked to. That round trip is the whole data plane in two commands.

One honest warning

The playground overrides shipped defaults so experiments finish while you are still watching. A timing you observe here is not the shipped default:

SettingPlaygroundShipped default
failoverCooldown30s5m
replication.maxLagSeconds30300
dns.ttl1060

Everything else matches: pollInterval, failureThreshold, recoveryThreshold, leaseTimeout, peerCheckInterval and maxSyncWait sit at their defaults. MySQL is mysql:9.7, the operator v1.0.0.

Words for the first hour

You will meet these on every later page. One sentence each.

WordMeaning here
SiteOne MySQL plus its sidecar, pinned to one place (iad, pdx, reader).
PrimaryThe one site that currently accepts writes. Named in status.activeSite.
ReplicaA site that copies from the primary and is not taking writes.
FailoverPromoting a replica to primary because the old primary is gone or being moved.
OperatorThe controller that polls sites and decides who is primary. Not on the request path.
SidecarA second container in each MySQL pod. It can stop its own MySQL writing without asking the operator.
ServiceA Kubernetes name your app connects to. -primary always points at whoever is writable right now.
RPOHow much recently committed data you accept losing. Bloodraven’s RPO on a crash is not zero.
RTOHow long you accept being unable to write. This is where Bloodraven spends its budget.
GTIDA unique id for every transaction. The set of them is how you count what a failover cost.
FenceSET GLOBAL super_read_only = ON. Blocks writes. Does not close existing connections.

The full card is Unit 7. This is enough to start.

Where you are

playground is up: three sites, iad writable and named in activeSite, pdx and reader read-only and replicating at 0 seconds behind, Degraded reason Healthy, and a counter application reading and writing through mysql-playground-primary.

Next: what those pods actually are, and which of them is allowed to become primary.

Flashcards

status.activeSite

The single site the operator currently treats as the writable authority — one site name, or empty when no site holds authority.

1 / 10

Quiz

Question 1 of 5

playground reports activeSite: iad, and its site entries read: iad state writable, no other fields; pdx state read-only, replicating: true, secondsBehindSource: 0; reader state read-only, replicating: true, secondsBehindSource: 0. Which site is the primary, and what in the dump establishes it?

Show answer

Answer: iad — it is named in activeSite and it is the one site whose state is writable

activeSite names the site the operator currently treats as the writable authority, and iad is the only entry in state writable — the two agree, which is what a healthy group looks like. pdx is wrong because replicating: true marks a follower applying the primary's stream, not a primary; the primary is never probed for replication at all. The first-entry answer is wrong because status.sites[] runs parallel to spec.sites, in declaration order, with no relation to who is primary. The last option confuses activeSite with status.lastFailoverTarget, which is the field that records a past promotion. (objective 8)

Question 2 of 5

Same dump. pdx and reader both read state read-only, replicating: true, secondsBehindSource: 0. How do you establish which of them is the dedicated non-promotable reader?

Show answer

Answer: Read spec.sites[].role — the status block carries no role field

Role is spec, not status: status.sites[] carries name, state, lastSeen, replication fields and recovery fields, and nothing about role. The second option is the common trap — the documentation talks about site roles beside site state and it is easy to assume both live in the same block; they do not, and status.sites[].role simply does not exist. The third confuses role with state: both a primary-candidate follower and a read-only reader sit in state read-only when they are healthy, which is exactly why the dump cannot separate them. The fourth invents a field — lbIP is spec, and status never mirrors it. (objective 8)

Question 3 of 5

playground now reports activeSite: iad; iad state writable; pdx state read-only, replicating: true; reader state unreachable. What reason does the Degraded condition carry?

Show answer

Answer: Healthyrole: read-only sites are excluded from the writable/read-only/unreachable tallies

The reasons are computed over core sites only, and a site with role: read-only is not a core site — so a dead reader leaves the tally at one writable, zero unreachable, which is Healthy. The second option is the most tempting and would be right if the unreachable site were pdx: a live primary with an unreachable core peer is exactly the Degraded shape, but the reader does not count. TotalLoss requires every core site unreachable, not one site of any kind. NoPrimary describes the opposite situation — no writable site at all, every core site read-only — and here iad is writable. (objective 8)

Question 4 of 5

Your counter application has just written through mysql-playground-primary. status.sites[] shows pdx with replicating: true and secondsBehindSource: 0, so that write is certainly present on pdx.

Show answer

Answer: False

The reversal: a zero does not prove the replica is caught up. Seconds_Behind_Source compares the last transaction the replica has executed against the last event it has downloaded, so it reads 0 when the receiver thread has stalled or the replica is simply idle — a replica that stopped fetching an hour ago can still report 0. That is why the check in this topic is to go and read the row: SELECT value, updated_at FROM counter_db.counters WHERE id = 1 against pdx by name. Nothing in the status block substitutes for reading the data. (objectives 8, 9)

Question 5 of 5

In one status dump pdx shows secondsBehindSource: 0; in another, pdx has no secondsBehindSource key at all. What does each tell you, and what is the trap in treating them as the same?

Show answer

Answer:

secondsBehindSource: 0 is a reported measurement — MySQL answered the replication probe and gave a lag of zero seconds. An absent key means no value was reported: MySQL returned NULL, which is what it does when replication is not running, or the operator never probed that site at all, which is the case for the writable primary. Treating absence as zero reads a site that is not replicating as a site that is perfectly caught up — precisely backwards. Absence means unmeasured, not good.

A full-credit answer shows: A strong answer covers: (1) 0 is a value MySQL actually reported; (2) an absent key is a null/unreported value, arising when replication is not running or when the site was never probed — the primary is never probed; (3) the failure mode is inverted meaning, reading 'unmeasured' as 'zero lag'. Credit also for noting the field is optional in the status schema, so it is omitted rather than serialised as null.

The two look alike in a quick scan and mean opposite things. A reported 0 says the probe ran; an absent key says it did not, or MySQL had nothing to report. On a healthy group the primary's entry is the everyday example of the absent form, which is why it is worth learning on a healthy cluster rather than in an incident. (objective 8)

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.