Skip to content

MySQL 主从同步原理与延迟排查

问题:为什么主从复制会延迟,怎么查、怎么修?

主从复制是 MySQL 高可用架构的基石——读写分离、灾备、在线变更都靠它。但几乎所有维护过 MySQL 的人都被「主从延迟」坑过:读请求打到从库拿到的是脏数据,监控报警响个不停,DBA 紧急上线处理。

本文从复制原理出发,先讲清楚三个线程如何协作、binlog 格式的影响,再聚焦延迟的根因排查和实际解法,最后给出一个可直接落地的指标体系。

一、主从复制三线程模型

MySQL 主从复制本质上是异步的 binlog 分发 + 重放。参与方只有三个线程:

主库                      从库
┌──────────────┐        ┌──────────────────┐
│  提交事务     │  ──→   │  IO Thread       │
│  ↓            │ binlog │  ↓               │
│ Binlog Dump   │───────→│ Relay Log        │
│ Thread        │        │  ↓               │
└──────────────┘        │  SQL Thread      │
                        │  ↓               │
                        │  应用数据         │
                        └──────────────────┘
  • Binlog Dump Thread(主库):事务提交后写入 binlog,Dump 线程负责顺着 binlog 位置把事件推送给从库。主库为每个从库启动一个独立的 Dump 线程——如果有 3 个从库,主库上就有 3 个 Dump 线程。
  • IO Thread(从库):接收事件,写入 relay log。这一步只涉及网络 I/O 和顺序写,速度通常很快。
  • SQL Thread(从库):读取 relay log 并重放 SQL。这是整个复制链路中最慢的一环,也是延迟的起点。

核心矛盾:主库可以多线程并发写,但从库的 SQL Thread 在 5.6 之前是单线程,5.6 之后才逐步引入并行回放。写入量一大,SQL Thread 必然跟不上。

1.1 binlog 格式对复制的关键影响

不少开发者只关心 binlog 开没开,却不关心格式。三种格式对复制行为影响巨大:

格式记录内容体积从库重放行为延迟风险
STATEMENT原始 SQL从库重新执行 SQL,依赖上下文高——NOW()LIMIT 等非确定性函数可能产生不一致
ROW(5.7+ 默认)每行变更前/后快照大(单条 UPDATE 影响 1000 行则记录 1000 行前后镜像)直接应用行变更,无需上下文低——但 binlog 体积大,网络传输压力大
MIXED自动切换根据语句是否确定性自动选择中——复杂场景下会自动降级为 ROW

生产坑:某团队把 binlog_format=STATEMENT 跑在读写分离架构上,从库执行 DELETE FROM t LIMIT 1 时因为主从数据排序不同,删了不同的行,导致主从数据永久不一致。线上必须用 ROW 格式,这是硬性红线。

ROW 格式的代价是 binlog 体积膨胀。实测:同样的 UPDATE t SET c=1 WHERE id IN (SELECT id FROM t2 WHERE ...) 影响 5000 行,STATEMENT 格式 binlog 约 2KB,ROW 格式约 1.2MB——差了 600 倍。所以网络带宽也成了延迟瓶颈。

二、延迟的成因分析

2.1 大事务——最直接的元凶

一个 DELETE FROM orders WHERE create_time < '2024-01-01' 删了 200 万行,主库可能 2 秒完成,但 ROW 格式 binlog 里记录了 200 万条行变更(前后镜像约 200MB+)。从库 IO Thread 接收 200MB 数据需要时间,SQL Thread 一条一条重放更慢,耗时可能是几十秒甚至几分钟。

排查手段:看 SHOW BINLOG EVENTS 定位大事务时间戳,或者监控 SHOW SLAVE STATUSSeconds_Behind_Master 突然跳增。

2.2 单线程重放瓶颈

MySQL 5.6 之前,从库只有一个 SQL Thread。主库 8 个线程并发写,从库 1 个线程串行放,不延迟才怪。

5.6 引入的 slave_parallel_workers 参数支持多个 SQL 线程,但 5.6 的并行粒度是按库分DATABASE),如果你的业务都在一个库里,并行等于没开。

5.7 引入 LOGICAL_CLOCK 模式,基于主库的组提交信息判断哪些事务可以并行。8.0 进一步引入 writeset 并行复制,只要事务修改的行集(writeset)没有交集,就允许并行回放——即使这些事务不在同一个组提交批次里。这意味着 UPDATE t1UPDATE t2 可以并行,即使它们提交时间相隔几秒。

2.3 从库硬件不足

从库的磁盘如果是 HDD,随机写性能比 SSD 差一个数量级;CPU 核数不够,解析和重放 binlog 事件也慢。很多公司把从库当「低配备用机」,结果延迟成了常态。

真实案例:某电商公司从库用 4C8G 云服务器 + 普通云盘,主库是 32C64G 物理机 + NVMe SSD。主库 TPS 8000,从库延迟稳定在 15-30 秒。升级到 16C32G + ESSD 后延迟降到 0.5s 以内。

2.4 锁竞争

从库上如果还有别的查询(比如报表查询、备份读取),这些查询会和 SQL Thread 争抢表锁或行锁,导致重放被阻塞。

典型场景:晚上 10 点跑日报聚合查询,大表全表扫描。SQL Thread 要更新同一张表,被 MDL(元数据锁)或行锁堵住,从库延迟瞬间飙升到几百秒。

2.5 网络延迟

IO Thread 接收 binlog 慢,relay log 积压,SQL Thread 无数据可放。这种情况在跨机房部署时尤其明显。北京机房到上海机房 RTT 约 30ms,每次 binlog 事件传输都多 30ms,累积起来就很可观。

三、延迟排查三板斧

3.1 SHOW SLAVE STATUS\G

这是第一站,重点关注三个字段:

sql
SHOW SLAVE STATUS\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.1.100
                  Master_Log_File: mysql-bin.000023
              Read_Master_Log_Pos: 85763400
                   Relay_Log_File: relay-bin.000089
                    Relay_Log_Pos: 12345
            Relay_Master_Log_File: mysql-bin.000023
                 Slave_IO_Running: Yes
                Slave_SQL_Running: Yes
          Seconds_Behind_Master: 45    -- ← 延迟秒数,近似值
              Exec_Master_Log_Pos: 82341000
              Relay_Log_Space: 3422400  -- ← relay log 积压大小(字节)

Seconds_Behind_Master 是近似值,它的计算方式:当前时间 − SQL Thread 执行到的 binlog 事件的时间戳。大事务场景下这个值可能不准——比如主库的大事务刚提交,从库还没开始放,延迟可能显示为 0。

关键判断Relay_Log_Space 持续增长说明 IO Thread 接收快但 SQL Thread 消费慢,问题在重放端;Slave_IO_State 显示 Waiting for master to send eventSeconds_Behind_Master 大,说明网络传输慢或者主库 Dump 线程来不及生成事件。

3.2 看 SHOW PROCESSLIST

SQL Thread 是否在等待什么?

sql
SHOW PROCESSLIST;

重点关注:

  • Waiting for table flush:有 DDL 或 FLUSH TABLES 在排队,SQL Thread 被卡住。从库跑 FLUSH TABLES;ALTER TABLE 时最常见。
  • Waiting for lock:SQL Thread 和从库上的其他查询在争锁。
  • System lock:可能是元数据锁。

3.3 用 pt-heartbeat 精确测量

Seconds_Behind_Master 是秒级精度且受时钟偏差影响。Percona Toolkit 的 pt-heartbeat 通过在主库定期写入时间戳、从库计算差值,能达到毫秒级精度。

bash
# 在主库创建心跳表
pt-heartbeat --create-table -D percona -u root -p

# 主库写入心跳(1 秒一次)
pt-heartbeat --update -D percona --daemonize

# 从库查询延迟
pt-heartbeat --check -D percona -S /tmp/mysql.sock

输出示例:0.003 表示 3ms 延迟,45.200 表示 45.2 秒。这个值比 Seconds_Behind_Master 可靠得多。

四、修复方案

4.1 并行复制(Multi-Threaded Slave)

MySQL 8.0 的并行复制已经成熟,优先开启:

ini
# my.cnf 从库配置
slave_parallel_workers = 8
slave_parallel_type = LOGICAL_CLOCK

关键点是 LOGICAL_CLOCK 模式——它基于主库的组提交(Group Commit)信息,同一组提交的事务在从库上可以安全地并行回放。主库的 binlog_group_commit_sync_delay 可以控制组提交的等待时间,适当调大可以让更多事务被分到同一组,提高并行度。

ini
# 主库配置,提升并行复制的效果
binlog_group_commit_sync_delay = 1000  -- 微秒,等待 1ms 聚合更多事务
binlog_group_commit_sync_no_delay_count = 100

注意binlog_group_commit_sync_delay 调太大(>5000)会显著增加主库响应延迟,因为事务提交被强制等待。通常 1000 微秒(1ms)是安全值。

4.2 拆分大事务

大事务不仅产生延迟,还会导致 binlog 文件膨胀、主从切换变慢。业务上应该限制单次操作的数据量:

sql
-- 分批删除,每批 1000 行,带 SLEEP 给从库喘息时间
DELIMITER $$
CREATE PROCEDURE batch_delete_old_orders()
BEGIN
  DECLARE done INT DEFAULT 0;
  REPEAT
    DELETE FROM orders
    WHERE create_time < '2024-01-01'
    LIMIT 1000;
    SET done = ROW_COUNT();
    SELECT SLEEP(0.1);  -- 给从库 SQL Thread 喘口气
  UNTIL done = 0 END REPEAT;
END$$
DELIMITER ;

业务代码层面,也建议对批量操作做分页:Java 里用 LIMIT 1000 OFFSET ? 循环,每次提交独立事务。

4.3 半同步复制(Semi-Sync Replication)

异步复制下,主库提交事务后不管从库是否收到,直接返回客户端。如果主库在从库收到 binlog 前宕机,切换后数据丢失——这就是异步复制的 RPO > 0。

半同步复制至少有一个从库确认收到 binlog 后,主库才返回客户端提交成功。思路:

client → 主库提交事务 → 写 binlog → 等待至少一个从库 ACK → 返回客户端
ini
# 主库安装插件 & 开启
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 10000;  -- 10 秒超时,超时后降级为异步

# 从库
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
STOP SLAVE IO_THREAD; START SLAVE IO_THREAD;

代价:每次事务提交多一次网络往返(主库 → 从库 → 主库 ACK),延迟增加约 1-5ms。rpl_semi_sync_master_timeout 是保命机制——如果从库挂了,等待超时后自动降级为异步,避免主库不可写。

4.4 架构层面解耦

如果延迟无法根除,应用层必须感知:

  • ProxySQL:支持延迟感知路由,mysql_replication_hostgroups 可以配置当从库延迟超过阈值时自动将流量切回主库。
sql
-- ProxySQL 配置示例
UPDATE mysql_replication_hostgroups
SET writer_hostgroup=0, reader_hostgroup=1
WHERE comment='production';

-- 延迟超过 10 秒的从库不分配读流量
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight, max_latency_ms)
VALUES (1, 'slave1', 3306, 1, 10000);
  • 缓存兜底:读请求先查 Redis,Redis miss 再从从库读,缓存只有几秒 TTL,能扛住延迟窗口。

4.5 relay log 损坏的兜底

relay log 损坏会导致从库复制中断。常见原因:磁盘满、MySQL 异常 crash、文件系统错误。

sql
-- 暂停并从当前位置重建 relay log
STOP SLAVE;
RESET SLAVE;
START SLAVE;

注意 RESET SLAVE 会清除 relay log 和 master.info 信息,但不会丢失数据——IO Thread 会从之前记录的 Master_Log_File + Read_Master_Log_Pos 重新拉取 binlog。前提是主库的 binlog 还没被 purge 掉。

五、总结

环节常见问题解决手段
大事务单条 SQL 影响百万行分批操作、设置 max_execution_time
单线程重放跟不上主库写入开启并行复制 LOGICAL_CLOCK(8.0 建议 writeset)
从库硬件磁盘 IOPS 不足升 SSD、增加从库数量做负载分担
锁竞争SQL Thread 被阻塞从库上报表查询错峰、用 SELECT ... FROM SECONDARY 或只读实例
网络延迟跨机房复制半同步复制(rpl_semi_sync_master)、压缩传输
binlog 格式ROW 格式体积爆炸保证带宽充足、监控 Binlog_cache_disk_use
异步复制 RPO>0主库宕机丢数据半同步复制 + 配合 sync_binlog=1innodb_flush_log_at_trx_commit=1

主从延迟不可怕,可怕的是没有监控手段应急方案。建议每套 MySQL 集群都配置 pt-heartbeat + 延迟告警(阈值 10 秒),同时业务层做好降级方案,别让从库延迟直接拖垮线上服务。

面试追问:给你一个 Seconds_Behind_Master=0 但从库数据明显不一致的场景,你怎么排查?——答案可以从 binlog 格式(STATEMENT 非确定性函数)、sync_binlog 不为 1 导致主库 crash 后 binlog 丢失、log_slave_updates 未开启导致级联复制断链这几个方向答。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。