mysql semi-sync failover harness
mysql semi-sync failover harness
This is the harness behind The Commits That Didn’t Survive the Failover.
This harness runs a digest-pinned MySQL 8.0 primary and a GTID replica and measures what a failover actually loses under asynchronous vs semi-synchronous replication.
The mechanism: with plain async replication the primary commits and ACKs the
client without waiting for the replica, so a primary crash can lose transactions
the client was already told had succeeded. Semi-synchronous replication
(AFTER_SYNC) makes each commit block until a replica acknowledges it has
received (relay-logged) the transaction, before the client sees success. That
protects the transmit even when the replica hasn’t applied the change yet.
Four experiments, all on the same schema (bench.t, an auto-increment table):
- A. async failover loss — plain async, then
STOP REPLICA IO_THREADon the replica to induce a deterministic receipt gap (this models a replica whose receipt has fallen behind, disclosed honestly). Burst N acked inserts into the primary,docker killthe primary, promote the replica, count survivors. - B. semi-sync failover — semi-sync ON, then
STOP REPLICA SQL_THREADonly, so the replica still acks receipt into its relay log but hasn’t applied yet. Burst N acked inserts (each commit blocks on the replica’s receipt ack),docker killthe primary,START REPLICA SQL_THREADto drain the relay log, count survivors. - C. latency cost — per-commit wall-clock latency for M single-row inserts, once async and once semi-sync, both with the replica applying normally. Reports p50/p95/p99/max.
- D. semi-sync timeout fallback (kept under
results/attempts/) — semi-sync on,STOP REPLICA IO_THREADso nothing can ack; time how long a commit stalls before the primary falls back to async (rpl_semi_sync_source_timeout), and confirmRpl_semi_sync_source_statusflips OFF.
Each experiment recreates the cluster fresh (docker compose down -v + up).
The harness drives Docker via subprocess so it can hard-kill the primary
mid-run. Containers are torn down on exit.
These are laptop measurements demonstrating the mechanism, not production numbers. In particular, primary and replica run on the same host, so the semi-sync ack round-trip in experiment C is sub-millisecond — the latency cost is in the noise here and is what widens on a real network.
Run it
Docker with Compose v2, plus Python 3.9+.
cd benchmarks/mysql-semisync-failover
docker compose up -d --wait # primary on :3307, replica on :3308 (loopback only)
python3 -m venv /tmp/mysql-venv && source /tmp/mysql-venv/bin/activate
pip install -r requirements.txt
python benchmark.py | tee results/summary.txt
# benchmark.py recreates the cluster fresh per experiment and tears everything
# down on exit; the manual `up` above is just a connectivity smoke test.
The harness manages its own cluster lifecycle. If you interrupt it, clean up
with docker compose down -v --remove-orphans.
Results
summary.txt— the captured console run used in the post (MySQL 8.0.46).failover_loss.csv— acked / present-on-replica / lost for experiments A and B.latency_percentiles.csv— p50/p95/p99/max per mode from experiment C.latency_async.csv,latency_semisync.csv— raw per-commit latencies (ms) so charts can be recomputed.run_metadata.csv— MySQL version, image digest, params, and headline numbers.attempts/experiment_d_timeout.txt— the semi-sync timeout stall (kept underattempts/because a fixed ~10s wall is a coarse, timing-sensitive result).
The mechanism (async ACKs before the replica has the data; semi-sync AFTER_SYNC
waits for receipt) is not host-specific. What a single-laptop run understates is
the latency cost of semi-sync, since there is no network distance between the
nodes.