MySQL 8.0.35 主从延迟模拟与解决

基于二进制包最小化安装,GTID 主从复制架构 + 主从延迟常见原因模拟与解决

  • 主库:192.168.195.141
  • 从库:192.168.195.142

1. 环境信息

项目 主库 (Master) 从库 (Slave)
IP 192.168.195.141 192.168.195.142
MySQL版本 8.0.35(二进制包,glibc2.17) 8.0.35(二进制包,glibc2.17)
server-id 1 2
gtid_mode ON ON
enforce_gtid_consistency ON ON
binlog_format ROW ROW
角色 Master Slave

2. 清空环境(两台均执行)

bash 复制代码
systemctl stop mysqld
rm -rf /data/mysql/3306/data/*
rm -f /etc/my.cnf /etc/systemd/system/mysqld.service /etc/sysconfig/mysql
systemctl daemon-reload

3. 编辑配置文件

3.1 主库 /etc/my.cnf(141)

ini 复制代码
[client]
socket = /data/mysql/3306/data/mysql.sock
[mysqld]
basedir = /usr/local/mysql
datadir = /data/mysql/3306/data
user    = mysql
port    = 3306
socket  = /data/mysql/3306/data/mysql.sock
log_error = /data/mysql/3306/data/mysqld.err
log_timestamps = system
log-bin = mysql-bin
server-id = 1
gtid_mode = ON
enforce_gtid_consistency = ON

3.2 从库 /etc/my.cnf(142)

ini 复制代码
[client]
socket = /data/mysql/3306/data/mysql.sock
[mysqld]
basedir = /usr/local/mysql
datadir = /data/mysql/3306/data
user    = mysql
port    = 3306
socket  = /data/mysql/3306/data/mysql.sock
log_error = /data/mysql/3306/data/mysqld.err
log_timestamps = system
server-id = 2
gtid_mode = ON
enforce_gtid_consistency = ON

4. 初始化实例(两台均执行)

bash 复制代码
chown mysql.mysql /data/mysql/3306/data/
/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --initialize

获取临时密码:

bash 复制代码
grep "temporary password" /data/mysql/3306/data/mysqld.err

5. 配置 systemd 服务(两台均执行)

5.1 创建 /etc/systemd/system/mysqld.service

ini 复制代码
[Unit]
Description=MySQL Server
Documentation=man:mysqld(8)
Documentation=http://dev.mysql.com/doc/refman/en/using-systemd.html
After=network.target
After=syslog.target

[Install]
WantedBy=multi-user.target

[Service]
User=mysql
Group=mysql
Type=forking
PIDFile=/data/mysql/3306/data/mysqld.pid
TimeoutSec=0
ExecStart=/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --pid-file=/data/mysql/3306/data/mysqld.pid --daemonize $MYSQLD_OPTS
EnvironmentFile=-/etc/sysconfig/mysql
LimitNOFILE = 65535
Restart=on-failure
RestartPreventExitStatus=1
PrivateTmp=false

5.2 创建 /etc/sysconfig/mysql

ini 复制代码
MYSQLD_OPTS=

6. 启动实例并修改 root 密码(两台均执行)

bash 复制代码
systemctl daemon-reload
systemctl start mysqld
systemctl enable mysqld

# 使用 --init-file 方式修改密码
cat > /tmp/mysql-init << "EOF"
alter user root@localhost identified by "Root@123456";
EOF

systemctl stop mysqld
/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --init-file=/tmp/mysql-init --user=mysql --daemonize
sleep 3
# 验证
/usr/local/mysql/bin/mysql -uroot -S /data/mysql/3306/data/mysql.sock -p'Root@123456' -e "select 1;"

# 恢复 systemd 管理
pkill -9 mysqld; sleep 2; systemctl start mysqld

7. 搭建 GTID 主从复制

7.1 主库创建复制用户

sql 复制代码
-- 在主库(141)执行
CREATE USER 'repl'@'%' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

7.2 从库建立 GTID 复制

sql 复制代码
-- 在从库(142)执行
CHANGE MASTER TO
MASTER_HOST='192.168.195.141',
MASTER_USER='repl',
MASTER_PASSWORD='123456',
MASTER_AUTO_POSITION=1,
GET_MASTER_PUBLIC_KEY=1;

START SLAVE;

7.3 验证主从状态

sql 复制代码
-- 在从库(142)执行
show slave status\G
复制代码
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
        Seconds_Behind_Master: 0
                Auto_Position: 1

8. Seconds_Behind_Master 的实现逻辑

8.1 计算公式

复制代码
Seconds_Behind_Master = 从库当前系统时间 - SQL线程当前重放事务在主库的开始时间戳 - 主从系统时间差

即:

c 复制代码
long time_diff = ((long)(time(0) - mi->rli->last_master_timestamp) - mi->clock_diff_with_master);

其中:

  • time(0):从库当前的系统时间
  • last_master_timestamp:SQL 线程当前重放的事务在主库开始执行的时间戳(binlog 事件中的时间戳)
  • clock_diff_with_master:IO 线程启动时,主从之间的系统时间差

8.2 特殊值含义

含义
0 SQL 线程已重放完所有 relay log,且 IO 线程正在运行
NULL SQL 线程未运行,或 SQL 线程重放完 relay log 但 IO 线程未运行
>0 SQL 线程存在延迟,数值为延迟秒数

注意: 如果 IO 线程启动后调整过系统时间,需重启复制,否则会影响 Seconds_Behind_Master 的计算结果。


9. 如何分析主从延迟

9.1 从库服务器负载情况

CPU 分析(top):

复制代码
Cpu(s): 0.2%us, 0.2%sy, 0.0%ni, 99.5%id, 0.0%wa, 0.0%hi, 0.2%si, 0.0%st
指标 含义
us 处理用户态任务的 CPU 时间占比
sy 处理内核态任务的 CPU 时间占比
id 处于空闲状态的 CPU 时间占比
wa 等待 IO 的 CPU 时间占比

当 CPU 使用率(1 - id)超过 90% 时需引起关注。

磁盘 IO 分析(iostat -xm 1):

重点关注:

  • await:IO 请求的平均耗时(ms),包括磁盘处理时间和队列等待时间
  • %util:磁盘饱和度,采样周期内有多少时间在做 IO 操作

9.2 主从复制状态对比

第一对:判断 IO 线程延迟

复制代码
主库 (File, Position)  vs  从库 (Master_Log_File, Read_Master_Log_Pos)

如果主库位置 > 从库 IO 线程位置,则 IO 线程存在延迟。

第二对:判断 SQL 线程延迟

复制代码
从库 (Master_Log_File, Read_Master_Log_Pos)  vs  (Relay_Master_Log_File, Exec_Master_Log_Pos)

如果 IO 线程位置 > SQL 线程位置,则 SQL 线程存在延迟。

9.3 主库 binlog 写入量

查看主库 binlog 的生成速度,如多少分钟生成一个 binlog 文件。


10. 主从延迟的常见原因及解决方法

10.1 IO 线程存在延迟

IO 线程延迟较少见,常见原因:

原因 解决方法
网络延迟/带宽限制 开启 slave_compressed_protocol 启用 binlog 压缩传输
从库磁盘 IO 瓶颈 调整双一设置或关闭 binlog
网卡故障 排查网络硬件问题

10.2 SQL 线程存在延迟

场景一:主库写入量过大,SQL 线程单线程重放

具体体现:

  1. 从库磁盘 IO 无明显瓶颈
  2. Relay_Master_Log_File, Exec_Master_Log_Pos 不断变化
  3. 主库写入量过大(如 SATA SSD 下 binlog 生成速度快于 5 分钟一个)

解决方法:开启并行复制

sql 复制代码
-- 在从库(142)执行
STOP SLAVE;
SET GLOBAL slave_parallel_workers = 4;       -- 设置并行工作线程数
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';  -- 基于LOGICAL_CLOCK的并行复制
START SLAVE;

-- 验证
SHOW VARIABLES LIKE 'slave_parallel_workers';
-- Value: 4

SHOW VARIABLES LIKE 'slave_parallel_type';
-- Value: LOGICAL_CLOCK

参数解释:

  • slave_parallel_workers:并行工作线程数,0 表示单线程,建议设置为 4-8。
  • slave_parallel_type:并行复制类型。LOGICAL_CLOCK 基于组提交的并行复制,同一组提交的事务可以在从库并行重放。

场景二:STATEMENT 格式下的慢 SQL

具体体现: 在一段时间内 Relay_Master_Log_File, Exec_Master_Log_Pos 没有变化。

原理: STATEMENT 格式下,慢 SQL 在主库执行慢,在从库重放同样慢。例如对千万数据的无索引表执行 DELETE,主库耗时 7.52s,从库重放时 Seconds_Behind_Master 最大可达 7s。

解决方法:优化 SQL

sql 复制代码
-- 开启慢查询记录(将 SQL 重放过程中执行时长超过 long_query_time 的操作记录在慢日志)
SET GLOBAL log_slow_slave_statements = ON;

log_slow_slave_statements 在 MySQL 5.6.11 中引入,可将 SQL 重放过程中执行时长超过 long_query_time 的操作记录在慢日志中,便于定位从库慢 SQL。


场景三:表上没有任何索引,且 binlog 格式为 ROW

具体体现: 在一段时间内 Relay_Master_Log_File, Exec_Master_Log_Pos 不会变化。

原理: ROW 格式下,对无索引表操作,主库只需一次全表扫描,但从库重放时对每条记录的操作都会进行一次全表扫描。同样的 DELETE 操作,ROW 格式下延迟可能是 STATEMENT 格式的 100 倍。

模拟验证:

sql 复制代码
-- 主库创建无索引表并插入数据
USE test_delay;
CREATE TABLE t_no_idx (id INT, c VARCHAR(200));
-- 插入 100000 行数据...

-- 主库执行 DELETE(ROW 格式)
DELETE FROM t_no_idx WHERE id <= 100;
-- 主库执行很快,但从库重放时每行都需全表扫描,产生延迟

解决方法一:在从库上临时创建索引

sql 复制代码
-- 在从库(142)执行
USE test_delay;
ALTER TABLE t_no_idx ADD INDEX idx_id (id);

注意:尽量选择区分度高的列添加索引,列的区分度越高,重放速度越快。

解决方法二:设置 slave_rows_search_algorithms

sql 复制代码
-- 在从库(142)执行
STOP SLAVE;
SET GLOBAL slave_rows_search_algorithms = 'INDEX_SCAN,HASH_SCAN';
START SLAVE;

参数解释:

  • INDEX_SCAN,HASH_SCAN:当无索引时使用 HASH_SCAN 替代 TABLE_SCAN,对每行记录不再做全表扫描,而是使用哈希查找。设置后延迟可大幅降低(PDF 示例从 723s 降至 53s)。
  • 默认值即为 INDEX_SCAN,HASH_SCAN

场景四:大事务

原理: ROW 格式下,操作涉及的记录数较多时,从库重放耗时随记录数增加而增加。

记录数 主库执行时长(s) Seconds_Behind_Master 最大值(s)
50000 0.76 1
200000 3.10 8
500000 17.32 39
1000000 63.47 122

解决方法:分而治之,每次小批量执行

sql 复制代码
-- 不推荐:一次性大事务
UPDATE t_big_tx SET c=REPEAT(M,120) WHERE id<=1000000;

-- 推荐:分批执行
UPDATE t_big_tx SET c=REPEAT(M,120) WHERE id<=10000;
UPDATE t_big_tx SET c=REPEAT(M,120) WHERE id BETWEEN 10001 AND 20000;
-- ... 依此类推

场景五:从库上有查询操作导致锁等待

原理: 从库的查询操作可能阻塞 SQL 线程重放,常见的是查询操作阻塞 DDL 操作(Metadata Lock 等待)。

模拟验证:

sql 复制代码
-- 步骤1:在从库(142)执行长查询
USE test_delay;
SELECT id, SLEEP(10) FROM t_mdlock;
-- 该查询对 t_mdlock 表持有 Metadata Read Lock

-- 步骤2:在主库(141)执行 DDL
USE test_delay;
ALTER TABLE t_mdlock ADD COLUMN c2 INT;
-- DDL 需要 Metadata Write Lock,与从库查询冲突

-- 步骤3:查看从库状态
SHOW PROCESSLIST;
复制代码
+----+-------------+-----------------+------+---------+------+--------------------------------+----------------------------------------+
| Id | User        | Host            | db   | Command | Time | State                          | Info                                   |
+----+-------------+-----------------+------+---------+------+--------------------------------+----------------------------------------+
| 72 | system user | connecting host | NULL | Connect | 287  | Waiting for source to send event | NULL                                |
| 73 | system user |                 | test_delay | Query | 24 | Waiting for table metadata lock | ALTER TABLE t_mdlock ADD COLUMN c2 INT |
| 78 | root        | localhost       | NULL | Query   | 0    | init                           | SHOW PROCESSLIST                       |
+----+-------------+-----------------+------+---------+------+--------------------------------+----------------------------------------+
sql 复制代码
SHOW SLAVE STATUS\G
复制代码
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
        Seconds_Behind_Master: 21
      Slave_SQL_Running_State: Waiting for table metadata lock

关键发现:

  • SQL 线程(Id=73)状态为 Waiting for table metadata lock
  • Seconds_Behind_Master: 21,延迟已产生
  • IO 线程正常运行(不受影响)

解决方法:

  1. 等待查询结束,Metadata Lock 自动释放,SQL 线程恢复重放
  2. KILL 阻塞查询:KILL <查询线程Id>;
  3. 业务层面避免在从库执行长查询,或将长查询路由到专用只读节点

验证恢复:

sql 复制代码
-- 长查询结束后
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 0
-- Slave_SQL_Running_State: Replica has read all relay log; waiting for more updates

场景六:从库上存在备份(FLUSH TABLES WITH READ LOCK 阻塞 SQL 线程)

原理: 备份操作执行 FLUSH TABLES WITH READ LOCK(FTWRL)获取全局读锁,会阻塞 SQL 线程的重放。

模拟验证:

sql 复制代码
-- 步骤1:在从库(142)模拟备份操作
FLUSH TABLES WITH READ LOCK;
SELECT SLEEP(60);  -- 模拟备份持续 60 秒
UNLOCK TABLES;

-- 步骤2:在主库(141)写入数据
USE test_delay;
INSERT INTO t_ddl VALUES (5,'ftwrl_test2',NULL);

-- 步骤3:查看从库状态
SHOW PROCESSLIST;
复制代码
+----+-------------+-----------------+------+---------+------+--------------------------------+----------------------------------------+
| Id | User        | Host            | db   | Command | Time | State                          | Info                                   |
+----+-------------+-----------------+------+---------+------+--------------------------------+----------------------------------------+
| 21 | system user |                 | NULL | Query   | 19   | Waiting for global read lock   | NULL                                   |
| 46 | root        | localhost       | NULL | Query   | 24   | User sleep                     | SELECT SLEEP(60)                       |
+----+-------------+-----------------+------+---------+------+--------------------------------+----------------------------------------+
sql 复制代码
SHOW SLAVE STATUS\G
复制代码
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
        Seconds_Behind_Master: 19
      Slave_SQL_Running_State: Waiting for global read lock

关键发现:

  • SQL 线程状态为 Waiting for global read lock
  • Seconds_Behind_Master: 19,延迟已产生
  • FTWRL 的全局读锁阻塞了 SQL 线程

解决方法:

  1. 使用 mysqldump --single-transaction 替代 FTWRL 进行 InnoDB 备份(不会阻塞重放)
  2. 调整备份时间窗口,避开业务高峰
  3. 使用 XtraBackup 等物理备份工具

验证恢复:

sql 复制代码
-- FTWRL 释放后(UNLOCK TABLES 或会话断开)
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 0

场景七:磁盘 IO 存在瓶颈

原理: 从库磁盘 IO 性能不足,导致 SQL 线程重放速度跟不上主库写入速度。

解决方法:调整双一设置或关闭 binlog

sql 复制代码
-- 在从库(142)执行

-- 查看当前设置
SHOW VARIABLES LIKE 'sync_binlog';
-- Value: 1(每次事务提交都同步 binlog 到磁盘,最安全但最慢)

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
-- Value: 1(每次事务提交都刷新 redo log 到磁盘,最安全但最慢)

-- 调整为宽松设置(提升 IO 性能,降低安全性)
SET GLOBAL sync_binlog = 0;                        -- 不主动同步 binlog,由操作系统负责
SET GLOBAL innodb_flush_log_at_trx_commit = 2;     -- 每次事务提交写入 os cache,每秒 fsync

-- 验证
SHOW VARIABLES LIKE 'sync_binlog';
-- Value: 0

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
-- Value: 2

参数解释:

  • sync_binlog = 1:每次事务提交都 fsync binlog,最安全但最慢(DUAL 1,"双一"设置之一)。
  • sync_binlog = 0:不主动 fsync,由操作系统缓存刷新,性能最好但崩溃可能丢失事务。
  • innodb_flush_log_at_trx_commit = 1:每次事务提交都 fsync redo log,最安全(DUAL 1,"双一"设置之二)。
  • innodb_flush_log_at_trx_commit = 2:每次事务提交写入 os cache,每秒 fsync,最多丢失 1 秒数据。

注意: 从库调整双一设置是可接受的,因为从库数据可从主库重新同步。但主库不建议调整。

也可考虑关闭从库 binlog(MySQL 5.7+ GTID 模式下允许):

sql 复制代码
-- 需要修改 my.cnf 并重启
[mysqld]
skip-log-bin
# 或
disable-log-bin

11. 主从延迟原因总结图

复制代码
主从延迟
├── IO 线程延迟(较少见)
│   ├── 网络延迟/带宽限制 → 开启 slave_compressed_protocol
│   ├── 从库磁盘 IO 瓶颈 → 调整双一设置或关闭 binlog
│   └── 网卡故障 → 排查网络硬件
│
└── SQL 线程延迟(常见)
    ├── 主库写入量过大,单线程重放 → 开启并行复制
    ├── STATEMENT 格式慢 SQL → 优化 SQL + log_slow_slave_statements
    ├── 无索引表 + ROW 格式 → 从库加索引 / slave_rows_search_algorithms=INDEX_SCAN,HASH_SCAN
    ├── 大事务 → 分而治之,小批量执行
    ├── 从库查询阻塞 DDL(Metadata Lock)→ 避免从库长查询 / KILL 阻塞查询
    ├── 从库备份(FTWRL)→ 使用 --single-transaction 或 XtraBackup
    └── 磁盘 IO 瓶颈 → 调整双一设置或关闭 binlog

12. 延迟场景验证汇总

序号 延迟场景 模拟方式 从库状态 Seconds_Behind_Master 解决方法 验证结果
1 主库写入量大,单线程重放 slave_parallel_workers=0 + 大量INSERT SQL线程持续重放 视写入量而定 开启并行复制(slave_parallel_workers=4, slave_parallel_type=LOGICAL_CLOCK) ✅ 并行复制已开启
2 STATEMENT格式慢SQL SET binlog_format=STATEMENT + SLEEP SQL线程执行慢SQL 约等于慢SQL执行时间 优化SQL + log_slow_slave_statements ✅ 原理验证通过
3 无索引表+ROW格式 无索引表 + DELETE(ROW格式) SQL线程全表扫描 视数据量而定 从库加索引 / slave_rows_search_algorithms=INDEX_SCAN,HASH_SCAN ✅ 索引已添加,HASH_SCAN已设置
4 大事务 大量UPDATE单事务 SQL线程重放大事务 随记录数增加 分而治之,小批量执行 ✅ 原理验证通过
5 从库查询阻塞DDL SELECT SLEEP + ALTER TABLE Waiting for table metadata lock 21s 等待/KILL查询/避免从库长查询 ✅ 延迟21s后恢复0
6 从库备份FTWRL FLUSH TABLES WITH READ LOCK Waiting for global read lock 19s --single-transaction / XtraBackup ✅ 延迟19s后恢复0
7 磁盘IO瓶颈 双一设置导致IO瓶颈 IO等待 视IO压力 sync_binlog=0 + innodb_flush_log_at_trx_commit=2 ✅ 参数已调整

13. 当前环境参数确认

13.1 主库(141)

复制代码
gtid_mode                = ON
enforce_gtid_consistency = ON
binlog_format            = ROW
log-bin                  = mysql-bin
server-id                = 1
gtid_executed            = b2a07ac2-8842-11f1-9d92-000c29048d1f:1-35061

13.2 从库(142)

复制代码
gtid_mode                         = ON
enforce_gtid_consistency          = ON
binlog_format                     = ROW
server-id                         = 2
slave_parallel_workers            = 4
slave_parallel_type               = LOGICAL_CLOCK
sync_binlog                       = 0
innodb_flush_log_at_trx_commit    = 2
slave_rows_search_algorithms      = INDEX_SCAN,HASH_SCAN

14. 连接信息

bash 复制代码
# 主库(141)
/usr/local/mysql/bin/mysql -uroot -S /data/mysql/3306/data/mysql.sock -p'Root@123456'

# 从库(142)
/usr/local/mysql/bin/mysql -uroot -S /data/mysql/3306/data/mysql.sock -p'Root@123456'
相关推荐
不知疲倦的仄仄1 小时前
MySQL/Read View快照/MVCC/串行化
java·数据库·mysql
Mico183 小时前
MySQL 5.7.35 升级到 8.0.35 — 原地升级(主从架构方式)
mysql
万亿少女的梦1684 小时前
基于Spring Boot的乐助在线助农系统设计与实现
java·spring boot·mysql·敏感词过滤·助农系统
艾莉丝努力练剑4 小时前
【MYSQL】MYSQL学习的一大重点:基本查询(下)
android·数据库·学习·mysql·面试·八股文
冰暮流星4 小时前
mysql之数据库创建,查询,删除,启用
数据库·mysql·oracle
提笔了无痕19 小时前
MySQL SQL 从 EXPLAIN 到索引优化,搞懂 SQL 为什么慢
android·sql·mysql
辰同学ovo20 小时前
从一条 SQL 到三个原则:MySQL 心智模型
sql·mysql·adb
Mico181 天前
MySQL 8.0.35 GTID主从复制常见管理操作
mysql
网安墨雨1 天前
MySQL数据库 SQL语句详解
自动化测试·软件测试·数据库·python·sql·mysql