直接开工,这篇东西我打算按自己在生产环境里摸爬滚打的经验来写,不整那些教科书式的废话,重点放在“你真去搭一套主从、真去做读写分离时,会遇到什么、该怎么选、坑在哪里”上。
1. 先拆掉概念上那堵墙:主从复制和读写分离到底是不是一回事
提到MySQL,很多人会把主从复制和读写分离当成一个东西,其实这是两个层次完全不同的概念。主从复制是数据层面的机制,解决的是“数据怎么从一台MySQL实例安全、完整地同步到另一台实例”的问题;读写分离是流量层面的架构策略,解决的是“应用发过来的SQL,怎么让写请求走主库、读请求走从库”的问题。复制是读写分离的基础设施,没有复制就没有可用的从库,但有了复制不代表你就能自动享受读写分离带来的扩展红利。
我见过不少团队,DBA辛辛苦苦把一主两从的复制搭好了,开发同学也把连接池配了两套,结果上线后发现从库的CPU飙满、主库反而闲得发慌。查了一圈才发现,应用里有个定时任务,每分钟会把全量配置表扫一遍做本地缓存,这个查询走了从库没错,但那是全表扫描,一次就是好几秒,从库怎么可能扛得住。这就是典型的“复制搭好了但读写分离没设计好”——你只解决了数据流向,没解决查询模型。
所以这篇文章,我想把这两条线分开讲清楚:先讲复制这条链路是怎么跑通的,再讲读写分离这个流量闸门该怎么设计,最后把那些“看着文档没问题、一到线上就出事”的坑逐个拆给你看。写这篇文章的缘由也很直接:上周帮一个老朋友排查他们生产环境的从库复制中断问题,顺手把整个排查链路走了一遍,觉得很有代表性,干脆整理出来,给正在搭主从或者被读写分离困扰的朋友一个参考。
先说点入门的东西。MySQL主从复制的官方文档里,核心角色就三个:主库(Master)、从库(Slave)、以及可选的中间层(比如半同步复制里的ACK机制)。从架构上看,最常用的是一主一从和一主多从,多从的场景通常是为了分摊读流量,或者给数据分析、报表查询提供独立的数据源,避免分析任务把主库拖垮。
这里我强烈建议,你要在脑子里建立的第一张图是:主库是唯一能写数据的源头,从库是主库的只读副本。从库可以设置成read_only,但read_only只是挡掉了普通账号的写操作,super权限的账号依然能写,所以真正严格的线上环境里,很多人会再加上super_read_only。这个参数很多新手不知道,等到从库因为一个手滑的UPDATE产生数据漂移,复制链路直接中断的时候才后悔莫及。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 复制链路的底层逻辑:binlog、relay log 与三个线程的接力赛
2.1 一条更新语句在主从之间经历了什么
要玩转主从复制,你得先理解复制内部那条“流水线”。一条UPDATE语句在主库上提交之后,会经历这么几个动作:
- 主库把这条变更写入二进制日志(binlog),
sync_binlog参数的设置决定了事务提交时binlog落盘的时机; - 从库通过IO线程去主库拉取binlog,把新产生的日志片段写到本地的中继日志(relay log);
- 从库的SQL线程读取relay log里的内容,在从库本地按顺序重放这些变更。
整个复制模型依赖三个线程:主库上的Binlog Dump线程,从库的IO线程和SQL线程。在从库上执行SHOW SLAVE STATUS\G,你能看到Slave_IO_Running和Slave_SQL_Running两个状态,这俩线程任何一个是No,都代表复制已经中断了。
Seconds_Behind_Master这个指标,是很多初学者的“安心丸”,也是很多老手的“烟雾弹”。它表示SQL线程当前执行的binlog时间戳和IO线程拉到的最新binlog时间戳之间的差值。注意,如果从库IO线程还没有拉到这些binlog,那么这个指标会是0,让你误以为完全没有延迟,其实主库可能已经领先从库十万八千里了。所以监控延迟不能只看这一项,要结合主从两个库各自的Master_Log_File和Read_Master_Log_Pos来综合判断。
2.2 binlog 的三种格式,选错了迟早要出事
binlog的格式有STATEMENT、ROW和MIXED三种,这可能是复制配置里最值得花时间理解的一个参数。
STATEMENT格式记录的是SQL语句本身,日志量小,但有些语句是不确定性的,比如NOW()、UUID(),或者带LIMIT的UPDATE,在从库重放时可能产生不一样的结果;ROW格式记录的是每行数据的变更前后值,日志量更大,但最安全、最精确,任何变更都能精确重放;MIXED格式由MySQL自己判断,默认走STATEMENT,遇到不安全语句自动切成ROW。
从MySQL 5.7.7开始,默认的binlog格式就是ROW,生产环境我也建议统一用ROW。为什么?因为ROW格式不仅可以精确复制,还能配合binlog_row_image参数控制日志量,更重要的是,很多数据同步工具(比如Canal)都要求主库开启ROW格式,否则无法从binlog里解析出完整的数据变更记录。
日志量变大确实是个现实问题。我之前在一个日增千万级订单的表上试过,切换成ROW格式后binlog体积大概膨胀了2.5倍,磁盘空间和网络带宽都要重新评估。但和“复制错数据”的风险相比,这点存储成本完全可以接受。磁盘不够可以加,网络慢可以调,数据错了可是要出大事的。
2.3 GTID:别再用日志文件和位置点来绑定复制了
传统的复制方式,从库配置CHANGE MASTER TO的时候要指定MASTER_LOG_FILE和MASTER_LOG_POS,也就是“从主库的哪个binlog文件、哪个位置开始拉”。这种方式的痛点很明显:一旦主库binlog被清理,或者从库重新搭建、切换主库,你很难精确找回那个位置点。
GTID(全局事务标识符)的出现就是为了解决这个问题。每个在主库上提交的事务都会生成一个全局唯一ID,从库通过GTID来自动判断哪些事务已经执行过、哪些还需要拉取,主从切换的时候不需要再手工指定文件位置。配置起来就是在主从两边的my.cnf里加上:
ini复制gtid_mode = ON
enforce_gtid_consistency = ON
然后在从库上执行:
sql复制CHANGE MASTER TO
MASTER_HOST='10.0.0.1',
MASTER_USER='repl',
MASTER_PASSWORD='YourPassword',
MASTER_AUTO_POSITION = 1;
MASTER_AUTO_POSITION = 1就是让从库自动用GTID定位。这里有个坑,从库上如果做过非事务操作、或者有手工插入的事务,GTID集合就会产生空洞,导致自动定位失败。所以启用GTID复制之后,任何在从库上的手工写操作都要格外谨慎。
3. 别急着复制数据:一主一从的完整搭建步骤与参数选型
3.1 环境准备:用Docker快速起两个实例
我不推荐你拿现成的生产库做实验,最好的方式是先在本地用Docker起一套主从环境,把参数调明白了再上生产。Docker安装MySQL很简单,但有一个细节需要注意:容器默认的/etc/mysql/conf.d目录可以挂载自定义配置,这个目录里的.cnf文件会被MySQL自动加载,用好它能省掉很多重建容器的麻烦。
先创建主库的数据目录和配置目录:
bash复制mkdir -p /data/mysql-master/conf /data/mysql-master/data
mkdir -p /data/mysql-slave/conf /data/mysql-slave/data
主库的配置文件/data/mysql-master/conf/my.cnf如下:
ini复制[mysqld]
server-id = 1
log-bin = mysql-bin
binlog_format = ROW
gtid_mode = ON
enforce_gtid_consistency = ON
port = 3307
character-set-server = utf8mb4
bind-address = 0.0.0.0
注意,我用3307这个端口来区分主库,避免和本机已有的MySQL冲突。然后启动主库容器:
bash复制docker run -d --name mysql-master \
-p 3307:3306 \
-v /data/mysql-master/conf:/etc/mysql/conf.d \
-v /data/mysql-master/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=Root123456 \
mysql:8.0
这里的MYSQL_ROOT_PASSWORD是容器首次启动时初始化用的,如果/data/mysql-master/data已经有数据,这个环境变量会被忽略,这一点容易让人困惑,如果发现密码不对,多半是数据目录里已经有一份旧的认证信息了,此时要么清掉数据目录,要么用docker exec进容器重置。
从库的配置基本一样,把server-id改成2,port改成3308,只保留gtid_mode和enforce_gtid_consistency,从库本身不需要开log_bin(如果不开的话),但为了以后级联复制或者作为备选主库使用,我建议还是把log_bin也打开。
这里有个关键点:从库虽然可以不开log_bin,但relay_log是必须有的。从库的SQL线程执行完relay log里的内容后,会生成一个relay-log.info文件记录执行进度,这个文件如果损坏,从库可能重复执行或跳过事务,导致数据不一致。
3.2 主库创建复制账号,从库执行CHANGE MASTER TO
主库上创建一个专门用于复制的账号,权限无需给太大,只要REPLICATION SLAVE和REPLICATION CLIENT两个权限就够:
sql复制CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl123456';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
REPLICATION CLIENT这个权限很关键,它允许你执行SHOW MASTER STATUS、SHOW BINARY LOGS这类命令。不给这个权限,你连主库当前日志位置都没法查看,排查问题会非常被动。
从库上执行复制连接:
sql复制CHANGE MASTER TO
MASTER_HOST='127.0.0.1',
MASTER_PORT=3307,
MASTER_USER='repl',
MASTER_PASSWORD='Repl123456',
MASTER_AUTO_POSITION = 1;
START SLAVE;
SHOW SLAVE STATUS\G;
看到Slave_IO_Running和Slave_SQL_Running都是Yes,基本就成功了。但是这里还有一个隐藏条件:Master端如果要被Docker容器外访问,必须保证网络端口映射正确,并且主库的bind-address不能是127.0.0.1。我见过有人折腾了半天主从连不上,最后发现是bind-address导致的。
如果你的主库不是空的,已经有了存量数据,需要先在主库做一次全量备份,再恢复到从库。备份工具我推荐mysqldump,注意加上--master-data或者--single-transaction:
bash复制mysqldump -uroot -p -h127.0.0.1 -P3307 \
--single-transaction --set-gtid-purged=ON \
--all-databases > backup.sql
然后在从库上导入:
bash复制mysql -uroot -p -h127.0.0.1 -P3308 < backup.sql
导入后重新执行CHANGE MASTER TO即可。GTID模式下最怕的是备份导出时把系统表的数据也带过去,然后目标库的GTID集合出现冲突,所以实际操作中--set-gtid-purged=ON是必须的,它会在导出的SQL里写入SET @@GLOBAL.GTID_PURGED,从库导入后就能正确跳过已有的历史事务。
3.3 主从参数选型:哪些参数建议一步到位
我把自己在建主从时常用的参数整理成一张表,方便你照着配置,重点参数我都会解释清楚:
| 参数 | 建议值 | 原因 |
|---|---|---|
server-id |
全局唯一 | 同一个复制架构里不允许重复,否则同步会乱 |
log-bin |
必开 | 主库必开,从库建议也开 |
binlog_format |
ROW |
精确复制,配合Canal等生态 |
gtid_mode |
ON |
简化主从切换和故障恢复 |
enforce_gtid_consistency |
ON |
强制事务安全,阻止GTID不支持的操作 |
sync_binlog |
1 |
每次提交强制刷盘,避免主库宕机丢binlog |
innodb_flush_log_at_trx_commit |
1 |
保证事务提交后日志不丢 |
expire_logs_days(或binlog_expire_logs_seconds) |
7天 | 太短会导致从库来不及拉取 |
read_only |
从库开 | 防止普通账号写入从库 |
super_read_only |
从库开 | super权限也挡掉,更安全 |
sync_binlog=1和innodb_flush_log_at_trx_commit=1这两个配置结合起来,能保证在主库发生崩溃时,最多只丢一个事务,这在金融级场景里是底线要求,代价是会牺牲一部分写入性能。如果你追求性能可以适当放宽,但至少你要知道这是拿什么换来的。
3.4 一个小实验:直接验证复制是否真的在工作
搭好之后,不要直接上线业务,先做一个最简单的验证:在主库建一张测试表,插入几行数据,然后去从库查。
sql复制-- 主库
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO t_user(name) VALUES ('alice'), ('bob');
从库上执行:
sql复制USE testdb;
SELECT * FROM t_user;
如果能看到那两行数据,说明基本链路通了。接着验证延迟监控,在主库多插入一些数据,在从库上反复执行:
sql复制SHOW SLAVE STATUS\G
观察Seconds_Behind_Master数值的变化。这一步虽然简单,但能帮你建立对延迟指标的直觉——刚插入数据时一定要看Master_Log_File和Read_Master_Log_Pos有没有变化,单纯看Seconds_Behind_Master会被骗。
4. 读写分离的四种落地姿势:中间件、代理、组件、还是应用层自己切
4.1 四种方案的对比与选型
主从搭建只是第一步,业务真正要接进来,必须做读写分离。这个环节选错了,后面的运维成本会直线上升。我把常见的四种方案摆出来对比一下:
| 方案 | 代表组件 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|---|
| 独立代理层 | MySQL Router、ProxySQL、MaxScale | 对应用透明,切换方便,天然支持连接池和读权重 | 多一层网络开销,代理本身要高可用 | 团队没有精力改代码,或异构语言多 |
| 客户端组件 | ShardingSphere-JDBC | 性能损耗小,功能强大,支持分库分表和读写分离 | 需要改造应用代码,各语言版本成熟度不一 | Java技术栈,需要同时做分库分表的团队 |
| 中间件一体化方案 | MyCat、Vitess | 可做分片+读写分离统一管理 | 引入复杂度高,排查链路长 | 超大业务量,架构强管控场景 |
| 应用层手动切 | 多个数据源+路由规则 | 零依赖,完全可控 | 对业务代码侵入大,容易漏改 | 就一两张表做读写分离的小项目 |
如果让我给刚起步的团队推荐,我会优先推MySQL Router或者ProxySQL。原因很简单:不用动一行业务代码,数据库团队自己就能把读写分离落地,出问题也可以回滚。MySQL Router是官方组件,轻量,但配置和监控选项偏简单;ProxySQL功能更丰富,支持查询规则、线程池、连接复用,生产环境里我用得更多。
4.2 用MySQL Router跑通一个最小可用配置
用MySQL Router只需要一个配置文件,先把路由规则定义清楚。假设主库在127.0.0.1:3307,从库在127.0.0.1:3308,监听6446端口,一个典型配置如下:
ini复制[DEFAULT]
log_level = INFO
[routing:read_write]
bind_address = 0.0.0.0
bind_port = 6446
mode = read-write
destinations = 127.0.0.1:3307
protocol = classic
[routing:read_only]
bind_address = 0.0.0.0
bind_port = 6447
mode = read-only
destinations = 127.0.0.1:3308
protocol = classic
应用层只需要配置两个数据源:写请求走6446端口,读请求走6447端口。MySQL Router会自动把读写流量分流到对应的MySQL实例上。这个方案的优点是数据库拓扑变化(比如主从切换)时,你只需要改Router配置,应用根本不知道底层发生了什么。
但这里有一个非常关键的坑:MySQL Router本身是单点。如果Router挂了,所有数据库请求都会中断。所以生产环境至少要部署两个Router实例,用keepalived做VIP漂移,或者放在负载均衡器后面。很多团队第一次上MySQL Router时都没意识到这一点,等Router的机器宕机、业务全红的时候才追悔莫及。
4.3 应用层的读写分离:Spring或轻量框架里的做法
如果你是Java技术栈,不想额外引入代理,用Spring的AbstractRoutingDataSource可以做应用层的读写分离。核心思路是:定义一个动态数据源,每次数据库操作前根据当前事务的读写属性,选择主库或从库连接。
模拟写一下这个路由逻辑:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
DbContextHolder是一个ThreadLocal,里面存了MASTER或SLAVE的标识。查询方法上加个@ReadOnly注解,AOP拦截到之后就把标识设为SLAVE,否则设为MASTER。
java复制@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(readOnly)")
public void setReadOnly(ReadOnly readOnly) {
DbContextHolder.setDbType(DbType.SLAVE);
}
}
这里有个性能隐患要提醒你:如果主从延迟比较大,应用层刚写入的数据从从库读不到,用户刷新页面看到的是旧数据。要解决这个数据一致性问题,通常的做法是:对强一致性的读请求强制路由到主库,或者把刚写过的数据放到缓存里,下次优先读缓存。千万不要以为读写分离只是“读写分离”这么简单,它引入的最大代价就是最终一致性,这个代价你得在设计阶段就想清楚。
4.4 读流量扩容的边界:什么时候该加从库,什么时候该上缓存
很多团队对“水平扩展读能力”有个误区:一觉得从库压力大就加从库,加到七八个之后发现主库的binlog分发线程成为了瓶颈,因为主库要为每个从库各开一个dump线程,从库越多,主库的压力越大。
所以加从库不是无限的,通常一个主库挂3~5个从库是常见形态,再多就要考虑用级联复制(主 -> 分发层从库 -> 二级从库)或者上缓存。缓存的优先级其实应该高于加从库:同一个热点数据,比如商品详情、配置项,命中缓存的QPS可以轻松上万,底层数据库可能只需要承受几十QPS的透传流量。先用缓存挡住读流量,再让从库承担真正的查询压力,架构才能撑得住。
5. 复制延迟与数据一致性:主从架构里绕不开的两个坑
5.1 延迟是怎么产生的,为什么只靠加从库解决不了
主从复制从原理上就有天然延迟,因为这是一个“主库写 -> binlog落盘 -> 从库IO线程拉取 -> 网络传输 -> relay log落盘 -> 从库SQL线程重放”的串行链路。最耗时的往往不是传输,而是从库SQL线程的单线程重放——早期MySQL从库只能串行执行relay log,虽然5.7之后有了MTS(多线程复制),但在重放事务时仍然有很多限制。
生产环境里最容易引发高延迟的操作有这么几类:
- 一个大事务:比如一条UPDATE语句把一张千万级别的表整个更新一遍,这条事务会在主库执行很久,在从库也要执行很久,期间
Seconds_Behind_Master会飙升; - DDL操作:
ALTER TABLE在从库重放时一样需要重建表,而且会看到从库的SQL线程长时间卡住; - 从库配置比主库低:磁盘随机写能力、CPU核数、内存大小都会影响重放速度;
- 从库上还有别的查询任务在跑:报表、大查询、全表扫描,这些查询会跟SQL线程抢资源,拖慢重放。
给从库增加配置、优化慢查询、把大事务拆小,这些是解决延迟的根本方向。只靠增加从库数量分担读流量,其实对单条链路的延迟没有任何帮助,因为你依然是每个从库各跑各的relay log重放。
5.2 半同步复制:牺牲一点性能换更低的丢数据风险
很多人以为主从都搭好了,数据就“安全”了,其实在默认的异步复制模式下,主库提交事务后,不会等待从库确认接收binlog,一旦主库在binlog还没送到从库时宕机,这些数据就丢了。
半同步复制(Semi-Synchronous Replication)解决了部分这个问题:主库在提交事务前,必须等待至少一个从库确认已经收到binlog(并写入relay log)。这样你最多只会丢失还没来得及发送的少量事务,而不是所有新写入的数据。
配置半同步需要加载插件,MySQL 5.7之后默认自带插件:
sql复制-- 主库
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 = 1000;
-- 从库
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
STOP SLAVE;
START SLAVE;
注意rpl_semi_sync_master_timeout这个参数,单位是毫秒,它定义了主库等待从库ACK的最长时间。如果超过这个时间,主库会自动降级为异步复制,继续提交事务,避免阻塞业务。这个降级机制是保护可用性的,但也意味着极端故障下还是可能丢数据。怎么权衡,得看你的业务对丢数据的容忍度。
5.3 数据不一致的排查:当复制状态正常、数据却对不上时
复制状态正常不代表数据一定一致,这是运维里最容易踩的暗坑。最常见的不一致来源是:
- 从库上有
super账号不小心执行了写操作; - 主从两边的字符集、排序规则不一样,导致重放结果有差异;
- 主库执行了非事务引擎的变更,比如
MyISAM表,不受复制机制保护; sql_mode在主从两边的配置不一致,导致某些语句在主库能执行、从库报错。
排查数据一致性,我推荐两个工具:pt-table-checksum做校验,pt-table-sync做修复。这两个工具都是Percona Toolkit里的,校验原理是:在主从两边分别对表做分块checksum,然后对比差异。
bash复制pt-table-checksum h=127.0.0.1,P=3307,u=root,p=Root123456 \
--databases=testdb --replicate=testdb.checksum
跑完之后,从库上查testdb.checksum表就能看到哪些表不一致。修复操作要用pt-table-sync,并且强烈建议先加--dry-run参数预览将要执行的SQL,确认没问题再真正执行:
bash复制pt-table-sync --execute \
--databases=testdb \
--tables=t_user \
h=127.0.0.1,P=3307,u=root,p=Root123456 \
h=127.0.0.1,P=3308,u=root,p=Root123456
这里再强调一次:这个工具的使用要极其谨慎。它会先在从库上修复数据,再回到主库执行对应变更,如果表结构复杂、或者有外键约束,可能产生意想不到的副作用。生产环境用之前一定先备份。
6. 从库复制中断的完整排查链路:从报警到恢复的一次实战
6.1 一次真实的复制中断:故障现场和第一反应
前几天帮朋友排查的那个案例,就是从库复制中断。报警信息很简单:Slave_SQL_Running: No,错误代码1062(主键重复)。第一反应是看Last_Error字段的具体报错内容,这一步非常重要,它直接指明了故障类型。
sql复制SHOW SLAVE STATUS\G
重点看几个字段:Slave_IO_Running、Slave_SQL_Running、Last_IO_Errno、Last_SQL_Errno、Last_SQL_Error、Seconds_Behind_Master。
这里的报错是Duplicate entry '100' for key 'PRIMARY',说明SQL线程试图往从库插入一条主键为100的记录,但这条记录已经存在了。为什么会存在?大概率是从库之前有业务逻辑绕过read_only直接写了数据,或者主库执行了INSERT ... ON DUPLICATE KEY UPDATE而两个表的初始数据不一致。
6.2 恢复步骤:不要直接跳过,先搞清根因
很多人看到1062的第一反应是执行STOP SLAVE; SET GLOBAL sql_slave_skip_counter=1; START SLAVE;,跳过这个事务继续复制。这确实是一条“快路子”,但极其危险——跳过的那个事务,可能包含多条变更,也可能是一个大事务的一部分,跳过去之后从库就会比主库少一批变更,而且你自己根本不知道少了什么。
正确做法是:
- 确认主库当前状态,拿到主库的binlog文件名和位置,以及从库当前执行到的位置;
- 对比主从两库中出错的那一行数据,判断是主库多了一条、还是从库多了一条;
- 修正从库数据,如果是从库多了垃圾数据,先删除/更正从库那一行,再
START SLAVE; - 如果无法精确修数据,就从主库重新拉一份该表的快照,或者重建从库。
这次案例中,我们在主库上查到该记录的created_at明显晚于从库那条,确认是从库之前被写过一条“影子数据”。处理方法是先从从库删掉那条多余记录,然后启动复制,Seconds_Behind_Master逐渐降回0,复制恢复正常。
6.3 复制中断的常见错误速查表
| 错误码 | 典型报错片段 | 原因 | 处理思路 |
|---|---|---|---|
| 1062 | Duplicate entry | 主键冲突,从库已有重复数据 | 比对主从,清除从库多余记录,避免盲目skip |
| 1032 | Could not execute Delete_rows event | 从库缺少要删除或更新的行 | 比对主从数据,按主库补全从库数据 |
| 1236 | Could not find first log file name in binary log index | 从库需要的binlog已被清理 | 备份主库或从现有备份重建从库 |
| 1594 | Relay log read failure | relay log损坏 | 从库上RESET SLAVE后重新拉取 |
每次处理完复制中断,都要记一条“根因分析”,不要简单地说“主键冲突”。要深入问一句:为什么从库会有一条不该存在的记录?是权限配置问题?是read_only没开?还是某个临时工手滑了?不解决根因,同样的坑还会反复踩。
6.4 主从切换与故障恢复
最后聊一下主从切换。正常情况下,主从切换是灾难恢复的核心动作,但在没有自动化工具的情况下,手工切换要非常谨慎地按顺序执行:
- 确认主库是否彻底不可用,还是只是网络抖动;
- 在从库上执行
STOP SLAVE; RESET SLAVE ALL;,解除从库身份; - 把从库的
read_only和super_read_only关掉,提升为新的主库; - 修改应用连接配置或VIP指向,让写流量切到新主库;
- 如果原主库还在运行,尽快把它设为新主库的从库,拉取缺失数据。
多实例的自动切换我建议用Orchestrator或者MHA来做,靠人肉切换大概率会在凌晨三点手忙脚乱。切换的时候还有一个容易忽略的问题:业务连接池里的长连接。数据库切换后,连接池里还握着旧主库的连接,如果不做连接有效性校验或重连,应用会持续报错。所以应用层的连接池要配置testOnBorrow或者vaildateQuery=SELECT 1,保证拿到的是新主库的连接。
7. 日常巡检与经验总结:一套我一直在用的自检清单
写到最后,把我日常巡检主从环境时必看的一些点列出来,算是给这篇文章收个尾。这些点看起来琐碎,但真出事的时候能救命。
复制状态巡检清单:
- 每天至少检查一次
SHOW SLAVE STATUS\G,确认Slave_IO_Running和Slave_SQL_Running都为Yes; - 监控
Seconds_Behind_Master,并设置阈值报警(比如超过30秒就要预警),同时结合Master_Log_File和Read_Master_Log_Pos看IO线程是否有积压; - 检查
Last_IO_Error和Last_SQL_Error,只要有非空值就要立刻处理; - 定期用
pt-table-checksum核对核心表的checksum,防止静默不一致; - 检查主库binlog保留时间,确保从库不会因为binlog被清理而中断;
- 检查磁盘空间,特别是relay log的膨胀,从库SQL线程卡住时relay log会不断堆积直到磁盘写满。
数据写入规范:
- 从库务必开启
read_only和super_read_only; - 任何需要直接修改从库数据的操作,必须有变更记录和审批流程;
- 生产环境的DDL要评估对复制的影响,大表DDL用
pt-osc或gh-ost在线变更,避免长时间阻塞SQL线程。
工具链沉淀:
- 部署一套监控,把
SHOW SLAVE STATUS的关键字段采集到Prometheus,配合Grafana展示趋势,而不只是看报警; - 把主从切换的SOP写成脚本和文档,每半年演练一次,练到不需要查文档就能操作。
我自己在维护主从环境的这段时间,最大的体会是:主从复制本身不难,难的是你永远不知道它什么时候会出幺蛾子,以及出问题后你有没有一套冷静的排查路径。希望这篇基于实际踩坑整理出来的内容,能帮你少走一些弯路。如果你在搭建过程中遇到什么奇怪的报错,欢迎在评论区把SHOW SLAVE STATUS的输出贴出来,我看到了会尽量帮你分析。
