Schedule icon
Webhook icon
Commands icon
Docker icon
If icon
Fail icon
Set icon
SlackIncomingWebhook icon
Pause icon

Find and Fix Silent MySQL Replica Drift with pt-table-checksum and an Approval

Detect rows where MySQL replicas differ from the primary with pt-table-checksum, preview the pt-table-sync fix, apply it after approval and re-check.

Categories
Infrastructure

A MySQL replica can be "healthy" (both threads running, zero lag) and still hold different data than the primary: a write made directly on the replica, a crash with an unsafe setting, a replica rebuilt from a broken backup, a statement that is not deterministic. Nobody notices until a read from that replica returns the wrong row, or until it is promoted and becomes the truth. Replication lag monitors cannot see it; only comparing the data can.

This blueprint runs Percona Toolkit's standard procedure end to end. pt-table-checksum compares every table chunk by chunk through replication itself, so each replica computes its own checksum of exactly the rows the primary checksummed. When something differs, pt-table-sync previews the statements that would repair each replica, a person approves them, and they run on the primary, from where they replicate, so the fix never writes to a replica directly. The tables that differed are then checksummed again to prove they match.

How it works

  1. weekly_check (Schedule, shipped disabled) or after_incident (Webhook, after a failover or a replica rebuild). concurrency limit 1.
  2. preflight (shell.Commands, Percona Toolkit image):
    • every replica answers and both replication threads run; a stopped applier is reported with the error from performance_schema.replication_applier_status_by_worker (the table and the row), not just "a worker stopped";
    • no replica has replicas of its own (they would copy its checksum results instead of computing theirs);
    • errant GTIDs: transactions a replica has and the primary never ran, the usual sign of direct writes;
    • tables without a PRIMARY or UNIQUE key, which pt-table-sync cannot repair;
    • the replicas are written to percona.dsns, so exactly the listed ones are checked.
  3. replicas_ready (If + Fail) stops before any checksum when a replica cannot take part.
  4. checksum: pt-table-checksum with --max-lag (it pauses while a replica lags) and --chunk-time, then the documented diff query on percona.checksums on every replica. Its exit code 16 means "differences found" and is this flow's result, not an error.
  5. any_drift (If):
    • none: kv.Set (last clean check) and Slack;
    • otherwise preview_fix: pt-table-sync --replicate --sync-to-source --print per replica, with the statements saved as fix-*.sql outputs. Over max_fix_statements the flow refuses: rebuild that replica instead.
  6. request_approval (Slack: what differs, how many REPLACE and DELETE statements, the first lines) and approve_fix (Pause, 4 h).
  7. apply_fix: pt-table-sync --execute through the primary, then pt-table-checksum --tables on the tables that differed. fixed (If) reports success or fails with what still differs.
  8. errors → one Slack alert with the task, the reason and the last clean check.

What you get

  • Messages from the test runs:
    • "MySQL replicas consistent: 3 table(s), 52002 rows of shop identical on 127.0.0.1:3311,127.0.0.1:3312 and the primary 127.0.0.1:3310."
    • "MySQL replica drift: 127.0.0.1:3311: shop.orders (1 chunk); 127.0.0.1:3312: shop.customers (1 chunk). Fix planned by pt-table-sync: 127.0.0.1:3311: 6 statement(s) (5 REPLACE, 1 DELETE, 0 INSERT); …"
    • "MySQL replica drift fixed: … applied through the primary. A new checksum of shop.customers,shop.orders matches on every replica."
    • "FAILED at not_checkable: 127.0.0.1:3311 has 1 replica(s) of its own: with row-based binlogs they would copy its checksums instead of computing theirs."
  • outputs.result: the checksum totals and differences, the planned fix, and the result of the re-check. The planned statements are in the preview_fix output files.
  • The KV entry mysql_replica_checksum_last_clean.

Error handling

  • Nothing is repaired without an approval, and nothing at all is changed when the fix is declined, not answered within 4 hours, or larger than max_fix_statements.
  • A replica that cannot take part stops the check before it starts: not answering, not replicating (with the applier's error), or chained.
  • The fix never writes to a replica. pt-table-sync runs REPLACE and DELETE on the primary with the primary's own values, so they are no-ops there and repair the replica as they replicate; GTIDs stay consistent.
  • The repair is verified. If a table still differs after the fix, the run fails and says why: a table without a unique key, or something still writing to the replica directly.
  • Credentials go to a defaults file inside the task container, never onto a command line.
  • Every failure sends one Slack alert with the reason and the date of the last clean check.

What this blueprint teaches about Kestra

  • Exit codes as data: a tool whose non-zero exit status means "found something" is handled in the script, so the flow decides with If instead of failing the task.
  • outputFiles for review: the exact SQL a person approves is attached to the execution.
  • Pause with a typed onResume input, nested Ifs for the clean, refused, declined and fixed paths, and kv.Set to remember the last clean check.

Prerequisites

  • MySQL 8.0 with replicas (GTIDs recommended). A user that can connect to the primary and every replica with SELECT, PROCESS, SUPER (or SYSTEM_VARIABLES_ADMIN), REPLICATION SLAVE, REPLICATION CLIENT, and ALL on the percona database; pt-table-sync also needs INSERT, UPDATE, DELETE on the checked databases.
  • A Kestra worker that can run Docker containers and reach the servers (the tasks use networkMode: host).
  • A Slack incoming webhook.

Secrets

  • MYSQL_USERNAME, MYSQL_PASSWORD: the user above.
  • SLACK_WEBHOOK_URL.
  • MYSQL_CHECKSUM_WEBHOOK_KEY: a long random string for the webhook.

Inputs

  • primary and replicas (host:port), databases.
  • max_lag_seconds (5), chunk_time (0.5), max_fix_statements (1000), toolkit_image (percona/percona-toolkit:3.7.1).
  • The approval waits 4 hours (pauseDuration of approve_fix).

Outputs

  • result (JSON), described above.

Quick start

  1. Point primary, replicas and databases at your servers, add the secrets, and run. A healthy set of replicas reports "consistent" with the number of tables and rows compared.
  2. To see a repair, make a replica drift on purpose: SET SESSION sql_log_bin = 0; UPDATE shop.orders SET total = total + 1 WHERE id = 10; on the replica, run again, read the plan in Slack and resume with Apply the fix through the primary now? checked.
  3. Real result from the test servers: 6 statements for one replica and 1 for the other, applied through the primary, and the re-check of both tables matched.

How to read the result

  • Errant GTIDs stay after a fix. pt-table-sync repairs the rows, not the replica's GTID history, so the warning keeps appearing until the errant transactions are dealt with: inject empty transactions with those GTIDs on the primary if the writes were harmless, or rebuild the replica. Until then, a failover to that replica would send them to the others.
  • A table "without a PRIMARY or UNIQUE key" is still checksummed, so drift in it is reported; it just has to be repaired by hand.
  • A diff of one chunk in a big table means the drift is within that chunk's key range; percona.checksums on the replica shows the boundaries.

How to extend

  • Checksum one large database per run with --databases and a Loop, so each run stays inside a maintenance window.
  • Add --where per table to check only recent rows on huge append-only tables.
  • Point the webhook at your failover tooling, so every promoted replica is checked before it takes writes.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.