半夜两点接到电话,说线上一个千万级的 MySQL 实例要加只读从库。数据量接近 900G,业务还在持续写入,窗口只有四个小时。第一反应是:完了,是不是又要锁表。
很多人在这种场景下会下意识走老路:在主库上FLUSH TABLES WITH READ LOCK,拿到一个一致性快照,然后把数据文件拷到新机器,再记录 binlog 位点,启动复制。这套流程在十年前的单机小库时代没问题,但在今天,只要执行全局读锁,写入队列就会立刻开始积压。慢查询从偶尔变成常态,业务侧能明显感知,稍大一点的库,一个锁拿下去可能就是几分钟甚至几十分钟的写入中断。
真正的问题不是“要不要锁表”,而是怎么在不长时间阻断主库写入的前提下,得到一个一致性快照,并让这个快照和后续的 binlog 事件正确衔接。MySQL 做主从复制,从库追的是日志,不是文件本身。只要能解决快照和复制位点的衔接问题,从库完全可以做到无感加入。
这篇文章就围绕这个场景展开:MySQL 数据持续写入时,怎么在线加从库,才能既不停业务,也不锁主库。我会把思路、工具选型、关键参数、实操步骤和踩坑点一次写清楚。
1. 为什么“加个全局锁再拷数据”越来越不现实
1.1 锁表不是备份工具的问题,是复制位点的衔接问题
先厘清一个概念。很多人以为备份工具是罪魁祸首,觉得换个“不锁表”的工具就能解决。其实不是。mysqldump 加--single-transaction在某些场景下确实可以避免长时间锁表,但 MySQL 复制真正难的不是“拿到一份数据”,而是“拿到一份能和主库 binlog 对得上、且落盘时不产生冲突的数据”。
如果只用mysqldump --single-transaction备份一个 InnoDB 大库,然后在从库上source恢复,整个过程虽然不锁主库,但有两个隐患:
- 恢复阶段是单线程导入。900G 的逻辑备份导入可能要好几个小时,窗口根本不够。
- 备份出来的 SQL 里自带事务和表结构,如果原表数据量太大,恢复时的 binlog 放大效应非常明显,从库同步起点很容易被拉后。
所以在生产环境,重点不是“用什么工具”,而是“怎么设计一套不需要靠锁表来保证一致性的迁移链路”。
1.2 单机小库时代可行,不等于生产环境可行
以前加从库,常见做法是锁表、打包数据目录、拷贝到新服务器、改配置、启动复制。因为当时数据量小,几十 G 已经算大库,锁几分钟业务也能忍。但现在的生产库动辄几百 G,甚至上 T,业务压力大,写入几乎不会停。锁表时间一旦超过阈值,连接数会堆积,主从延迟会拉大,更麻烦的是,很多监控系统会误报警。
更重要的是,数据量大之后,物理文件拷贝的耗时不一定比逻辑备份短。拷完之后还要解决版本差异、配置文件路径、ibdata 文件大小、目录权限等问题,每一步都可能让“锁表时间”继续往后延。
所以现在加从库的主流思路是:使用并行逻辑备份工具,在可接受的资源开销下拿到一致性快照,然后通过 GTID 自动衔接后续 binlog。这样主库全程只承受“读负载”和“binlog 写入”,不会被锁阻塞。
注意:下面提到的命令和参数,是基于我实践中比较常用的版本和配置写的。具体落地前,请先确认你手上的 MySQL 版本、Mydumper 版本、操作系统和磁盘类型,不要照搬所有参数。
2. 不锁表加从库,真正靠的是 GTID 和一致性快照
2.1 GTID 让主从复制从“对坐标”变成“对集合”
在 GTID 出现之前,MySQL 主从复制靠的是MASTER_LOG_FILE和MASTER_LOG_POS。只要备份快照和 binlog 位点之间有一丁点偏差,从库就可能丢数据或重复执行事务。
GTID 改变了这个问题的处理方式。每个事务都有一个全局唯一标识,主库执行完一个事务,这个事务的 GTID 会记录到gtid_executed集合里。从库连接主库时,不再需要精确告诉它“从哪个文件的哪个位置开始”,只需要说“我这边的 GTID 集合是什么,请你把差集发给我”。
这意味着,只要备份过程中我们能拿到主库当时的gtid_executed集合,并把数据导入从库时也把那个集合写进去,后续复制就是自动补齐,不需要人肉对齐坐标。
这也是为什么现在很多生产实践都建议:还没开启 GTID 的实例,如果准备做在线扩展,先把 GTID 打开,再考虑加从库。GTID 开启本身对现有复制的影响,取决于在线开启方式和版本,但比起每次迁移都手工对点位,GTID 带来的收益明显更高。
2.2 Mydumper 并行备份:把迁移从“顺序读”变成“并行读”
工具选型上,我更建议用 Mydumper,而不是 mysqldump。原因有两点:
- Mydumper 支持多线程并行导出,默认会按照表拆分成多个任务,小表一张一个线程,大表可以按行范围拆分。
- Mydumper 的
--kill-long-queries思路不是锁表,而是通过检测并跳过长时间运行的查询,让备份在业务压力可控的前提下进行。
当然,它本身也是一个逻辑备份工具,也需要通过--single-transaction或类似机制拿到 InnoDB 快照。但因为它会并发读取多个表,整体备份时间可以大幅缩短。此外,Mydumper 导出的文件是“每个表一个 SQL 文件 + 元数据文件”的结构,恢复时也可以用多线程并行导入,这一步对缩短从库构建时间非常关键。
如果你用的是 MySQL 8.0,还可以考虑官方 MySQL Shell 的 util.dumpInstance 工具,它对 InnoDB 和 GTID 的支持也做得比较完善。但 Mydumper 在兼容性和灵活性上有更长的实战历史,团队里无论谁接手都更容易理解和维护。
2.3 为什么选择事务导入而不是直接 source
备份完只是第一步,从库恢复时最忌讳的是单线程执行。900G 的逻辑备份如果直接用source,可能跑一晚上都不一定能结束。
恢复阶段的基本思路是分两条路:
- 先把表结构、存储过程、函数、触发器等对象建好。
- 再把大表的 SQL 用多条并行流导入。
Mydumper 导出的结构文件和数据文件是分离的,正好可以这样操作。恢复时优先恢复所有建表语句,再启动多个并行任务导数据。整个过程要确保目标从库的 binlog 关闭或至少不做无谓记录,否则导入操作本身会产生大量 binlog,浪费时间,也影响从库后续追主库日志的性能。
不过要注意,关闭 binlog 只是恢复阶段的做法。恢复完成后,正式启动复制前,需要开启 binlog,否则从库自身后续也可能承担新的从库角色,或者是发生了主从切换后它要变成主库,那时候没有 binlog 就会很被动。
3. 生产实操:一套最小但不缺步的演练流程
3.1 操作前必须确认的五件事
不要上来就执行备份命令。生产环境操作前,先把下面五项确认清楚,每项都可能决定你后面是否白忙活。
- 磁盘空间。备份目录要有足够空间。按经验,逻辑备份体积大约是原库实际数据量的 50% 到 80%,不同引擎和数据类型差异比较大,宁可多留一倍余量。恢复端同样要检查,因为逻辑导入过程中会存在临时文件、排序文件和 binlog。
- CPU 和 IO 负载。Mydumper 并行备份对磁盘 IO 和 CPU 有一定压力。如果主库是业务高峰期,建议先降低线程数,或者用
--compress压缩备份文件,减少磁盘写入量。 - 权限。备份用户至少需要
SELECT、RELOAD、PROCESS、SHOW VIEW、EVENT权限。从库恢复用户需要CREATE、INSERT、DROP、ALTER等权限。用于复制的用户至少需要REPLICATION SLAVE或REPLICATION CLIENT权限。 - GTID 状态。先执行
SHOW VARIABLES LIKE 'gtid_mode',确认是ON。如果是OFF_PERMISSIVE或ON_PERMISSIVE,需要先完成在线切换,不要在半开状态下做迁移。 - 防火墙和安全组。新从库要能访问主库的 3306 端口。很多线上环境为了安全会限制内网 IP 访问,加从库前要先确认白名单和防火墙规则。
上面的每一项都不是形式检查。第一个决定备份会不会中途失败,第二个决定主库会不会因为备份被拖垮,第三个决定你是不是最后还要回主库再刷一遍权限,第四个决定你后面能不能直接使用CHANGE MASTER TO ... MASTER_AUTO_POSITION = 1,第五个决定复制是否能建立成功。
3.2 Mydumper 备份命令与参数理解
下面是一段常见的主库备份命令,具体线程数和压缩选项可以按需调整:
mydumper \ --user=backup_user \ --password='YourPassword' \ --host=主库IP \ --port=3306 \ --outputdir=/data/backup/mysql-backup \ --threads=8 \ --compress \ --triggers \ --events \ --routines \ --single-transaction \ --less-locking \ --kill-long-queries解释几个关键参数:
--less-locking:适合 InnoDB 表,在备份过程中尽量减少对 MyISAM 表的长时间锁等待。--kill-long-queries:如果检测到长时间运行的查询会尝试杀掉,目的是避免FLUSH TABLES WITH READ LOCK等待太久。生产环境要不要用这个参数,取决于业务是否允许强制 kill 慢查询,如果你不确认,不要默认打开。--compress:备份文件压缩后体积小,传输快,但恢复时多一步解压,也会增加恢复端 CPU 开销。
备份结束后,进入输出目录,找到metadata文件,它里面会记录备份时刻的 binlog 文件、pos 点以及 GTID 集合。这个文件是后续启动复制的核心依据。
实际操作时,我建议先拿一个小库跑一遍备份和恢复,确认工具版本、参数路径、目录权限都正常,再动主库。不要在主库上直接试错。
3.3 从库恢复并启动 GTID 复制
备份文件拷到从库后,先导入表结构,再导入数据。如果你用了--compress,得先解压。Mydumper 对 GNUparallel支持得比较好,但如果你不确定,也可以先手动解压,再用多个备份目录并行导入。
导入表结构:
mysql --user=restore_user --password='YourPassword' --host=从库IP --port=3306 < /data/backup/mysql-backup/schema.sql导入数据:
for f in /data/backup/mysql-backup/*.sql; do mysql --user=restore_user --password='YourPassword' --host=从库IP --port=3306 库名 < "$f" done只看上面这段代码,你会发现它是顺序执行的。真正做大数据量恢复时,我会把这个循环改成 xargs 或 parallel,控制并发数量,同时把所有日志重定向到文件里,方便定位哪张表导入失败。
数据导入完成后,启动 GTID 复制。这里有两种做法。
第一种,数据里已经包含了主库备份时的 GTID 集合,直接在从库上执行:
CHANGE MASTER TO MASTER_HOST='主库IP', MASTER_USER='repl_user', MASTER_PASSWORD='YourPassword', MASTER_PORT=3306, MASTER_AUTO_POSITION=1; START SLAVE;第二种,如果恢复过程因为某种原因导致事务没有完全对应,需要手动把备份出来的 GTID 集合写入:
SET GLOBAL GTID_PURGED = '主库metadata文件里的GTID集合';GTID_PURGED设置完成后,再执行上面的CHANGE MASTER TO ... MASTER_AUTO_POSITION = 1。这里最容易出问题的地方是:如果目标从库上已经执行过一些本地事务,GTID 集合不为空,直接设置GTID_PURGED会报错。所以恢复前要确保从库是个干净的实例,或者先手动清掉本地 GTID。
注意:
SET GLOBAL GTID_PURGED不是日常操作。如果从库中已经存在和主库冲突的 GTID,先考虑丢弃该实例重新搭建,而不是强行往里灌数据。强行处理可能把从库搞成数据不一致的“坏库”。
启动复制后,马上执行:
SHOW SLAVE STATUS\G需要关注几个值:Slave_IO_Running、Slave_SQL_Running是否为Yes,Seconds_Behind_Master是否在慢慢变小,Last_Errno是否为 0,Retrieved_Gtid_Set和Executed_Gtid_Set是否在增长。
4. 数据校验:不要因为没锁表就跳过这件事
4.1 前三分钟的延迟监控最要紧
刚启动复制时,从库需要从备份位点开始追日志。如果主库写入压力大,延迟可能会先冲高再回落。这时候不要慌,看的是趋势而不是瞬时值。
前 3 分钟重点监控:
Seconds_Behind_Master是否有明显回落趋势。Slave_SQL_Running是否持续为Yes。Last_SQL_Error是否出现主键冲突或表不存在。- 主库的 binlog 磁盘占用是否正常。
如果延迟持续上升,通常是主库写入量太大,而备份位点离当前时间太远。这种情况可以等一段时间观察,如果延迟超出预期,比如超过半小时还没降下来,就需要考虑备份时选择的时间点是不是太早,或者从库的 IO 能力是不是瓶颈。
4.2 用 mysqldump 或 count(*) 做抽样校验
很多人觉得有mysqlchecksum插件或者 pt-table-checksum,就一定能做全量校验。但要注意,生产环境在持续写入时,全量校验是很难做到“完全一致”的。因为主库和从库永远存在一个微小的追日志窗口,实时校验任何表都可能在时间点上不一致。
更稳妥的做法是:
- 先确认复制延迟降到接近 0。
- 把校验窗口放在业务低峰期。
- 使用 pt-table-checksum 或 mysqldump 导出一部分核心表做对比,关注行数和关键字段的校验值。
不要试图在校验工具里加太多条件,校验工具本身也会给主库和从库带来负载。第一次校验选几张核心业务表和最大的几张表就够了,剩下的可以放到下一个低峰期分批完成。
4.3 演练一次主从切换,离开前再离
搭建从库的最终目的,要么是读扩展,要么是容灾切换。很多团队在从库搭好之后只看了Slave_IO_Running是 Yes 就走了,从没做过切换演练。等到真正需要切换时,才发现从库的复制账号权限不对、只读参数没配好、VIP 指向没有改,甚至 GTID 集合有问题。
所以我建议,如果加班费允许,尽量在搭建完成后做一个简单的“切换演练”,比如手动把从库提升为可写状态,跑几条 DML 验证基本功能,再切回去,或者单独在一套测试环境里模拟。这样做之后,你会对方案更有把握,而不是只靠SHOW SLAVE STATUS的结果来安慰自己。
5. 常见问题与排查链路
5.1 报错排查顺序:IO、权限、版本、位点
如果从库复制报错,按下面顺序排查:
- 先看
Last_IO_Error。如果是连接主库失败,检查主库防火墙、端口、账号权限。 - 再看
Last_SQL_Error。如果是 SQL 执行失败,后台打印SHOW SLAVE STATUS\G的完整错误信息,不要只看最后一行。 - 确认主从版本是否一致或兼容。比如 MySQL 5.7 的二进制日志在还原到 8.0 时,可能因为默认字符集、sql_mode 或 binlog 格式兼容问题失败。
- 确认备份时点之后,主库是否执行过 DDL。逻辑备份还原到从库后,主库又执行了
ALTER TABLE,从库按旧表结构继续追日志时也容易报错。
一个比较隐蔽的问题:从库导入数据后,gtid_executed里包含了备份阶段的所有事务,但如果你在从库执行过任何手动 SQL,哪怕是改了权限表,GTID 集合也可能出现额外项。此时即使MASTER_AUTO_POSITION=1能连上主库,也可能因为事务集合存在不属于主库的 GTID 导致复制无法继续。遇到这种情况,最靠谱的方案是重建从库,不要在不干净的实例上反复重试。
5.2 Mydumper 导入数据时的 binlog 放大效应
如果你在从库恢复数据时,目标从库的 binlog 是开启的,那么导入数据的每个操作都会记录 binlog,数据本身加上 binlog 写入,磁盘 IO 会翻倍。
建议做法是:
- 恢复数据前临时关闭从库的 binlog 或者使用
sql_log_bin=0。 - 恢复完成后,再启用 binlog。
但有一点要提醒:如果你的主从复制链路本身要求从库继续级联复制,那么导入阶段关闭 binlog 不会影响后续CHANGE MASTER建立的复制,因为它通过 GTID 从主库拉取日志,而不是依赖从库自身的 binlog。这个操作只影响从库作为更下游主库时的复制能力,不影响当前链路。
5.3 备份完再开 GTID 没有意义
如果你为了这次迁移临时在主库开启 GTID,开始备份前就要确认已经开启完成。如果备份都跑完了,再去打开 GTID,那么备份文件里没有 GTID 信息,之后还是要手工对位点。
MySQL 在线开启 GTID 可以通过修改gtid_mode从 OFF 切到 OFF_PERMISSIVE,再切到 ON_PERMISSIVE,最后切到 ON,同时设置enforce_gtid_consistency=ON。这个过程要监控有无新事务产生异常,也需要一些时间。如果应急时不允许在线开关,也可以考虑用老式位点方式搭建从库,只是后续维护成本更高。
6. 这套方案的适用边界与更底层的工作流
6.1 哪些场景值得用,哪些场景不该硬套
这套方案非常适用于以下场景:
- 主库数据量大,业务持续写入,窗口紧。
- 团队已经使用 GTID,复制管理经验较丰富。
- 需要从 0 搭建一个用于读扩展或容灾的从库。
- 主库硬件支持多线程备份,IO 和 CPU 有一定余量。
不适合硬套的场景也要说清楚:
- 数据量很小,只有十几 G,用 mysqldump 可能更快,没必要上并行工具。
- 主库磁盘 IO 已经很高,再跑并行备份可能拖垮业务,此时要考虑从备份机或延迟备库导数据。
- 从库机器性能太弱,恢复阶段并行导入也可能变成瓶颈。
- 如果只是临时拉一个数据快照做分析,不需要建立持续复制,那用 mysqldump 更省事。
方案没有绝对优劣,关键是先判断场景,再选工具。
6.2 真正沉淀下来的是“可验证的迁移流程”
加从库这件事,一次跑通不难,难的是每次都稳定跑通。回头总结,我建议团队里把下面几个步骤固化成标准操作流程:
- 预检:检查版本、空间、权限、GTID 状态、业务写入负载。
- 备份:Mydumper 并行导出,控制线程数,保留元数据。
- 传输与恢复:先传文件,再恢复结构,再并行导入数据。
- 启动复制:确认 GTID 集合,执行
CHANGE MASTER,观察复制状态。 - 校验与演练:延迟归零后做数据抽样校验,完成一次切换演练。
这个流程的本质,是把“对复制位点的人肉记忆”转成“对 GTID 集合和一致性快照的自动衔接”。主库不再需要牺牲写入来换取一致性,从库的搭建也从“夜间战战兢兢的操作”变成了“白天也可以做的常规变更”。
下次再接到加从库的工单,可以先把上面这套流程在测试环境走一遍。只要预检、备份、恢复、复制、校验五步都能在测试环境里稳定复现,生产环境大概率也能在窗口内完成。真正的从容,从来不是靠某个工具锁不锁表,而是靠一套你自己验证过、记录过、知道失败点在哪里的完整流程。