MySQLReplication

MySQL Replication

MySQL replication allows data from one database server (the source, formerly called master) to be automatically copied to one or more database servers (replicas, formerly called slaves). It is the foundation of read scaling, high availability, and disaster recovery in MySQL deployments.

How Replication Works
  1. The source records every data-changing event to its binary log (binlog).

  2. The replica's I/O thread connects to the source and copies new binlog events into its own relay log.

  3. The replica's SQL thread reads the relay log and replays the events on the replica's data.

Replication Topology Options

Topology

Description

Best for

Single source / single replica

1 source, 1 replica for failover

Simple HA with a hot standby

Single source / multiple replicas

1 source, N replicas for reads

Read scaling — distribute SELECT load

Chain (cascading) replication

Source -> Replica A -> Replica B

Distributing replication across networks/regions

Multi-source replication

One replica receives from multiple sources

Data consolidation, merging shards

Circular (multi-master)

Source A <-> Source B both replicate to each other

Active-active setups (high complexity, risk of conflict)

Bash
# Single source / multiple replicas (most common read-scaling pattern)
#
#        [Source]
#        /      \
#   [Replica1] [Replica2]
#
# Writes go to Source only; reads distributed across replicas
# Each replica has server_id = 2, 3, etc.
Binary Log Format Comparison

Format

What is Logged

Log Size

Safety

Best for

STATEMENT

The SQL statement itself

Small

Unsafe for non-deterministic functions (NOW(), UUID())

Compactness on simple workloads

ROW

Actual row data (before + after images)

Large

Exact and safe for all statements

Production default (MySQL 8.0 default)

MIXED

STATEMENT by default, ROW for unsafe statements

Medium

Good balance, can be unpredictable

Legacy setups migrating to ROW

Note
MySQL 8.0 defaults to ROW format, which is the safest choice for most production deployments. ROW format replication is immune to non-deterministic function drift.
GTID Deep-Dive

GTID (Global Transaction Identifier) assigns a globally unique ID to every committed transaction across the entire replication topology. This makes failover and replica re-pointing dramatically simpler.

SQL
-- GTID format: source_uuid:transaction_id_range
-- Example: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100
-- Meaning: transactions 1 through 100 from source with UUID 3E11FA47...

-- Check the GTIDs executed on this server
SHOW VARIABLES LIKE 'gtid_executed';

-- Check the GTID mode values
SHOW VARIABLES LIKE 'gtid_mode';
-- OFF          = GTID disabled
-- OFF_PERMISSIVE = Accepts both GTID and non-GTID transactions (migration step 1)
-- ON_PERMISSIVE  = Generates GTIDs, accepts both (migration step 2)
-- ON          = Full GTID mode (required for GTID replication)

-- Switching from position-based to GTID replication (online migration, MySQL 5.7.6+)
-- Step 1: source and all replicas
SET @@GLOBAL.GTID_MODE = OFF_PERMISSIVE;
-- Step 2:
SET @@GLOBAL.GTID_MODE = ON_PERMISSIVE;
-- Step 3: wait for Ongoing_anonymous_transaction_count = 0
SHOW STATUS LIKE 'Ongoing_anonymous_transaction_count';
-- Step 4:
SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = ON;
SET @@GLOBAL.GTID_MODE = ON;
Setting Up Replication (MySQL 8.0+)

Step 1 — Configure the source in my.cnf:

Bash
[mysqld]
server_id                 = 1
log_bin                   = /var/log/mysql/mysql-bin.log
binlog_format             = ROW
binlog_row_image          = MINIMAL   # reduce log size while staying safe
gtid_mode                 = ON
enforce_gtid_consistency  = ON

Step 2 — Configure the replica in my.cnf:

Bash
[mysqld]
server_id                 = 2
gtid_mode                 = ON
enforce_gtid_consistency  = ON
read_only                 = ON
super_read_only           = ON   # blocks even SUPER users from writing

Step 3 — Create a replication user on the source:

SQL
CREATE USER 'replicator'@'%'
  IDENTIFIED WITH caching_sha2_password BY 'str0ngP@ss!';

GRANT REPLICATION SLAVE ON *.* TO 'replicator'@'%';
FLUSH PRIVILEGES;

Step 4 — Take a consistent backup and restore on replica:

Bash
# On source — consistent dump with GTID info
mysqldump --single-transaction --set-gtid-purged=ON   --all-databases -u root -p > full_backup.sql

# Transfer to replica, then restore
mysql -u root -p < full_backup.sql

Step 5 — Connect the replica to the source:

SQL
-- MySQL 8.0.23+ syntax
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST          = '192.168.1.10',
  SOURCE_PORT          = 3306,
  SOURCE_USER          = 'replicator',
  SOURCE_PASSWORD      = 'str0ngP@ss!',
  SOURCE_AUTO_POSITION = 1;    -- GTID auto-positioning (no manual file/offset needed)

START REPLICA;
Monitoring: SHOW REPLICA STATUS

SQL
SHOW REPLICA STATUSG
*************************** 1. row ***************************
             Replica_IO_Running: Yes
            Replica_SQL_Running: Yes
          Seconds_Behind_Source: 0
              Last_IO_Errno: 0
              Last_IO_Error:
             Last_SQL_Errno: 0
             Last_SQL_Error:
    Executed_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5023
Replica Lag — Causes and Solutions

Seconds_Behind_Source measures how far behind the replica is. It is the timestamp difference between the current SQL thread event and the current clock time — not necessarily the number of seconds of data missed.

  • Heavy write load on source that the single SQL thread cannot keep up with

  • Long-running queries on the replica blocking the SQL thread

  • Network latency between source and replica

  • Replica hardware (I/O, CPU) weaker than source

  • Large transactions (e.g. a bulk UPDATE of millions of rows) — single event, long to replay

Bash
# my.cnf — enable multi-threaded parallel replication (MySQL 5.7+)
[mysqld]
replica_parallel_workers      = 8       # number of parallel SQL applier threads
replica_parallel_type         = LOGICAL_CLOCK  # better parallelism than DATABASE mode
replica_preserve_commit_order = ON      # maintain commit order for GTID consistency
Tip
Use pt-heartbeat from Percona Toolkit for more accurate replica lag measurement than Seconds_Behind_Source, which can show 0 even when the replica is behind on large transactions.
Semi-Synchronous Replication

By default MySQL replication is asynchronous: the source commits and moves on without waiting for any replica. With semi-synchronous replication, the source waits until at least one replica acknowledges receiving (but not necessarily applying) the events before returning success to the client. This prevents data loss on source failure at the cost of added latency.

SQL
-- Install plugin on BOTH source and replica
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';

-- Enable on source
SET GLOBAL rpl_semi_sync_source_enabled = 1;
-- Fall back to async after 1 second if no replica acknowledges (prevents blocking forever)
SET GLOBAL rpl_semi_sync_source_timeout = 1000;
-- Require at least 1 replica to ack (increase for stricter durability)
SET GLOBAL rpl_semi_sync_source_wait_for_replica_count = 1;

-- Enable on replica
SET GLOBAL rpl_semi_sync_replica_enabled = 1;

-- Restart the I/O thread to activate semi-sync
STOP REPLICA IO_THREAD;
START REPLICA IO_THREAD;

-- Monitor semi-sync status
SHOW STATUS LIKE 'Rpl_semi_sync%';
Note
Semi-synchronous replication guarantees the source has received acknowledgment that at least one replica has the binlog events before committing. It does NOT guarantee the replica has applied them to its data.
Common Replication Errors and Fixes

Error

Error Number

Common Cause

Fix

Duplicate entry

1062

Row already exists on replica (perhaps inserted directly)

Skip event or re-sync replica

Row not found

1032

Row was deleted on replica but source tries to update it

Skip event or re-sync replica

Access denied

1045

Replication user password changed or revoked

Update CHANGE REPLICATION SOURCE password

Relay log corrupted

1594

Disk issue or crash mid-write

STOP REPLICA; RESET REPLICA; re-provision

GTID skip needed

N/A

Specific GTID transaction needs to be skipped

Inject empty transaction for that GTID

SQL
-- Skip a single erring event (position-based replication)
STOP REPLICA;
SET GLOBAL sql_replica_skip_counter = 1;
START REPLICA;

-- Skip a specific GTID transaction (GTID replication)
-- Find the problematic GTID from SHOW REPLICA STATUS: Last_SQL_Error_Timestamp
STOP REPLICA;
SET GTID_NEXT = '3E11FA47-71CA-11E1-9E33-C80AA9429562:101';  -- the failing GTID
BEGIN; COMMIT;     -- inject an empty transaction to mark this GTID as executed
SET GTID_NEXT = 'AUTOMATIC';
START REPLICA;

-- Verify replication resumed
SHOW REPLICA STATUSG
Warning
Skipping events breaks data consistency between source and replica. After skipping, validate the affected tables and consider a full re-sync if the replica will be promoted to source.
Parallel Replication

Bash
# LOGICAL_CLOCK mode: transactions that committed in overlapping binary log groups
# on the source can be applied in parallel on the replica.
# This matches the source's concurrency more accurately than DATABASE mode.
[mysqld]
replica_parallel_workers      = 8
replica_parallel_type         = LOGICAL_CLOCK
replica_preserve_commit_order = ON

SQL
-- Verify parallel workers are active
SHOW PROCESSLIST;
-- Should show multiple 'replica sql' threads

-- Monitor parallel replication efficiency
SHOW GLOBAL STATUS LIKE 'Replica_transactions_multi_threaded_retries';
-- High retries = conflicts between parallel workers; reduce workers or switch to DATABASE mode
Promoting a Replica to Source (Failover)
  1. Stop writes to the current source (or it has already crashed).

  2. On the chosen replica: verify Seconds_Behind_Source = 0 in SHOW REPLICA STATUS.

  3. On the chosen replica: run STOP REPLICA; RESET REPLICA ALL;

  4. On the chosen replica: run SET GLOBAL read_only = OFF; SET GLOBAL super_read_only = OFF;

  5. Point all other replicas at the new source: CHANGE REPLICATION SOURCE TO SOURCE_HOST = 'new-source', SOURCE_AUTO_POSITION = 1;

  6. Update application connection strings / load balancer / MySQL Router to point at the new source.

  7. Monitor for replication lag on the remaining replicas.

Tip
Use tools like MySQL Router, Orchestrator, or ProxySQL to automate failover and transparent application re-routing. Manual failover is error-prone and slow under pressure.
Read Scaling with Replicas

Bash
# Application config example (Node.js / mysql2 pool cluster)
const pool = mysql.createPoolCluster();

pool.add('WRITE', {
  host: 'db-source.internal',
  user: 'app', password: 'secret', database: 'myapp'
});
pool.add('READ1', {
  host: 'db-replica-1.internal',
  user: 'app', password: 'secret', database: 'myapp'
});
pool.add('READ2', {
  host: 'db-replica-2.internal',
  user: 'app', password: 'secret', database: 'myapp'
});
Warning
Do not immediately read your own write from a replica — replica lag can cause stale reads. Either read from the source for post-write reads, or use semi-synchronous replication with SESSION_TRACK_GTIDS to wait for the write's GTID on the replica.
Key Replication Variables Reference

Variable

Scope

Description

server_id

Both

Unique integer ID — must differ on every server in the topology

log_bin

Source

Path/name for binary log files — enables binlogging

binlog_format

Source

ROW | STATEMENT | MIXED — ROW is safest

gtid_mode

Both

ON to enable GTID replication

enforce_gtid_consistency

Both

ON — prevent statements incompatible with GTID

read_only

Replica

Prevents accidental writes from non-SUPER users

super_read_only

Replica

Blocks even SUPER users from writing

replica_parallel_workers

Replica

Number of parallel SQL threads (0 = single-threaded)

replica_parallel_type

Replica

LOGICAL_CLOCK or DATABASE — LOGICAL_CLOCK recommended

rpl_semi_sync_source_enabled

Source

1 to enable semi-synchronous replication

binlog_row_image

Source

FULL/MINIMAL/NOBLOB — MINIMAL reduces log size

Replication Filters

You can replicate only specific databases or tables using filter options in my.cnf on the replica:

Bash
# my.cnf on replica — replicate only specific databases or tables
replicate-do-db        = myapp        # Only replicate this database
replicate-ignore-db    = test         # Skip this database entirely
replicate-do-table     = myapp.orders # Only replicate this table
replicate-ignore-table = myapp.sessions  # Skip this table
Warning
Replication filters with statement-based logging can produce unexpected results. A statement that changes a filtered database while the default database is something else may be incorrectly filtered or not filtered. Always use ROW format when using replication filters.
Multi-Source Replication

MySQL 5.7+ allows a single replica to receive replication from multiple sources simultaneously — useful for data consolidation or merging sharded databases.

SQL
-- Add a second source to an existing replica
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST          = '192.168.1.20',
  SOURCE_PORT          = 3306,
  SOURCE_USER          = 'replicator',
  SOURCE_PASSWORD      = 'pass',
  SOURCE_AUTO_POSITION = 1
FOR CHANNEL 'shard2';

START REPLICA FOR CHANNEL 'shard2';

-- Monitor each channel separately
SHOW REPLICA STATUS FOR CHANNEL 'shard2'G

-- Stop a specific channel
STOP REPLICA FOR CHANNEL 'shard2';
Best Practices
  • Always use GTID mode for new deployments — it simplifies failover dramatically.

  • Set read_only = ON and super_read_only = ON on all replicas to prevent accidental writes.

  • Monitor Seconds_Behind_Source continuously and alert when it exceeds your SLA threshold.

  • Test failover procedures regularly — a plan you have not practised is no plan at all.

  • Keep replica MySQL version equal to or newer than the source version (never older).

  • Enable binlog_row_image = MINIMAL to reduce ROW format log size while remaining safe.

  • Use semi-synchronous replication when data loss on source failure is unacceptable.

Monitoring Replication Health

SQL
-- Most important replication health checks
SHOW REPLICA STATUSG

-- Key fields and their healthy values:
-- Replica_IO_Running:     Yes
-- Replica_SQL_Running:    Yes
-- Seconds_Behind_Source:  0 (or close to 0)
-- Last_IO_Error:          (empty)
-- Last_SQL_Error:         (empty)

-- Monitor via performance_schema (MySQL 8.0)
SELECT
  channel_name,
  service_state,
  LAST_ERROR_MESSAGE,
  LAST_ERROR_TIMESTAMP
FROM performance_schema.replication_applier_status_by_worker;

-- Compare GTID sets: what has the replica executed vs what source has committed?
-- On source:
SELECT @@GLOBAL.GTID_EXECUTED AS source_gtids;

-- On replica:
SELECT @@GLOBAL.GTID_EXECUTED AS replica_gtids;

-- Find which transactions replica is missing:
SELECT GTID_SUBTRACT(
  '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-1000',   -- source
  '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-990'     -- replica
) AS missing_gtids;
-- Returns: 3E11FA47-71CA-11E1-9E33-C80AA9429562:991-1000
Replication Quick Reference

Topic

Command / Setting

Start/stop replication

START REPLICA; / STOP REPLICA;

Check status

SHOW REPLICA STATUS\G

View binlogs on source

SHOW BINARY LOGS;

GTID mode

gtid_mode = ON, enforce_gtid_consistency = ON

Prevent replica writes

read_only = ON, super_read_only = ON

Parallel workers

replica_parallel_workers = 8, replica_parallel_type = LOGICAL_CLOCK

Semi-sync enable

rpl_semi_sync_source_enabled = 1 (source), rpl_semi_sync_replica_enabled = 1 (replica)

Skip error (position)

STOP REPLICA; SET GLOBAL sql_replica_skip_counter = 1; START REPLICA;

Skip GTID error

STOP REPLICA; SET GTID_NEXT = "uuid:N"; BEGIN; COMMIT; SET GTID_NEXT = AUTOMATIC; START REPLICA;

Purge old binlogs

PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);

Binary Log Management

Binary logs grow continuously and must be managed proactively. The source server accumulates binlog files that replicas have already consumed. Purging old files frees disk space.

SQL
-- List current binary log files and their sizes
SHOW BINARY LOGS;

-- Set automatic expiry (MySQL 8.0: binlog_expire_logs_seconds)
SET GLOBAL binlog_expire_logs_seconds = 604800;  -- 7 days in seconds
-- my.cnf: binlog_expire_logs_seconds = 604800

-- Manual purge: delete logs older than a specific date
PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);

-- Manual purge: delete logs up to a specific file
PURGE BINARY LOGS TO 'mysql-bin.000050';

-- Check how much disk the binary logs are using
SELECT
  SUM(file_size) / 1024 / 1024 AS total_binlog_mb
FROM information_schema.FILES
WHERE file_type = 'BINARY LOG';

-- IMPORTANT: before purging, verify all replicas have consumed those logs
-- Check each replica's relay log position to ensure it is past the logs you will delete
SHOW REPLICA STATUSG  -- check Executed_Gtid_Set or Read_Master_Log_Pos