上个月又接到一个典型需求:业务报表系统要上线,主库的读压力已经顶不住了,需要快速加一台 MySQL 从库分担只读流量。这种需求对做数据库运维的人来说太常见了,而“新增从库”这件事,说难不难,说简单也不简单——很多人第一次上手就卡在“备份怎么保证一致、binlog 位置怎么对齐”上,稍不注意就把一个本来半小时能搞定的窗口拖成几个小时甚至搞出数据不一致。
这篇文章不绕弯子,直接讲我用得最多、也最稳妥的一种方式:停服务方式新增从库。所谓停服务,就是在允许的维护窗口内,先停掉主库写入(或者直接停库),拿一个绝对一致的备份恢复出新实例,再通过 binlog 位置(或 GTID)把新从库挂到主库后面追增量。它不需要额外安装热备工具,不依赖 InnoDB 事务特性,逻辑简单,出了问题也容易回退,特别适合数据量中等、业务可以接受短停写的场景。
适合看这篇文章的人:被要求加从库但没把握的初级 DBA,自建 MySQL 环境的后端开发,以及想系统性搞懂主从复制原理的同学。我会把从架构判断、动手准备、完整命令,到最后的排障经验都过一遍,照着做基本能落地。
1. 为什么需要从库,什么时候该选“停服务”这种笨办法
1.1 从库到底解决了什么问题
先把动机理清楚。加从库不是炫技,绝大多数场景是三类诉求:第一是读写分离,主库承担写,从库承担报表、统计、后台任务等只读流量,把主库的 IO 和 CPU 释放出来;第二是容灾,主库故障时可以把从库提升为新主库,缩短业务不可用时间;第三是备份隔离,从库上跑备份任务,避免备份作业冲击主库。
但复制不是银弹。主从之间是异步回放,从库数据一定比主库“晚一点点”,这个延迟平时可能只有几秒,遇到大事务、DDL 或从库磁盘慢时会被放大到分钟级。所以设计读流量切到从库之前,必须确认业务能容忍“读到稍旧数据”。这个串得开头就定好,否则从库加完上线第一天就会被投诉“数据不对”。
另外要注意,加从库不是“把数据 copy 一份就行”。复制链路的本质是:从库启动一个 IO 线程连上主库,把自己缺的 binlog 拉到本地 relay log,再由 SQL 线程串行回放。这里面的“从哪个 binlog 文件、哪个偏移量开始拉”,就是新增从库最关键的起点问题。谁把这个点搞错了,谁就会在 Slave_IO_Running 或 Slave_SQL_Running 上翻车。
1.2 三种新增从库路线,为什么有时停服反而最稳
行业内新增从库主要有三条路,我先把对比摆出来。
| 方式 | 核心工具 | 一致性保证 | 对业务影响 | 适用场景 |
|---|---|---|---|---|
| 逻辑在线热备 | mysqldump --single-transaction | InnoDB 事务快照 | 几乎无锁,但快照有 undo 开销 | 小数据量、全 InnoDB、无大表 |
| 物理在线热备 | XtraBackup / MySQL Enterprise Backup | 物理文件级一致 | 影响较小,但需装额外工具 | 数据量大、团队成熟 |
| 停服务备份 | mysqldump 冷备 / 直接拷贝数据目录 | 绝对一致 | 需要停写窗口 | 数据量中等、可短停、混合引擎 |
很多人觉得“停服务”是落后方案,能在线搞定为什么要停机?但实战里恰恰相反,在几种特定条件下,停服反而是最优解:
第一,库里存在 MyISAM 表。mysqldump 的 --single-transaction 只对 InnoDB 生效,MyISAM 表在备份期间必须加锁才能保证一致,一旦表大,锁表时间完全不可控,与其在线锁半天,不如直接规划一个停写窗口。
第二,主库之前没开 log_bin,或者 binlog_format 配置不对。这种情况想加从库必须先改参数并重启主库,重启再顺手拿一份停服备份,一次窗口把两件事都办了。
第三,不想在生产环境引入 XtraBackup 这类需要安装内核模块、容易踩兼容性坑的工具,或者数据量也就几十 GB 以内,mysqldump 完全跑得动。这时候用停服方式,逻辑链路最短,每一步都可解释、可验证。
停服方式的本质,就是把“一致性保证”从复杂的在线快照机制,简化为“写入停了,备份一定是一致的”。它牺牲了可用性窗口,换来了最少的变量和最强的可预测性。对于维护窗口排得开、业务配合度高的场景,这是最值得推荐的新手路线。
2. 动手前必须想清楚的三件事
2.1 先给主库做一次数据体检,判断能不能走“轻量停服”
不要上来就开干。第一步是花十分钟掌握主库的真实情况,重点查三样:数据量、存储引擎、binlog 状态。
先看数据量,把所有库的大小拉出来:
SELECT table_schema, ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables GROUP BY table_schema ORDER BY size_mb DESC;如果总数据量只有几十 GB,mysqldump 逻辑备份完全够用;如果单表就有几百 GB,就老老实实评估物理拷贝或 XtraBackup,这篇文章的停服方案也支持物理拷贝,我会在后面的步骤里讲。
再看引擎分布,重点确认有没有 MyISAM:
SELECT engine, COUNT(*) AS table_count FROM information_schema.tables WHERE table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys') GROUP BY engine;全 InnoDB 和混着 MyISAM,备份策略完全不同。全 InnoDB 还可以用 --single-transaction 在线做,一旦有 MyISAM,锁表风险直接拉高,停服窗口的优先级就上来了。
最后确认 binlog 状态:
SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format'; SHOW MASTER STATUS;log_bin 是 OFF 的话,主库本身就没开复制的前置条件,必须先重启开启,这一步几乎必然伴随一次停服,正好把新增从库的备份一起做了。binlog_format 建议是 ROW,MIXED 也能用,但 ROW 在从库回放时语义最安全。SHOW MASTER STATUS 能拿到当前 binlog 文件名和位置,这是后面所有操作的参照物。
2.2 从库服务器的软硬件准备
从库在动手前就应该是一个“随时能启动”的状态,而不是备份拷过去了才开始装 MySQL。
版本上有个铁律:从库大版本必须和主库一致,小版本不要低于主库。主库是 5.7.44,从库就装 5.7 系列且小版本大于等于 44,8.0 的从库接 5.7 的主库这种跨大版本组合,除非你清楚知道 binlog 兼容性问题,否则别碰。
从库的 my.cnf 至少需要包含以下几项,我先给一份基础模板,后面会根据备份方式微调:
[mysqld] server-id = 2 port = 3306 datadir = /data/mysql log_bin = mysql-bin relay_log = relay-bin log_slave_updates = 1 read_only = 1 skip_slave_start = 1 expire_logs_days = 7server-id 是整个复制拓扑里的唯一标识,绝对不能和主库或其他从库重复,否则复制会异常。read_only=1 是给从库上的“安全锁”,防止业务误连从库写数据,注意复制线程本身不受 read_only 限制,所以不会影响主从回放。skip_slave_start=1 是我个人强烈推荐的参数,加了它之后 mysqld 启动不会自动拉起复制线程,给了你足够的检查时间,而不是一启动就报错刷屏。
如果主库开启了 GTID,从库的 my.cnf 还要加 gtid_mode=ON 和 enforce_gtid_consistency=ON。这一点建议在检查阶段就确认主库的 gtid_mode 状态,别到了 CHANGE MASTER 时才纠结。
2.3 主库上的账号、参数与工具准备
从库要连主库拉 binlog,必须先有复制账号。在主库上执行:
CREATE USER 'repl'@'%' IDENTIFIED BY '这里写个强密码'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'%'; FLUSH PRIVILEGES;REPLICATION SLAVE 是复制必需权限,REPLICATION CLIENT 是为了让 mysqldump 能执行 SHOW MASTER STATUS 读取 binlog 坐标(如果你用 --master-data=2,这个权限缺了会直接报错)。host 我一般写应用网段而不是裸写 %,能收窄就收窄,少留一个暴露面。
还要检查主库的 max_allowed_packet。备份文件恢复和复制大事务回放都跟它有关,建议主从都设到 64M 或更高:
SET GLOBAL max_allowed_packet = 67108864;另外,mysqldump 工具的版本跟随实例自带即可,但要保证是从主库机器上执行,或者能连到主库的机器上执行,不要拿一台安装着不同版本 MySQL 客户端的机器去 dump,版本差太远会导致 SQL 语法兼容问题。
备份的落盘空间也要提前确认。数据 50GB 的话,dump SQL 文件大概会膨胀到数据量的 1.2 到 1.5 倍,再加上 binlog 保留,/backup 目录得预留足够余量。空间不够导致 dump 中断,是这个环节最蠢也最常发生的翻车点。
3. 停服务方式新增从库全流程实操
3.1 停写入与一致性点确认
停服务方式的核心动作,就是在备份和恢复之间保证“同一个时间点”。我推荐按两条路线选一条,根据你能停到什么程度来决定。
路线 A:应用先停写,主库只保留只读访问。在维护窗口开始后,先在应用层面把写流量停掉(比如摘掉写接口或暂时关闭写入任务),然后连上主库执行:
FLUSH TABLES WITH READ LOCK;这一步会给所有表加上全局只读锁,相当于把数据库“冻结”在一个时间点。紧接着立刻记录 binlog 坐标:
SHOW MASTER STATUS;输出大概长这样:
+------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +------------------+----------+--------------+------------------+-------------------+ | mysql-bin.000123 | 456789 | | | | +------------------+----------+--------------+------------------+-------------------+把 File 和 Position 抄清楚,如果开 GTID 的话还要把 Executed_Gtid_Set 抄下来。这就是从库追增量的起点,错一位都不行。
路线 B:彻底停库。如果维护窗口里主库可以完全停机,那就更省心,直接停服务:
systemctl stop mysqld这时候不存在任何并发写入,你可以用物理拷贝的方式拿一份“死一致”的数据目录,也可以重新启动实例之后在只读状态下做逻辑备份。我通常建议既然已经停了,顺手把物理目录打包一份存到备份机,一举两得。
这里有个细节容易栽坑:执行 FLUSH TABLES WITH READ LOCK 之前,先看一眼 PROCESSLIST 里有没有长时间未提交的事务或大查询。如果有一个跑了很久的导出任务占着某张表的元数据锁,FLUSH 会一直卡在那里等锁,你的窗口时间就在等待中白白浪费。养成习惯,锁表前先跑一遍 SHOW PROCESSLIST。
3.2 用 mysqldump 拿一致快照
在锁表(或停服后)状态下,执行逻辑备份:
mysqldump -uroot -p \ --all-databases \ --master-data=2 \ --routines --triggers --events \ > /backup/master_backup_$(date +%Y%m%d_%H%M%S).sql注意,我没有加 --single-transaction。原因很简单:我们已经用 FLUSH TABLES WITH READ LOCK 或停库保证了一致性,再加 --single-transaction 反而会让 InnoDB 开启一个长时间运行的一致性快照,占用 undo 空间,没有任何额外收益。很多教程无脑把两个参数写在一起,那是针对在线热备场景的习惯,停服场景下不需要。
--master-data=2 的作用是让 dump 文件头部自动写入 CHANGE MASTER TO 的坐标信息(来源于 SHOW MASTER STATUS),恢复时不依赖你手抄的小纸条,直接 grep 文件头就能拿到。如果你用的是 GTID 环境,dump 文件里可能还会包含 SET @@GLOBAL.GTID_PURGED=... 语句,这个后面会在从库上处理。
备份完成后,无论如何都要先释放主库的全局只读锁:
UNLOCK TABLES;别锁着表就去干别的,锁太久会把业务恢复时间无限拉长,而且期间任何一条慢 SQL 排到队尾都会雪上加霜。备份文件确认生成、大小合理之后,立刻解锁,让主库恢复服务,窗口才能尽早关闭。
3.3 物理文件拷贝法(可选,针对大数据量)
如果主库数据量到了逻辑备份要跑几个小时的程度,停服方式下更推荐直接拷贝整个数据目录,速度远快于 mysqldump,而且省去恢复阶段长时间的 SQL 回放。
操作序列如下,主库已经停服的前提下:
tar -czf /backup/mysql_data_$(date +%Y%m%d).tar.gz /data/mysql # 传到从库,用 scp 或你惯用的传输工具 scp /backup/mysql_data_*.tar.gz new_slave:/backup/在从库上,先停掉干净初始化的实例,把原始数据目录挪开,再解压主库目录覆盖过去:
systemctl stop mysqld mv /data/mysql /data/mysql_bak_empty mkdir -p /data/mysql tar -xzf /backup/mysql_data_*.tar.gz -C /data/ --strip-components=2 rm -f /data/mysql/auto.cnf chown -R mysql:mysql /data/mysql systemctl start mysqld删除 auto.cnf 这步千万别省,它保存着实例的 server_uuid。如果新从库沿用主库的 auto.cnf,两个实例拥有相同 UUID,复制启动会直接报错,这是物理拷贝方式最典型的坑。删除后 mysqld 首次启动会自动生成一个新的 UUID。
物理拷贝还有一个隐含前提:主库数据目录里的配置文件路径、日志路径必须和从库一致,否则启动时很多路径会找不到。如果两边目录结构不同,宁可先改从库的 my.cnf 去适配拷贝过来的目录,也不要试图去改数据文件内部记录的位置信息。
3.4 从库数据恢复与参数落地
无论用逻辑备份还是物理拷贝,从库实例启动后都要完成同一件事:让实例内部数据达到“备份时间点”状态。
逻辑备份的恢复最直接:
mysql -uroot -p < /backup/master_backup_20250112.sql如果 dump 了 mysql 系统库,恢复后从库会保留主库的账号信息。生产环境里我一般只 dump 业务数据(用 --databases 指定业务库列表),避免把主库的账号体系整个覆盖到从库上,省得后续权限管理出现两套库两头改的混乱。
恢复完先别急着配复制,检查一下从库数据是否完整。随便选几个业务大表,跟主库比对 COUNT(*),或者抽字段看几条样例。多花五分钟验证,能省掉后面排查复制错误的几个小时。
如果是物理拷贝恢复,启动后同样先做抽样验证,然后确认从库的 server-id 确实和主库不同,并且 read_only=1 已经生效:
SHOW VARIABLES LIKE 'server_id'; SELECT @@global.read_only;server-id 和 server_uuid 是两个概念,server-id 是复制拓扑里的逻辑编号,必须手动配成不同值;server_uuid 是实例身份标识,物理拷贝时通过删 auto.cnf 来重置。两个都确认对了,才能进入下一步。
3.5 建立复制关系:两种姿势,位置与 GTID
现在到了整个操作最关键的一步,告诉从库“去哪儿拉日志,从哪里开始拉”。
位置方式适用于未开 GTID 或你想用传统位点控制的场景。从备份文件头部 grep 到 CHANGE MASTER TO 那几行,或者直接使用我前面抄下来的坐标,执行:
CHANGE MASTER TO MASTER_HOST = '主库IP', MASTER_PORT = 3306, MASTER_USER = 'repl', MASTER_PASSWORD = '复制账号密码', MASTER_LOG_FILE = 'mysql-bin.000123', MASTER_LOG_POS = 456789;MASTER_LOG_FILE 和 MASTER_LOG_POS 必须与备份时主库的状态完全一致。如果是逻辑备份,用 --master-data=2 生成的文件头自带坐标,直接读它最准确;如果是物理拷贝,同样是锁表那刻的 SHOW MASTER STATUS 坐标。两者本质都是“这个备份对应主库的哪个 binlog 位点”。
GTID 方式适用于主库已经开启 GTID 的环境,现在 5.7 和 8.0 生产环境普遍这么配。逻辑备份恢复后,从库执行:
CHANGE MASTER TO MASTER_HOST = '主库IP', MASTER_PORT = 3306, MASTER_USER = 'repl', MASTER_PASSWORD = '复制账号密码', MASTER_AUTO_POSITION = 1;如果备份文件里带 SET @@GLOBAL.GTID_PURGED,需要在 CHANGE MASTER 之前先手动执行,让从库知道自己“已经消费了哪些事务”。这步在 GTID 环境经常被漏掉,漏了的典型症状是 SQL 线程报主键冲突或找不到 GTID。执行 GTID_PURGED 时有个硬限制:必须清空 gtid_executed,所以要在还没有启动复制、也没有执行过任何写操作的时候做。
3.6 启动复制并逐项验证
配置完 CHANGE MASTER,就可以启动复制线程:
START SLAVE;然后立刻查看状态,这一条命令是新增从库操作里最重要的验收语句:
SHOW SLAVE STATUS\G重点看这几项,缺一不可:
- Slave_IO_Running 必须是 Yes,表示从库能连上主库、正常拉取 binlog
- Slave_SQL_Running 必须是 Yes,表示 relay log 正在被正确回放
- Seconds_Behind_Master 从一个大数逐渐归零,表示追赶进度的延迟
- Last_IO_Errno / Last_IO_Error 必须为空
- Last_SQL_Errno / Last_SQL_Error 必须为空
看到两个 Running 都是 Yes 之后,不要急着宣布成功,要做一次端到端验证:在主库建一张测试表写几行数据,过几秒回从库查。
-- 主库执行 CREATE DATABASE repl_test; USE repl_test; CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT, val VARCHAR(50)); INSERT INTO t1 (val) VALUES ('hello'), ('world'); -- 从库执行 SHOW DATABASES LIKE 'repl_test'; SELECT * FROM repl_test.t1;数据能查到,并且 Seconds_Behind_Master 保持在 0 附近,这条复制链路才算真正建立成功。验证完把测试库删掉,或者留着也行,团队自己约定好就行。
4. 常见问题与排查技巧实录
4.1 高频错误速查表
做这个操作多了之后,我发现大部分翻车点高度集中,直接做成速查表,方便你遇到问题时快速定位。
| 症状 | 常见原因 | 处理办法 |
|---|---|---|
| Slave_IO_Running 显示 Connecting | 网络不通、端口被防火墙拦截、账号密码错误 | 从库机器上 telnet 主库 3306 试试;用复制账号手动连接测试 |
| 错误 1045 Access denied | repl 账号权限不足或密码错误 | GRANT REPLICATION SLAVE, REPLICATION CLIENT 后 FLUSH PRIVILEGES |
| 错误 1236 / Got fatal error 1236 | binlog 被清理,或指定的 binlog 位置早于最早保留的 binlog | 检查主库 binlog 保留策略,必要时重新做一次完整备份再配复制 |
| 错误 1593 / 主从 server-id 冲突 | 主从配置了相同的 server-id | 修改从库 server-id 并重启 |
| 错误 1677 / Column 0 of table 不匹配 | 表结构不一致,常见于版本不同或手动改过结构 | 严格对齐表结构,同版本实例重建 |
| 错误 1062 / Duplicate entry | 从库上有额外写入,或位点/GTID 指定错误导致重复回放 | 从库务必 read_only;核对待同步起始位点 |
| 物理拷贝后报 server_uuid 相同 | 没删除 auto.cnf | 删除从库 auto.cnf 后重启 |
| 恢复 SQL 时报 packet too large | max_allowed_packet 太小 | 主从都调大,建议 64M 以上 |
最隐蔽也最致命的是 1236,它往往不是配置写错,而是主库 binlog 保留时间太短。比如你花了两个小时恢复备份,等配复制的时候,备份对应的 binlog 文件已经被清理了,从库自然拉不到起点。所以判断 binlog 保留是否够长,要在动手前就做,别等恢复完才想这事。
4.2 我实际踩过的几个坑
第一个坑是“假解锁”。有一回我执行完 mysqldump 后忘了 UNLOCK TABLES,主库全局只读锁挂了快二十分钟,业务侧所有写请求开始堆积,报障电话直接打过来。自那之后我把“备份完成立刻解锁”写进了清单,并且在解锁后顺手执行 SHOW PROCESSLIST 确认锁确实释放了。
第二个坑是 GTID_PURGED 重复执行。从库第一次导入备份时,dump 文件里已经包含了 SET @@GLOBAL.GTID_PURGED=xxx,我手动执行了一遍又开始复制,结果报错说 gtid_executed 非空不允许设置。后来才明白,这个语句在 START SLAVE 前只需执行一次,别画蛇添足。
第三个坑是物理拷贝后权限不一致。主库的数据目录里文件属主是 mysql:mysql,解压到从库后忘了 chown,mysqld 启动直接因为权限被拒而失败。看起来是个小细节,但真到那一刻,你可能会从解压转身回来盯着 error log 发呆半天。所以我在物理拷贝步骤里把 chown 单独拎出来当成强制步骤。
第四个坑是忘了确认主库 binlog 保留时长。一次规模较大的初始化,备份恢复花了三个小时,等配复制时 1236 报错,查了主库才发现 binlog 只保留一天,备份时间点的 binlog 已经被滚掉了。最终重新做了一轮备份恢复。从那以后,凡是要新增从库,我第一件事就是查 expire_logs_days 和 binlog_expire_logs_seconds,并确认主库 binlog 的完整覆盖范围。
给新手一个良心的建议:做完一次停服新增从库之后,把整个流程沉淀成自己的 checklist,从查数据量、查引擎、查 binlog 状态,到创建账号、锁表、备份、恢复、CHANGE MASTER、验证,每一条打勾后再进行下一步。这套流程我做了无数遍,现在基本可以不看文档直接操作,但 checklist 仍然留着——这玩意儿不是给新手准备的仪式感,而是给熬夜维护的自己兜底用的。你永远不知道下一次窗口里,前面那位同事是不是已经帮你执行过一半操作了。