先把话放前面:如果你问我生产环境新增一台MySQL从库,最稳的办法是什么,我的答案永远是"停服方式"。别觉得它老派,恰恰相反,在遇到线上数据量动辄上百G、又要保证主从数据严格一致的时候,那些花哨的在线方案反而容易在某个隐蔽细节上翻车。停服方式的核心逻辑只有一句话:把"数据还在变化"这个最大的不确定因素干掉,后面所有步骤都变成确定性操作。
这篇文章我会从架构选型讲起,再逐步拆解从锁表、备份、恢复到追平复制的完整流程。我会把每个关键命令的参数、为什么要这么配、以及我踩过的坑都写清楚,你照着走一遍,基本能自己独立完成一台从库的搭建。一部分内容是基于常见实践的补充,具体情况还是要结合你们自己的环境来调整。
1. 为什么新增从库要选"停服务"方式:架构思路拆解
1.1 从库在架构里解决什么问题
先想清楚你加从库到底是为了什么。这个"目的"直接决定你选择哪种新增方式,以及后续的风险容忍度。
- 容灾高可用:从库作为备库,主库挂了能快速切换。这种场景对数据一致性和复制延迟极其敏感,一点点位置偏差都可能导致切换后丢数据。
- 读写分离:报表、分析、搜索类查询打到从库上,降低主库负载。这种场景下从库的延迟容忍度略高,但数据不能错。
- 作为备份源:从库承担备份任务,避免备份工具直接压主库。这种场景讲究从库不能掉队。
- 延迟从库:故意让从库慢一段时间,误删数据时可以从延迟从库找回。这种需求对复制链路本身的准确性要求很高。
不同目的,对从库的"新鲜度"要求不同,但对"数据正确性"的要求是一致的。停服方式恰恰在正确性上最无争议:你锁住的就是一个静止一致的快照,复制起点就是那条明确记录的binlog位置,全程没有任何模糊地带。
1.2 新增从库的四种常见方式横向对比
实际工作中,新增从库一般逃不出这四种办法。我做了一张对比表,方便你看完心里有数:
| 方式 | 是否需要停服 | 复制起点获取 | 适用场景 | 主要风险 |
|---|---|---|---|---|
| xtrabackup在线全量备份 | 不需要 | 从 xtrabackup_binlog_info 或GTID获取 | 大库、无法停服 | 备份IO影响主库,坐标匹配有讲究 |
| mysqldump逻辑导出 | 不需要(但建议低峰期) | --master-data=2自动记录位置 | 小库、表结构简单 | 速度慢,恢复时间长 |
| MySQL 8.0 Clone Plugin | 不需要 | 集群内部自动管理 | 8.0环境、同版本 | 依赖GTID,网络与磁盘要求高 |
| 停服+物理快照拷贝 | 需要维护窗口 | SHOW MASTER STATUS 直接记录 | 大库、有维护窗口 | 业务要停止写入,需要申请时间 |
单看表格可能感觉不够直观,我说下取舍逻辑。xtrabackup在线方式功能强大,但它要求在备份过程中准确对应binlog坐标,工具内部通过备份点LSN与日志位置匹配,这个机制本身需要你对InnoDB原理有一定理解,一旦遇到大事务跨备份点的情况,新手很容易配错。
mysqldump适合小库,像是几百兆几个G的数据量完全没问题,但上了几十G甚至上百G,mysqldump的性能完全没法看,一台从库导出导入往往要跑好几个小时,效率很低。
Clone Plugin确实是8.0的福音,但它的前提是主从都要8.0.17以上,还要开GTID,如果你们环境还是MySQL 5.7,或者升级节奏没那么快,这条路没法走。
停服+物理拷贝看起来最笨,但它在一致性上几乎零风险。备份的数据是一整块一致的InnoDB快照,binlog位置在备份前就能精确锁定,恢复完成后从那个点开始追日志即可。对第一次搭从库或者生产环境数据量大的团队,这个方式反而是最稳妥的兜底方案。
1.3 "停服"为什么能把一致性做稳
在线备份最难的点,其实不是备份本身,而是"备份数据与binlog位置对齐"。只要主库还在写入,备份期间产生的增量事务就必须通过redo日志和binlog综合定位,稍有偏差,从库复制就会在某个事务上报错,或者更隐蔽地出现数据对不上。
停服方式直接把这个难点绕过了。操作思路是:先停止业务写入,让主库进入一个稳定只读的静止状态,此时执行FLUSH TABLES WITH READ LOCK保证所有表数据落盘一致,然后SHOW MASTER STATUS记录当前binlog文件名和偏移量。从这个瞬间开始,数据不再变化,那么无论你花多长时间备份、传输、恢复,复制起点永远是同一个坐标,不存在"备份期间又产生了新日志但没被包含进来"这种问题。
你可以把整个过程理解为拍全家福:先让所有人站在原地不动,摄影师记下每个人站的位置,然后开始摆弄相机。无论拍多久,最后大家归位时,每个人的站位和照片里一模一样。这就是停服方式最核心的价值。
2. 动手前不踩坑:环境检查、参数对齐与备份规划
2.1 先摸清主库的家底
我见过不少人拿到新机器就开始装MySQL,装完才发现和主库版本不一致,或者binlog参数完全对不上,来回返工。动手之前,先把主库的家底摸清楚,至少要做到心里有数。
第一步,确认版本信息。用SELECT VERSION();看看主库具体是哪个小版本,新从库的版本最好和主库保持完全一致,如果是5.7和8.0跨大版本新增从库,我不建议直接用物理备份方式,逻辑导出重放或者升级到同一版本再操作会更稳。
第二步,确认是否已经开启GTID。查一下gtid_mode和enforce_gtid_consistency两个变量。如果主库已经开了GTID,后续change master可以直接用MASTER_AUTO_POSITION=1,会省很多事;如果没开,就走传统的binlog文件名+位置方式。
第三步,确认binlog基础配置。log_bin是否ON、binlog_format是否为ROW、binlog_expire_logs_seconds保留多久。这些参数不仅关系到复制能不能启动,还决定你从备份到追平这段时间日志够不够用。
第四步,检查一下全局字符集和排序规则。SHOW VARIABLES LIKE 'character_set_server';和SHOW VARIABLES LIKE 'collation_server';,从库最好与主库一致,否则以后建表默认字符集不同,数据同步容易出现乱码隐患。
最后,在主库上提前创建一个专用的复制账号,不要用root直接跑主从。授权语句很简单:
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.2' IDENTIFIED BY '强密码'; FLUSH PRIVILEGES;注意授权要指定到从库的IP,尽量不要授权给'%'全开放,安全问题永远不能放松。
2.2 复制相关的关键参数清单
主从复制能不能跑得稳,很大程度上取决于配置参数是否对齐。我整理了一份关键参数对照表,你在改新从库配置文件时可以逐项核对。
| 参数 | 主库现状建议 | 新从库建议 | 说明 |
|---|---|---|---|
| server_id | 与从库不同 | 绝对唯一,不能和主库或其他从库重复 | 重复会导致IO线程直接报错 |
| log_bin | ON | ON | 从库开启binlog是为了将来它自己也有从库 |
| binlog_format | ROW | ROW | 复制安全性的基础,RBR避免数据不一致 |
| gtid_mode | 按现状 | 与主库保持一致 | 要么都开,要么都关 |
| read_only | OFF | ON | 防止应用误写从库 |
| super_read_only | OFF | ON | 8.0建议开启,超级账号也被禁止写 |
| log_slave_updates | 按需 | 按需 | 如果是级联复制链,必须ON |
| relay_log | 无 | 建议显式配置 | 避免默认中继日志名称冲突 |
| sql_mode | 按现状 | 与主库一致 | 不一致可能导致回放SQL报错 |
| character_set_server | 按现状 | 与主库一致 | 避免中文乱码与建表差异 |
这里面最容易犯的错误有两个。第一个是server_id重复,特别是MySQL 8.0默认server_id是1,如果新从库也忘了改,启动复制后IO线程会报Fatal error: The slave I/O thread stops because master and slave have equal MySQL server ids,进程直接退出,这时候你几乎一眼就知道问题所在。第二个是super_read_only没开,从库被应用误写后,主从数据出现偏差,排查起来非常痛苦。
另外如果你的从库还要再级联一个从库,那log_slave_updates=ON必须开。这表示从库回放relay log时也会写自己的binlog,否则下一层从库没有日志可拉。
2.3 备份空间、传输与binlog保留时间
很多人低估了备份和恢复过程中对磁盘空间的消耗。数据文件本身是一份,xtrabackup备份目录又是一份,transmit到新机器后还有一份,加上恢复过程中需要应用redo日志,临时文件又占一部分。
我有一个简单的估算公式:备份目录至少准备原数据大小的1.5倍,新机器datadir目录准备原数据大小的2倍。如果你压着最后一块磁盘空间的容量来规划,后面大概率会在prepare阶段爆盘。
如果数据量在几十G以内,直接用scp或者rsync传送即可。为了节省带宽和时间,可以在备份时就加上--compress --compress-threads=4,在备份阶段同时压缩,传过去之后xtrabackup --decompress解压。如果是上百G的数据,建议用rsync的断点续传功能,避免网络抖动导致整包重传。
还有一个经常被忽略的点:binlog保留时间。主库binlog如果只保留6小时,而你备份加上传输用了8小时,从库追平的时候发现目标binlog早就被purge掉了,从库直接报1236错误,数据追到一半就断掉了。
所以在计划新增从库的前一天,建议把主库的binlog保留时间临时调大,比如说调到48小时甚至更长,等从库全部追平后再恢复原来的保留策略。
SET GLOBAL binlog_expire_logs_seconds = 172800; FLUSH LOGS;以上操作需要 SUPER 权限,8.0中对应BINLOG_ADMIN权限,具体看你们账号授权。千万别忘了事后再调回来。
3. 停服新增从库完整实操:从维护窗口到复制追平
3.1 维护窗口内的检查与停写
停服操作最关键的一步永远是"确认真的可以停了"。我自己的流程是,在计划停服的前10分钟,先跑一轮健康检查。
SHOW PROCESSLIST; SELECT * FROM information_schema.innodb_trx;- 看processlist里有没有长时间运行的DDL、大查询
- 看innodb_trx里有没有未提交的长事务
- 和业务方确认,定时任务、消息队列、报表调度是否都已暂停
- 让应用运维把应用服务器的开关先切换为"只读/维护"状态
千万别小看这一步。我曾经试过在维护窗口里锁表,结果一个凌晨跑批的定时任务突然触发,把一张大表更新到一半,导致备份出来的数据和日志位置对不上,扛着锁处理了半小时才解决,差点影响业务恢复。
停写的方式有两种:一种是把应用服务直接停掉或切到维护页,这是最彻底的;另一种是通过入口网关把所有写接口摘掉,但实际执行成本高,因为你不一定完全清楚哪些流量在写。对于核心系统,最稳妥的做法是提前通知,把应用层停了再操作数据库。
3.2 锁定数据快照并记录binlog位置
业务停写之后,进入锁表阶段。这里我强烈建议你开两个终端窗口。
第一个终端窗口执行:
FLUSH TABLES WITH READ LOCK;执行完成后,这个会话会一直持有全局只读锁。注意,这个锁不是LOCK TABLES那种按表锁,而是全局性的只读锁。此时所有写入都会被阻塞。会话断开会自动释放锁,所以操作期间一定不要关掉这个终端。
第二个终端窗口执行:
SHOW MASTER STATUS; SHOW BINARY LOGS; SELECT @@GLOBAL.GTID_EXECUTED;记下这三个输出,重点关注File和Position两列。
mysql-bin.000123 | 456789 | ... | ... |这个坐标就是你从库复制的起点。如果主库开启了GTID,那也把GTID_EXECUTED值抄下来,后面可以用GTID方式启动复制。
这里有一个关于锁的细节:如果你用的是xtrabackup做物理备份,其实它内部也有机制保证一致性,你甚至可以不手动FLUSH。但既然我们做的是"停服方式",手动加锁能让整个流程按部就班、路径清晰,而且多一道保险没有坏处。锁表期间,备份工具读取的数据一定是同一时间点的快照。
3.3 xtrabackup全量备份与数据搬运
第二个终端里继续操作,执行物理备份。以最常用的xtrabackup为例:
xtrabackup --user=backup --password=xxx --host=127.0.0.1 \ --backup --target-dir=/data/backup/full-$(date +%F) \ --parallel=4 --compress --compress-threads=4各参数说明:
--backup:执行备份动作--target-dir:备份文件存放目录--parallel:并行拷贝表数据文件的线程数,一般按CPU核数一半设置--compress:压缩备份,传输和落盘都省空间--compress-threads:压缩线程数
备份完成后,回到第一个持有锁的终端,执行:
UNLOCK TABLES;然后查一下备份目录下是否生成了关键文件。xtrabackup备份目录里会有一个xtrabackup_binlog_info文件,里面记录了备份点对应的binlog坐标。但因为我们是在锁表后备份的,这个坐标应该和我们SHOW MASTER STATUS记到的坐标一致,你可以对它做个交叉验证,如果两个位置对不上,说明备份过程中可能有异常,建议重新来一遍。
数据搬运这一步,如果是一台全新的从库服务器,直接用rsync或scp带压缩传输到新机器上。我习惯用rsync加校验:
rsync -av --progress /data/backup/full-2025-01-01/ root@10.0.0.2:/data/backup/full-2025-01-01/传完之后在新机器上跑一个md5sum -c或者du -sh对比一下两边备份目录的大小和文件数量,确认传输完整。
3.4 恢复数据、调整配置并启动实例
备份传到新机器后,开始恢复。先在备份目录上执行prepare,把数据文件恢复到一致状态:
xtrabackup --prepare --target-dir=/data/backup/full-2025-01-01这个步骤会做两件事:应用redo log中的已提交事务,回滚未提交事务。相当于把InnoDB日志和表数据文件对齐,让数据文件本身变成一致快照,可以直接启动。
然后假设备份要恢复到新机器的/var/lib/mysql:
systemctl stop mysqld rm -rf /var/lib/mysql/* # 如果datadir是全新目录则可以跳过这一步 xtrabackup --copy-back --target-dir=/data/backup/full-2025-01-01 chown -R mysql:mysql /var/lib/mysql这里特别注意chown这步。很多人恢复完直接启动MySQL,结果一直在报Permission denied,就是因为数据文件的属主是root,不是mysql用户。
启动之前,把新从库的my.cnf调整好。下面是一个参考配置片段,你需要根据实际环境修改路径和参数:
[mysqld] server_id = 223 port = 3306 datadir = /var/lib/mysql log_bin = /var/log/mysql/mysql-bin binlog_format = ROW relay_log = /var/log/mysql/mysql-relay-bin relay_log_index = /var/log/mysql/mysql-relay-bin.index log_slave_updates = ON read_only = ON super_read_only = ON gtid_mode = OFF enforce_gtid_consistency = OFF如果主库是开启GTID的,那么gtid_mode和enforce_gtid_consistency都要改成ON,否则后续用GTID方式启动复制会报错。配置完成后启动实例:
systemctl start mysqld然后立刻去看错误日志tail -f /var/log/mysql/mysqld.log,看到ready for connections就说明启动成功。
3.5 建立主从复制链路与状态验证
新实例起来后,先登录MySQL,确认基础状态无误:
SELECT @@SERVER_ID; SHOW VARIABLES LIKE 'read_only'; SHOW VARIABLES LIKE 'log_bin';然后配置主从复制。以传统binlog位置方式为例:
CHANGE MASTER TO MASTER_HOST='192.168.1.10', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='强密码', MASTER_LOG_FILE='mysql-bin.000123', MASTER_LOG_POS=456789;执行START SLAVE;,等待几秒后看状态:
SHOW SLAVE STATUS\G关键字段判断:
Slave_IO_Running: Yes,表示IO线程已经连上主库并拉取binlogSlave_SQL_Running: Yes,表示SQL线程正常回放relay logSeconds_Behind_Master: 0,表示追平主库Last_IO_Error和Last_SQL_Error应该都是空
如果你用的是MySQL 8.0,官方新语法是CHANGE REPLICATION SOURCE TO ...和START REPLICA;,但传统的CHANGE MASTER TO和START SLAVE在8.0中仍然可用,只是被标记为废弃语法。为了减少兼容性问题,我建议你在8.0环境直接用新语法,在5.7环境用旧语法。
建立复制后再验证一次数据同步,思路是主库建一张临时测试表写一行数据,从库查一下:
-- 主库 CREATE DATABASE test_repl; USE test_repl; CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20)); INSERT INTO t(name) VALUES('hello');-- 从库 SELECT * FROM test_repl.t;如果能看到hello,从库复制链路就是通的。验证完成后记得把测试库和表删掉,别污染线上环境。
4. 常见问题与排查技巧实录
4.1 IO线程起不来:账号、网络与bind_address
新从库最常见的问题是Slave_IO_Running: Connecting或者直接变成No。排查路径我建议按顺序来:
第一,先看网络通不通。从库服务器上执行telnet 主库IP 3306,如果端口不通,检查主库的bind_address是否只绑定了127.0.0.1。MySQL默认配置里如果bind-address=127.0.0.1,局域网内的从库自然连不上,改成bind-address=0.0.0.0或者指定内网IP即可。
第二,看账号权限。复制账号必须至少有REPLICATION SLAVE权限。我遇到过一种情况,授权语句没问题,但主库开了skip_name_resolve,从库连接时基于IP反查主机名失败,被拒绝访问。解决办法是授权时直接用从库IP,不要用主机名。
第三,看server_id是否冲突。前面已经提到,主从server_id相同,IO线程会直接报错退出,你SHOW SLAVE STATUS时能看到明确提示。
第四,看防火墙。云服务器的安全组和主机iptables都要检查,不要只盯着MySQL端口。
4.2 SQL线程报错:1062、1032与1236的处置
SQL线程报错的典型场景是复制在某个位置执行SQL失败。最常见的三个错误码:
| 错误码 | 含义 | 典型原因 |
|---|---|---|
| 1062 | 主键重复 | 从库已有相同记录,可能是起始位置重复或误写从库 |
| 1032 | 记录不存在 | 回放时找不到对应行,可能是binlog_format不对或数据不一致 |
| 1236 | binlog读取失败 | 位置被purge、MASTER_LOG_POS写错、binlog文件被清理 |
处理思路分两种。如果你确认错误是因为起始位置写错造成的,就停下来,重新CHANGE MASTER,指定正确的位置。如果只是个别事务出错,且绕过不影响业务一致性,可以临时跳过:
STOP SLAVE; SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1; START SLAVE;但这个东西只能应急。最终还是要用pt-table-checksum和pt-table-sync做一次主从数据校验,把不一致的数据捞出来处理。这里我需要强调一句:跳过计数器不是万能药,对于一些涉及外键约束或关联表的事务,可能前面跳过了,后面关联行又报错,陷入循环。所以跳过前先确认这个事务确实是单点的、可安全忽略的。
1236错误如果是binlog被purge导致的,那就非常麻烦,只能重新做一次全量备份。这也是为什么我前面一再强调要提前调大binlog保留时间,不要在这个环节上赌运气。
4.3 复制延迟的定位与并行复制调优
新增从库追平之后,你还会遇到另一个问题:延迟。Seconds_Behind_Master字段是DBA最常看的指标,但它的含义经常被误解。
这个值本质是SQL线程当前执行到的relay log时间点与IO线程最近拉取的relay log时间点之间的差距。如果从库IO线程本身就没跟上,或者主库一直在产生大量写入,延迟会表现为持续上涨。如果SQL线程在等待大事务回放完成,延迟也可能瞬间拉高。
常见的延迟原因:
- 从库硬件配置差于主库,回放速度跟不上主库写入速度
- 主库有大事务(比如一次更新几百万行),SQL线程要很久才能重放完成
- 从库是单线程复制,而主库写入并发很高
- 网络质量不好,IO线程拉取binlog慢
针对并行复制,MySQL 5.7和8.0都支持slave_parallel_workers。5.7建议设置成4到8,并配合slave_parallel_type=LOGICAL_CLOCK;8.0默认就是基于commit order的并行复制,通常保持默认即可。
slave_parallel_workers = 8 slave_parallel_type = LOGICAL_CLOCK这个大事务导致的延迟,有个比较有效的思路是控制binlog写入,把大事务拆成小批次提交。但这属于业务侧改造,不是DBA立刻能解决的。对于批量变更,尽量分页提交,也可以配合max_allowed_packet检查是不是有超大包在binlog里占用了大量回放时间。
4.4 新增从库验收自查清单
复制链路通了、延迟降到0,不代表事情就结束了。我自己有一个固定的验收清单,每次新增从库后逐项打勾,少一项晚上都睡不踏实。
| 检查项 | 命令 | 期望结果 |
|---|---|---|
| IO线程状态 | SHOW SLAVE STATUS\G | Slave_IO_Running: Yes |
| SQL线程状态 | SHOW SLAVE STATUS\G | Slave_SQL_Running: Yes |
| 复制延迟 | SHOW SLAVE STATUS\G | Seconds_Behind_Master: 0 |
| 日志位置推进 | SHOW SLAVE STATUS\G | Exec_Master_Log_Pos 在增长 |
| 从库只读保护 | SELECT @@super_read_only | 1 |
| 数据抽样校验 | 主备分别查关键表count | 结果一致 |
| 监控告警 | 检查监控平台从库主机及复制状态 | 正常上报 |
还有一个建议:在主库用一个专用账号做一次小事务写入,然后从库立刻查询。如果测试数据几秒内出现在从库,基本可以放心。
一点个人经验
这套流程我前前后后跑过几十次,最大的体会是:停服方式看起来"浪费"了一个维护窗口,但它把所有不确定性都压缩到了"业务停写"这一个环节。对于大多数业务,深夜一个小时的窗口并没有想象中那么难申请。反倒是那些为了省窗口而选择的在线方案,一旦中途某个坐标没对齐,你会花更多时间去排查,最终还是要回到停服方式来兜底。
如果你第一次做,我建议你提前一天先在测试环境把整个流程跑一遍,把SHOW MASTER STATUS的输出、备份文件大小、恢复耗时都记录下来。真正上生产时,照着记录一步步执行,最多半小时就能完成主从追平。这套方法跑顺了之后,你会越来越认同一个观点:数据库架构上的很多所谓"高效方案",都不如一个简单直接的可靠方案让人安心。