Backups and the binlog archiver

Where the dump comes from, why a reader can never supply it, and how a sealed binlog gets from a rotate on the primary into object storage — plus the tail that is gone forever.

By the end of this topic you can

  1. Choose S3 or PVC storage for playground and say what PVC-local costs you
  2. Say which site a backup runs from and why a read-only reader is never eligible
  3. Trace a sealed binlog from rotation to object storage, and name what can never be archived

playground runs on three sites — iad, pdx, and reader — with a counter application reading and writing through it. The failover story is complete: a primary dies, a candidate is writable about 12 seconds later, and you can read the exact loss from divergentGtid. None of that survives losing the cluster. None of it survives a bad DELETE at 14:02 last Tuesday either — that statement replicates to every site in milliseconds, and every fence you built preserves it faithfully. Backups and point-in-time recovery are a separate subsystem with separate failure modes.

Path from a rotate on the primary, to a sealed binlog file, through the sidecar, into object storage. The last open file is greyed out and labelled still being written — never archived.
Only sealed files ship. The active binlog is still being written, so it is never archived.

Storage first, and be blunt about it

spec.backup.profiles[].storage is a tagged union with exactly two arms, S3 and PVC, and a CEL rule that rejects a body which does not match the tag. That is the whole choice, and it is the most consequential one in the block (objective 1).

CompareBackup storage for playground
storage.type: S3storage.type: PVC
Where do the objects land?A bucket you name, optionally behind endpointURL for MinIO, Ceph or WasabiA PersistentVolumeClaim — one you name, or one the operator provisions per profile
Shared failure domain with the data?No — a separate service with its own durability contractYes — the same cluster, often the same storage class, sometimes the same node
Survives loss of the cluster?Yes, if the credentials and the bucket are outside itNo
Reasonable useProduction protection for playgroundPlayground, staging a dump you are about to move elsewhere

A PVC-local backup is not durable. A backup that shares a failure domain with the data it protects is an assumption, not a backup — the event that takes your PVCs takes the copy with it. Choose PVC when you want a fast local artefact, never as the thing that saves you.

Which site the job runs on

This is the part operators get wrong. selectSourceSite prefers a replica and falls back to the primary, and it records which happened as one of three reason strings you will see in status: "override", "replica-preferred", "primary-fallback" (objective 2).

"replica-preferred" is not a preference for “anything that is not the primary”. A candidate replica qualifies only if its observed state is read-only, it is actually replicating, and its secondsBehindSource is at or below maxLagSecondsForSource — default 300. Above that gate, the stale replica loses and the active site takes the job as "primary-fallback", which itself requires the primary to be writable and promotable. Do not confuse the gate with spec.replication.maxLagSeconds, which also defaults to 300 but drives only the ReplicationLagging condition and never picks a backup source.

Sites with role: read-only are excluded from the replica pool outright. An explicit sourceSiteOverride naming one is not silently ignored — it is rejected:

sourceSiteOverride "reader" names a read-only site, which cannot be a backup source

The reader is the obvious place to put a dump, and it is the one site you cannot. This is the same logic you met in Unit 1 wearing different clothes: a site that is not a primary-candidate is not authoritative, so it is not promotable — and here, not sourceable.

The artifact

Add to playground:

spec:
  backup:
    maxLagSecondsForSource: 300          # default, shown for clarity
    profiles:
      - name: nightly
        storage:
          type: S3                       # not PVC — see above
          s3:
            bucket: playground-backups
            prefix: playground
            endpointURL: http://minio.bloodraven-playground:9000
            credentialsSecret: playground-backup-s3
        retention: 7                     # default
    pitr:
      enabled: true                      # NEW — turns the archiver on
      profileName: nightly
      maxBinlogSize: 100M                # default when PITR is enabled
      archivePollInterval: 60s           # default

The archiver

Enabling PITR starts a binlog archiver inside the per-site sidecar. One mechanism is worth teaching properly, because the reason is the lesson: the archiver watches the directory, not mysql-bin.index. MySQL rewrites that index atomically — write .index.tmp, rename over the top — so an inode-level watch on the index file would survive exactly zero rotations. Watch the directory and the rename is an event you receive rather than a watch you lose.

inotify is an optimisation, not the mechanism. A ticker runs alongside it at archivePollInterval (default 60s), and a best-effort scan runs once at startup to sweep up binlogs produced while the sidecar was offline. If inotify is unavailable — a FUSE-mounted volume, say — the archiver drops to poll-only. Rotation is then detected with worse latency, up to 60 seconds, but never missed.

Every cycle starts with a role gate: the archiver reads @@read_only and, on a replica, clears its error, zeroes its backlog and returns. Only the primary archives. That single check is the whole post-failover story — when pdx is promoted, its archiver’s next scan finds read_only=0 and starts uploading. No extra wiring, no operator intervention.

Then the rule that surprises people: only sealed binlogs upload. The last entry in the index is the file MySQL is writing to right now, and the archiver drops it. Fewer than two entries means nothing is sealed and the cycle is a no-op. So the unarchived tail is exactly the current file’s contents, bounded by maxBinlogSize — default 100M, applied only when PITR is enabled and written into the generated my.cnf before spec.mysqlConf is merged, so your override still wins. Contrast that with gtid-mode, log-bin and log-replica-updates, which are written after overrides precisely so nothing can weaken them.

FlowOne binlog, rotation to prune
MySQL seals mysql-bin.000041, opens .000042, and rewrites mysql-bin.index by writing .index.tmp and renaming it.
The directory watch delivers the rename. The archivePollInterval ticker (60s) would have caught it anyway.
Read @@read_only. On a replica: clear error, backlog 0, return. Only the primary continues.
Read the index, drop the last entry — .000042 is the active file — and diff the rest against the site manifest.
Put .000041 under binlogs//, then append the manifest entry. Manifest after upload, so a failure never leaves a row pointing at a 404.
GET /pitr-cutoff. Entries whose lastEventTime precedes the cutoff are removed from the manifest and deleted from storage.

Pruning deserves honesty. The archiver asks the operator for the cutoff over /pitr-cutoff?namespace=&group=&profile=, at most once per sweep interval (default one hour), piggybacked on an archive scan rather than a second ticker. Errors are logged and deliberately not raised into archiver status, so a transient 503 from the operator does not turn your PITR health red. And if the retention config is absent — no operator address wired into the sidecar — maybeRunRetention returns immediately and there is no pruning at all. Archived binlogs accumulate until you notice the bill.

What you cannot recover

Two hard limits, and neither is a bug (objective 3). On PVC loss, the previously-active binlog lived on the destroyed PVC. It is gone forever, along with every transaction in it — PITR narrows your RPO to the rotation cadence only if the tail survives. And PITR cannot reach past the async-replication cutoff: transactions the old primary committed but never shipped are not in the replica’s binlog stream, and therefore not in PITR’s replay material. Restoring from the survivor cannot conjure writes the survivor never saw.

Now the sting. A backup storage failure has no data-plane impact whatsoever. MySQL keeps serving reads and writes, the counter keeps counting, playground stays Healthy, and your PITR RPO drifts backwards in silence for as long as nobody looks. That is the textbook silent degradation, and it is exactly why Unit 6 has an alerting topic.

You can now configure backups and PITR for playground, name the site a backup ran from and the reason string that explains it, and state precisely what is not recoverable. What you cannot yet do is prove any of it works — the artefact in the bucket is untested until something loads it. That is the next question.

Flashcards

Backup source reason string "replica-preferred" — what had to be true for the operator to record it?

The chosen site's observed state was read-only, it was actually replicating, and its secondsBehindSource was at or below maxLagSecondsForSource.

1 / 12

Quiz

Question 1 of 5

A scheduled backup of playground fires. iad is the active site and is writable; pdx is read-only and replicating but reports secondsBehindSource of 900, well past the 300-second default; reader is idle with role: read-only. Which site runs the dump, and what reason string lands in status?

Show answer

Answer: iad, reason "primary-fallback"

Replica-first is conditional, not unconditional: a replica qualifies only while secondsBehindSource is at or below maxLagSecondsForSource (default 300). At 900 seconds pdx fails the gate, so option 1 is wrong. reader carries role: read-only and is excluded from the replica pool outright — the tempting 'spare site' is the one site that can never source a backup, so option 3 is wrong. Option 4 inverts the design: the fallback exists precisely so a stale replica does not block the backup, and iad is writable and promotable, so it takes the job and status records "primary-fallback" (objective 2).

Question 2 of 5

To keep dump load off both primary candidates, you set sourceSiteOverride: reader on a MysqlBackup for playground. What happens?

Show answer

Answer: Source selection returns an error saying the override names a read-only site, which cannot be a backup source

Options 1 and 2 assume the override is a trump card over the role; it is not. Role is checked first, and a read-only reader is rejected with an explicit error rather than being dumped from — the same non-promotable-therefore-not-authoritative logic from Unit 1, showing up here as non-sourceable. Option 4 is the quieter and more dangerous misconception: rejection is loud and no Job is created, so you find out immediately instead of silently getting a source you did not ask for. A reader is a legitimate site to read from with a client, but never a site Bloodraven will hand a backup job to (objective 2).

Question 3 of 5

A clean archiver scan has just completed on the primary of playground. mysql-bin.index lists mysql-bin.000041 through mysql-bin.000045. Which file is not in object storage, and why?

Show answer

Answer: mysql-bin.000045 — it is the active binlog, the tail of the index, and the archiver drops it

The archiver reads the index and drops the last entry, because that is the file MySQL is writing to right now — only sealed binlogs upload. Option 1 confuses archival with pruning: pruning is driven by a cutoff timestamp fetched from /pitr-cutoff, is rate-limited, and would remove the object from storage and the manifest, not leave it un-uploaded. Option 3 is the assumption that makes people over-trust their RPO — there is always an unarchived tail on a live primary. Option 4 invents a safety margin the archiver does not have; sealed files are safe to read the moment they are sealed, and holding one back would only widen the gap (objective 3).

Question 4 of 5

PITR is enabled on playground and archival is healthy. The primary's node and its PVC are destroyed. A PITR restore can still replay right up to the last transaction the primary committed.

Show answer

Answer: False

The reversal: healthy archival does not mean complete archival. The previously-active binlog — everything written since the last rotate — lived on the destroyed PVC and is gone forever, so the replay material stops at the last sealed file that reached storage. The second limit compounds it: restoring from the surviving replica cannot reach past the async-replication cutoff, because transactions the old primary committed but never shipped were never in the replica's binlog stream. PITR narrows RPO to the rotation cadence only when the tail survives, which is one of the real costs of any backup that shares, or fails with, the data's own storage (objectives 1, 3).

Question 5 of 5

You want to shrink the unarchived tail on playground — the window of committed transactions that PITR would lose if the primary's PVC were destroyed right now. What do you change, and what does it cost you?

Show answer

Answer:

Lower spec.backup.pitr.maxBinlogSize below its 100M default. It is forwarded to MySQL as max_binlog_size, so smaller files rotate more often; each rotation seals the current file and makes it eligible for upload, so the active, never-uploaded file holds less. The cost is many more objects in storage and more upload churn. It is written before spec.mysqlConf is merged, so a spec.mysqlConf entry for the same key would override it.

A full-credit answer shows: A strong answer names maxBinlogSize (default 100M) as the control and explains the mechanism: only sealed binlogs upload, the active file is dropped, so faster rotation means a smaller unarchived tail. It should name a cost — object count, upload churn — and should not reach for archivePollInterval or maxLagSecondsForSource instead. Bonus credit for noting the before-overrides ordering, or that no setting removes the tail entirely.

archivePollInterval (60s) only changes how soon a sealed file is noticed, not how much data sits in the unsealed one, and inotify usually beats the ticker anyway. maxLagSecondsForSource (300) chooses which site runs a full dump and has nothing to do with binlogs. Rotation cadence is the only lever on the tail, and it never reaches zero — there is always a file MySQL is currently writing to (objective 3).

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.