最近后台收到不少朋友问 MySQL 高可用怎么做,尤其是那些业务刚起步、还没专门DBA的团队,一说“高可用”就想着上什么重量级方案,结果把自己折腾得够呛。我这些年帮别人搭过、也自己维护过不少 MySQL 集群,从最早的主从复制加 MHA,到后来用 Orchestrator,再到 MySQL 官方的 InnoDB Cluster,踩过的坑确实不少。这篇就把我实际用下来觉得最靠谱的几种方案、背后的原理,以及那些文档里不会写但关键时刻要命的细节,一次性梳理清楚。
读这篇文章之前,你需要对 MySQL 的基本操作有概念,知道 binlog 大概是什么。我会尽量把每个方案“为什么这么设计”讲明白,而不是只丢一堆配置命令。如果你正在为 MySQL 选型高可用方案,或者已经搭好主从但不知道后续怎么运维,这篇文章应该能帮你省下几个星期的摸索时间。
1. 高可用到底在解决什么问题
1.1 从一次半夜故障说起
先讲个真实经历。前几年我负责一个电商项目的数据库,当时架构是“一主一从”,主库扛读写,从库只做备份和偶尔的报表查询。自我感觉挺良好,直到有一回凌晨两点,主库所在物理机磁盘报警,紧接着整个实例直接宕机。我爬起来把 VIP 切到从库,再把从库提升为主库,整个过程花了十几分钟。对技术团队来说这个速度算是正常操作,但对线上业务来说,这十几分钟就是十几分钟的交易损失,老板早上看到数据报表的时候脸色很不好看。
那次之后我一直在想,所谓的“高可用”,本质上不是“不出故障”,而是“出故障之后业务感觉不到,或者只感觉到很短的中断”。MySQL 单机再稳,也扛不住硬件损坏、机房断电、误操作这些不可控因素。高可用方案要解决的核心问题就三个:数据尽量不丢、故障自动恢复、切换过程尽量快。
1.2 高可用的几个层级
很多人一上来就聊 MGR、PXC,其实高可用是分层级的,不同业务对可用性的要求完全不一样,方案选型必须从业务需求倒推。
第一层是“数据有备份”,这是最底线的要求。定时全备加 binlog 增量备份,服务器炸了至少能恢复到某个时间点。但问题在于恢复时间很长,一个 500G 的实例,物理备份恢复可能要一两个小时,业务根本等不起。这一层只能叫“备份”,谈不上高可用。
第二层是“主从自动切换”。至少一个主库一个从库,主库挂了之后从库自动顶上。这里的关键不仅是“切换”这个动作,还包括怎么让客户端知道连哪个库、切换时主从数据不一致怎么办。MHA、Orchestrator 这些工具干的就是这件事。
第三层是“多节点同时提供服务”。读写分离、负载均衡,多个节点组成一个集群,任何一个节点挂掉,流量自动打到其他节点。MySQL Group Replication、InnoDB Cluster、Percona XtraDB Cluster 都是这个思路。
大多数中小团队做到第二层已经能满足 99% 的场景,没必要为了追求第三层把架构搞得过于复杂。我见过不少团队一上来就上 PXC,结果节点之间网络抖动就得整个集群同步阻塞,反而把可用性搞得更差。
1.3 可用性指标怎么看
聊高可用,绕不开“几个 9”这个概念。99.9%(三个9)意味着一年允许宕机 8.76 小时,99.99%(四个9)是 52.56 分钟,而 99.999%(五个9)一年只能挂 5.26 分钟。
大多数互联网业务,做到三个 9 到四个 9 之间已经算不错了。就我经验来看,一主一从加自动切换,正常情况下能把可用性做到三个 9 到四个 9 之间;想要稳定达到四个 9 以上,光靠切换还不够,得考虑多机房部署、网络冗余、应用层重试机制这些因素。
还有个容易被忽略的指标叫 RTO(恢复时间目标)和 RPO(恢复点目标)。RTO 是“挂了之后多久能恢复服务”,RPO 是“最多丢多少数据”。一主一从半同步复制,RPO 可以做到接近零,RTO 看切换脚本效率,通常几十秒。如果业务能接受丢几秒数据,异步复制加自动切换就够了,性能和简单性都更好。这两个指标在方案选型时必须先定下来,不然配置参数的时候根本没有判断依据。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高可用基石:MySQL 主从复制
2.1 binlog 和复制链路是怎么工作的
不管哪种高可用方案,底层都离不开主从复制。要理解 MySQL 主从复制,重点抓住三个线程:主库上的 Binlog Dump 线程、从库上的 IO 线程和 SQL 线程。
主库上所有写操作都会记录到 binlog,Binlog Dump 线程负责把 binlog 发送给从库;从库的 IO 线程接收这些日志并写入本地的 relay log(中继日志);SQL 线程再读取 relay log,顺序执行里面的 SQL,把数据变更在从库上重放一遍。这个过程是异步的,从库的数据总是落后主库一点点,落后多少取决于网络延迟和从库自身的执行速度。
这里有个关键点:binlog 记录的格式。MySQL 有三种 binlog 格式,STATEMENT 记录 SQL 原文,ROW 记录每一行数据的变更前后值,MIXED 是混合模式。做主从复制,我强烈建议用 ROW 格式。STATEMENT 格式在碰到 UUID()、NOW() 这些非确定性函数时,从库重放出来的结果很可能和主库不一致;而 ROW 格式记录的是行变更结果,天然一致。
MySQL 8.0 默认就是 ROW,实际上很多人用 5.7 时也手动改成 ROW。唯一的代价是 binlog 体积会变大,尤其是有大批量 UPDATE 的时候,但这点磁盘成本相对于数据一致性来说完全值得。
2.2 主从复制的核心参数
既然要搭主从,几个核心参数得先搞清楚。先说主库上的 server_id,这个必须设置,而且每个节点的 server_id 不能重复,它是复制拓扑中区分节点的唯一标识。
再说 binlog 相关参数。log_bin 开启 binlog,binlog_format 设为 ROW。建议把 expire_logs_days(8.0 里是 binlog_expire_logs_seconds)设成一个合理的值,我一般设 7 到 14 天,太短了追不上数据,太长了占磁盘。
从库上有两个参数容易忽略。一个是 relay_log_purge,默认是开启的,就是说 relay log 执行完会自动删除。这个参数建议保持默认,除非你想做延迟复制或者调试,否则不必要的 relay log 会撑爆磁盘。另一个是 read_only,从库上建议打开,防止有人误连从库写数据,导致主从数据不一致。注意 read_only 对超级管理员不生效,如果要彻底一点,可以加上 super_read_only。
还有一个参数叫 gtid_mode,GTID 是全局事务标识符,每个事务都有唯一编号,主从复制时通过 GTID 自动定位同步位置,不用手动指定 binlog 文件名和偏移量。强烈建议开启 GTID,后面做切换、加从库都会方便很多。开启方式是:
ini复制gtid_mode = ON
enforce_gtid_consistency = ON
这两个参数必须同时开,而且要确保 binlog_format 是 ROW,否则启动会报错。
注意:GTID 一旦开启,不要随便关掉。集群里的节点都开了 GTID 之后再切回传统复制模式,很容易出复制中断,而且排查起来非常麻烦。我见过有人嫌 GTID "太新"不想用,坚持传统位点复制,结果每次加新从库都要手动去找 binlog 位置,费时费力还容易出错。
2.3 一步步搭建一主一从
理论讲完,直接实操。假设主库 IP 是 192.168.1.10,从库是 192.168.1.11,MySQL 版本都是 8.0。
第一步,主库创建复制专用账号:
sql复制CREATE USER 'repl'@'192.168.1.%' IDENTIFIED BY 'YourStrongPassword';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.1.%';
FLUSH PRIVILEGES;
这里用 REPLICATION SLAVE 权限就够了,别给太高权限,最小权限原则在数据库账号上同样适用。
第二步,从库配置并启动复制。如果你的从库是全新的,没有任何数据,直接执行:
sql复制CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='YourStrongPassword',
MASTER_AUTO_POSITION=1;
MASTER_AUTO_POSITION=1 意味着使用 GTID 自动定位,这是 MySQL 5.6.5 之后支持的特性,比传统手动指定 master_log_file 和 master_log_pos 方便得多。
第三步,启动复制并查看状态:
sql复制START SLAVE;
SHOW SLAVE STATUS\G
重点关注两个字段:Slave_IO_Running 和 Slave_SQL_Running 都应该是 Yes,同时 Seconds_Behind_Master 应该是一个很小的数字,理想情况下是 0。
前面说的是全新从库的情况。但生产环境里,从库往往是已经在跑的实例,需要先同步主库的存量数据。这时候需要先用 mysqldump 或 XtraBackup 做一次全量备份,恢复到从库后,再配复制。关键点是让从库的 GTID 和主库对齐,否则复制链路建立不起来。用 mysqldump 时加上 --set-gtid-purged=ON 参数,这样备份文件里会自动带上 GTID 信息。
bash复制mysqldump -uroot -p --single-transaction --set-gtid-purged=ON --all-databases > full_backup.sql
--single-transaction 对 InnoDB 表可以做到一致性快照备份,不影响线上写入。
2.4 半同步复制:性能和数据安全之间找平衡
普通异步复制最大的问题就是主库发生故障时,从库可能还没收到最新的 binlog,数据会丢。对很多业务来说,几秒钟的数据丢失不可接受,于是就有了半同步复制。
半同步复制的思路是:主库提交事务时,必须等待至少一个从库确认收到 binlog,才向客户端返回成功。这样主库挂了,至少有一个从库有最新的数据,RPO 就接近零了。
MySQL 的半同步复制是通过插件实现的,安装和启用相当简单。主库和从库都需要先安装插件:
sql复制INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
然后在主库上执行:
sql复制SET GLOBAL rpl_semi_sync_master_enabled = ON;
SET GLOBAL rpl_semi_sync_master_timeout = 1000;
rpl_semi_sync_master_timeout 是半同步的等待超时时间,单位毫秒。如果从库迟迟不确认,超过这个时间后主库自动退化为异步复制,保证业务写入不被阻塞。这个值别设太大,我一般设 1000 到 2000 毫秒。
从库上执行:
sql复制SET GLOBAL rpl_semi_sync_slave_enabled = ON;
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;
注意,改完半同步参数后,从库的 IO 线程要重启一次才能生效,容易漏掉这一步。
半同步复制能有效降低数据丢失的风险,但有个前提:从库写入 relay log 才算是“收到”,而不是执行完 SQL。也就是说半同步保证的是“binlog 没丢”,但不保证从库的数据和主库实时一致。这个细节很多人误判,把半同步当成数据实时同步来用,结果做读写分离时从库查不到刚写入的数据,这就是正常的复制延迟,别慌。
半同步在 MySQL 5.7 里增强过,从“一主一从确认”变成了可以指定 rpl_semi_sync_master_wait_for_slave_count,比如必须等两个从库确认。但要注意,等待的从库越多,事务提交延迟越高,对写入性能的影响越大。我一般建议一个从库确认就够了,能平衡性能和可靠性。
3. 自动故障转移:MHA 实战
3.1 MHA 的架构和角色
复制搭好了,但如果主库挂了,从库不会自动顶上,还需要人手动去执行提升操作。虽然比没有强,但依然算不上高可用。这时候就需要一个“监督者”,自动发现主库故障、选出一个数据最完整的从库、把它提升为新主库,然后让其他从库重新指向新的主库。MHA(Master High Availability Manager)就是干这个的经典工具。
MHA 由两部分组成:Manager 节点和 Node 节点。Manager 部署在一台独立的机器上(也可以和某个 MySQL 节点共用,但不推荐),负责监控主库状态、执行故障切换流程。Node 部署在所有 MySQL 服务器上,负责执行一些本地操作,比如保存中继日志、应用差异日志等。
MHA 的工作逻辑可以概括为:每个监控周期,Manager 通过 SSH 连接 MySQL 节点,执行 select 1 检查主库是否存活。连续失败达到设定次数,就判定主库宕机,进入切换流程。
MHA 最让我欣赏的一点是它的“数据补拉”机制。当主库挂了,MHA 会根据从库的 GTID 或 binlog 位点,找到数据最接近主库的从库,把其他从库缺失的 binlog 补过来,尽量保证所有节点数据一致后再提升主库。这意味着 RPO 能做到非常低,甚至为零。当然前提是主库没有彻底损坏,还能读到 binlog。
MHA 切换还有一个细节,它会自动生成一个脚本,叫 master_ip_failover。这个脚本的作用是把 VIP(虚拟 IP)从旧主库漂移到新主库,客户端通过 VIP 连接数据库,切换时感知不到后端的变化。这是 MHA 方案里最关键的一环,但也是最容易出问题的地方。脚本里涉及网卡操作、ARP 广播,很多环境里 arping 命令没装或者没有相应权限,VIP 漂移失败,就会导致客户端完全失联。
3.2 MHA 配置和部署要点
MHA 的安装不算复杂,但有几个点容易踩坑。MHA 是用 Perl 写的,依赖一些 Perl 模块。CentOS 上可以用 cpan 安装,或者直接下载 RPM 包。我最常用的是从 GitHub 上的 mha4mysql-manager 和 mha4mysql-node 仓库拉源码编译,或者用作者的 RPM 仓库,这里建议优先用包管理器装依赖,省得 Perl 模块折腾半天。
安装好之后,Manager 的配置文件长这样:
ini复制[server default]
manager_workdir=/var/log/masterha
manager_log=/var/log/masterha/masterha.log
user=root
ssh_user=root
repl_user=repl
repl_password=YourStrongPassword
ping_interval=3
secondary_check_script=masterha_conf_host --command=check --host=192.168.1.12 --port=3306
master_ip_failover_script=/etc/masterha/master_ip_failover
master_binlog_dir=/var/lib/mysql
[server1]
hostname=192.168.1.10
candidate_master=1
[server2]
hostname=192.168.1.11
candidate_master=1
ping_interval 是 Manager 检查主库的间隔,默认是 3 秒。别设太短,否则网络抖动就可能误判主库宕机,触发不必要的切换;也别太长,否则故障恢复时间会变长。
candidate_master=1 表示该节点是候选主库,故障切换时优先被选为主库。如果你的从库硬件配置比主库还好,或者想指定某个从库作为新主库,就加上这个配置。
配置好之后,先做一个健康检查:
bash复制masterha_check_ssh --conf=/etc/masterha/app1.cnf
masterha_check_repl --conf=/etc/masterha/app1.cnf
check_ssh 验证管理机和所有 MySQL 节点之间的 SSH 免密登录是否正常。check_repl 检查复制链路状态,MHA 会尝试在每台从库上启动一个临时管理节点,验证自己能否连上数据库、是否有权限做切换。
这两步都通过之后,启动 Manager:
bash复制nohup masterha_manager --conf=/etc/masterha/app1.cnf --remove_dead_master_conf &
重点说一下 --remove_dead_master_conf 这个参数:MHA 切换完成后,会自动把新主库从故障节点列表中移除,避免重复执行切换。这个参数很有用,但同时也意味着切换过一次之后,需要手动把新主库加回配置,否则下次故障 MHA 不会管它。
3.3 MHA 故障切换的真实推演
为了让你对 MHA 的切换流程有个直观感受,我把一次真实的主库宕机推演完整过程写出来。
假设主库 192.168.1.10 在凌晨 3 点突然宕机,MHA Manager 的 ping_interval 是 3 秒,连续 3 次 ping 失败后判定主库不可用,开始切换。
第一步,MHA 通过 SSH 登录所有从库,找到数据最新的节点。这里 MHA 会对比各个从库的 Exec_Master_Log_Pos 或 GTID 集合,选出最接近主库的那个节点作为候选主库。
第二步,MHA 尝试连接宕机的主库,如果还能 SSH 上去但 MySQL 起不来,它会尝试把主库残留的 binlog 拉到本地,分发给所有从库,尽量补齐缺失的数据。这一步做得好,RPO 可以降到接近零。但如果主库是硬件故障,完全无法 SSH,这步就只能跳过。
第三步,MHA 在所有从库上执行 stop slave,然后在候选主库上执行 reset slave all,让它脱离复制关系,变成独立主库,并执行 set global read_only=off,允许写入。
第四步,MHA 调整其他从库的复制指向,让它们全部指向新的主库,并启动复制。
最后一步,MHA 调用 master_ip_failover 脚本,把 VIP 从旧主库漂移到新主库。这个脚本是自定义的,成功执行后,客户端连接的 VIP 地址已经指向新主库,业务在短暂的连接中断后自动恢复。
整条链路走完,通常需要 10 到 20 秒。如果 master_ip_failover 脚本写得不好,这个时间会大大延长。所以我建议,MHA 部署完一定要做故障演练,不能只看文档说“没问题”就当没问题了。具体的演练清单,我放到后面的章节来说。
4. 新一代高可用方案:从 Orchestrator 到 InnoDB Cluster
4.1 Orchestrator 强在哪里
MHA 虽然经典,但它有个硬伤:依赖 SSH,架构偏重,而且没有可视化界面,运维同学排查问题不方便。这几年我用得更多的是 Orchestrator,它现在是 GitHub 上非常活跃的 MySQL 高可用管理工具,很多大厂都在用。
Orchestrator 的核心优势在于它是一个独立的元数据库,会持续采集整个复制拓扑的实时状态,能看到主从关系、延迟情况、GTID 集合等信息。它在故障切换时,通过 GTID 自动计算哪个从库数据最全,避免了 MHA 那种靠 SSH 登录执行命令的复杂机制。
Orchestrator 还有一个很实用的功能叫“拓扑可视化”,Web 界面里能直接看到主从节点之间的连线,哪个节点延迟高、哪个节点复制中断,一眼就能看出来。这种直观性在排查问题时节省了大量时间。
部署 Orchestrator 也很简单,它本质上是一个 Go 写的服务,下载二进制包解压就能跑。配置文件里比较重要的是:
json复制"MySQLTopologyUser": "orc_topology",
"MySQLTopologyPassword": "YourStrongPassword",
"MySQLReplicaUser": "orc_replica",
"MySQLReplicaPassword": "YourStrongPassword"
前一组账号用于读取复制拓扑信息,需要有 PROCESS, REPLICATION CLIENT 权限;后一组账号用于自动处理复制故障,需要更高级别权限。Orchestrator 会在发现主库故障时,自动执行恢复操作,把候选从库提升为新主库,并让其他从库指向新主库。
Orchestrator 和 MHA 选谁,我的经验是:如果你的架构比较复杂,节点数多,或者需要频繁查看拓扑状态,直接上 Orchestrator;如果只是简单一主一从,MHA 更轻量,配置也更快。但新项目的话,我倾向推荐 Orchestrator,它更适应现代 MySQL 的 GTID 复制体系,且社区活跃度明显更高。
4.2 读写分离场景下的 ProxySQL 介入
有了 Orchestrator,自动切换的问题解决了,但客户端怎么知道连接哪个节点?VIP 能解决一部分,但如果是读写分离,读流量要打到多个从库,就需要一个代理层做流量分发。目前最常用的是 ProxySQL。
ProxySQL 是一个非常灵活的开源 MySQL 代理,我实际用下来最大的感受是:它的配置管理方式很反直觉,但熟悉之后会发现非常强大。它的核心概念包括:前端端口、后端节点、路由规则、以及一个基于 SQLite 的持久化配置库。
读写分离的配置思路大致如下:后端定义两个 hostgroup,一个写组,一个读组。写组里放主库,读组里放从库。然后定义规则,凡是 SELECT 语句转发到读组,其他语句全部转发到写组。
sql复制INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (0, '192.168.1.10', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (1, '192.168.1.11', 3306);
INSERT INTO mysql_users (username, password, default_hostgroup) VALUES ('app_user', 'app_password', 0);
INSERT INTO mysql_query_rules (rule_id, match_pattern, destination_hostgroup) VALUES (1, '^SELECT', 1);
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL USERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL USERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;
这里 hostgroup_id 0 分配给写组,1 分配给读组。ProxySQL 会周期性地检查后端节点的状态,如果主库挂了,某个从库被提升为新主库,ProxySQL 能感知到并自动将写组指向新主库,前提是你做了相应的监控配置。
需要特别注意的是,match_pattern 用的是正则表达式。对事务内的 SELECT,规则要特殊处理。比如 SELECT ... FOR UPDATE 这种带锁的查询,不能走读库,必须在主库执行,否则会出现严重的数据一致性问题。我一般会在规则里把这类语句强制匹配到写组。
sql复制INSERT INTO mysql_query_rules (rule_id, match_pattern, destination_hostgroup) VALUES (2, '^SELECT.*FOR UPDATE', 0);
规则是按 rule_id 顺序匹配的,所以这条规则要放在通用 SELECT 规则之前,否则永远匹配不到。
4.3 Group Replication 与 InnoDB Cluster
如果说 MHA 和 Orchestrator 是在复制拓扑之上做“外挂式”高可用,那 MySQL Group Replication(MGR)和 InnoDB Cluster 就是“原生内置”的集群方案。
MGR 是 MySQL 官方的高可用方案,基于 Paxos 协议实现多个节点之间的数据复制。它的亮点是:所有节点都能写,写冲突由集群自动检测并解决。但实际上,MGR 最常见的部署方式还是单主模式,只有一个节点可写,其他节点只读,这样理解起来最简单,也最可靠。
InnoDB Cluster 是 MySQL 官方把 MGR、MySQL Shell、MySQL Router 整合在一起的一站式解决方案。用 MySQL Shell 的 dba 接口,可以方便地创建、配置、监控集群。MySQL Router 则是官方提供的轻量级代理,负责把客户端请求路由到正确的节点。
部署 InnoDB Cluster 最基本的流程:
bash复制mysqlsh --uri root@192.168.1.10
在 MySQL Shell 中执行:
javascript复制dba.configureInstance('root@192.168.1.10:3306');
dba.configureInstance('root@192.168.1.11:3306');
dba.configureInstance('root@192.168.1.12:3306');
var cluster = dba.createCluster('mycluster', {force: true});
cluster.addInstance('root@192.168.1.11:3306');
cluster.addInstance('root@192.168.1.12:3306');
之后 dba.getCluster().status() 就能看到集群的实时状态。
InnoDB Cluster 最大的优点是一切都由官方工具链封装好了,配置和管理都标准化,故障切换时 MySQL Router 能自动感知主节点的变化,把写请求切换到新主库。对团队里没有资深 DBA 的情况来说,这比手动配置 MHA 要省心得多。
但 InnoDB Cluster 也有不少限制:所有节点必须启用 GTID、binlog 必须保留足够长时间、不能有 MyISAM 表、DDL 和大的事务操作需要特别小心,因为它们可能会阻塞整个集群。如果你用的是 MySQL 8.0 以上版本,团队对新技术接受度高,InnoDB Cluster 是个很好的选择。如果还在 5.7 或者兼容老版本业务,MHA 或 Orchestrator 会更合适。
4.4 方案选型对照
我把常见的几种方案放在一起做个对比,方便你做决策:
| 方案 | 自动切换 | 数据一致性 | 运维复杂度 | 适用场景 |
|---|---|---|---|---|
| 主从复制 + 手动切换 | 否 | 异步/半同步可选 | 低 | 测试环境、能接受长时间手动处理的业务 |
| MHA | 是 | 近零丢失(依赖主库binlog可读) | 中高 | 经典场景,MySQL 5.7 时代主流 |
| Orchestrator | 是 | 依赖复制方式 | 中 | 复杂拓扑、需要可视化运维 |
| MGR/InnoDB Cluster | 是 | 组复制,强一致 | 中 | MySQL 8.0 新项目 |
| Percona XtraDB Cluster | 是 | 同步复制,强一致 | 高 | 对一致性要求极高、能接受性能损失的场景 |
选型的时候,别只看功能,更要看团队能不能维护。我见过一个团队上了 PXC,结果一次网络抖动导致整个集群挂掉,就是因为 PXC 的同步复制对网络要求太苛刻。对大多数业务来说,半同步复制加 MHA 或 Orchestrator 已经足够稳妥,没必要追求极致的强一致性。高可用的核心是“适合”而不是“最先进”。
5. 运维实战与故障排查实录
5.1 复制延迟排查
主从复制最让人头疼的问题之一就是复制延迟。Seconds_Behind_Master 一直在涨,从库的数据永远追不上主库,读写分离的时候业务就会查到旧数据。这时候别急着加从库,先定位延迟根源。
最常见的原因是大事务。比如主库执行了一条 UPDATE 更新几百万行,在 ROW 格式下,这条 UPDATE 会生成海量 binlog,从库要一条一条重放,延迟自然上去了。解决方案是把大事务拆成小批次执行,每批一万行,中间加 SLEEP,这样主库的 binlog 不会瞬间堆积,从库也能逐步跟上。
另一个常见原因是主库写入并发高,但从库是单线程重放(MySQL 5.7 之前)。5.7 以后的并行复制能解决一部分问题,前提是配置正确:
ini复制slave_parallel_workers = 4
slave_parallel_type = LOGICAL_CLOCK
LOGICAL_CLOCK 模式表示基于事务提交的先后顺序做并行重放,同一时刻提交的事务可以在从库并行执行,这比老的 DATABASE 模式并行度高得多,因为大多数业务的热点都集中在同一个库上,DATABASE 模式根本加不了速。
如果延迟还是降不下来,检查从库的磁盘 IO。从库重放 binlog 本质上是随机写,磁盘性能不行的话,延迟会越来越高。我当时遇到过一次延迟问题,排查到最后发现是云盘 IOPS 被打满了,直接把从库的磁盘升级到 SSD,延迟就降到了 0 附近。硬件瓶颈往往被忽略,但实际占比不小。
5.2 binlog 异常与数据一致性
复制最怕的是报错中断,Slave_SQL_Running 变成 No。常见错误有几种:一种是 SQL 线程执行出错,比如主库执行了 DROP DATABASE,从库上没有这个库;另一种是主从数据不一致,比如某条记录主库有从库没有,执行 UPDATE 时匹配不到行,不影响数据的话 SQL 线程可能会继续,但很多情况下会直接报错。
遇到这种情况,我的建议是:先别急着跳错,要分析根因。如果是误操作导致的错误,比如多删了数据,得从 binlog 里找到原始 SQL,在从库上手工补偿。如果只是某几条数据不一致,可以单独用工具比对和修复。
这里推荐两个工具:pt-table-checksum 用来比对主从数据一致性,查出哪些表的数据不一致;pt-table-sync 可以把不一致的数据修复回来,让从库和主库重新对齐。这两个工具是 Percona Toolkit 的一部分,做 MySQL 运维的必装工具。
但工具归工具,真正要避免的是主从数据不一致的产生。最有效的办法就是从架构层面禁止往从库写数据(read_only + super_read_only),同时用 ROW 格式的 binlog。我在这块有深刻的教训,曾经因为一个从库的 read_only 没有开启,业务上线时误连到了从库,写进去几条测试数据,结果后面一同步就冲突,排查了大半天才搞清楚。
提醒:改从库数据是运维的大忌。如果确实需要人工修改从库数据,一定先确认主库上对应的记录是什么状态,并且修改后立刻通过
pt-table-checksum做校验。别高估自己的记忆力和细心程度,自动化校验才是可靠的。
5.3 半同步切换踩坑
半同步复制用得好是利器,用得不好会给自己挖坑。我来说一个真实案例:有一次我们做故障切换演练,主库被强制 kill 掉,MHA 执行完切换后,新主库一直报错,业务写入频繁超时。查了半天才发现,问题出在半同步复制的配置文件上。
原因是这样:旧主库上启用了半同步插件,但新主库(之前的从库)上只启用了 rpl_semi_sync_slave_enabled,没有启用 rpl_semi_sync_master_enabled。主库提升之后,它不会自动成为半同步主库,这些参数没有跟着切过去,导致半同步复制配置一半生效一半失效,服务端等待从库确认,客户端等待服务端响应,形成了诡异的半死状态。
后来我在所有节点的配置文件里,把半同步的 master 和 slave 参数都同时开启了,两个插件都装好:
ini复制plugin_load_add = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_slave_enabled = 1
rpl_semi_sync_master_timeout = 1000
这样不管哪个节点被提升为主库,它都已经具备半同步主库的能力,不用在切换过程中再去修改配置。就这么一个小改动,后来的切换演练再也没出过这个问题。
另外,MySQL 8.0.22 之后新增了一个更优雅的半同步实现,叫“Replica半同步”,通过 rpl_semi_sync_source_enabled 参数控制,不再区分 master 和 slave,配置更简化。如果是 8.0.22 以上版本,建议直接用新参数。
5.4 高可用演练清单
高可用方案不是搭完就完事的,得有定期的演练。否则真正故障来了,你根本不知道脚本能不能跑通、VIP 能不能切过去、业务重连机制是否存在问题。我把自己的演练清单整理出来,每个季度至少做一次。
第一步,演练前备份。虽然演练的目标是验证故障切换,但不代表可以拿生产数据开玩笑。先做一次全量备份,防止演练过程中操作失误造成二次事故。
第二步,模拟主库故障。最直接的操作是 kill -9 主库 MySQL 进程,观察 Manager 能否在预期时间内探测到故障,并发起切换。如果用的是云数据库,不能直接 kill,可以在安全组里封掉数据库端口,模拟网络分区。
第三步,验证数据一致性。切换完成后,立即比较新旧主库的数据,重点检查最后几分钟内写入的记录,确认没有丢失。如果 RPO 不为零,要能明确知道丢了哪些数据、影响范围有多大。
第四步,验证业务连续性。检查应用连接的 VIP 是否成功漂移、应用日志里有没有报错、事务重连机制是否正常工作。这一步最容易发现隐藏问题,比如连接池的 connectionTimeout 设得太短,主库切换的十几秒内连接直接超时,应用反复报错,需要人工介入。
第五步,回切和清理。故障恢复后,把旧主库重新加入集群,让它作为新主库的从库同步数据。注意这里要防止旧主库的网络恢复之后,还残留着旧的 VIP 和半同步配置,导致新老主库同时对外提供服务,也就是经典的“脑裂”问题。解决脑裂的办法是在旧主库上配置 MySQL 的 read_only 和 super_read_only,并确保 MHA 或 Orchestrator 的防脑裂机制正常工作。
我的习惯是,把演练结果记录成表格:预期切换时间、实际切换时间、数据丢失量、业务中断时长,每次演练都做对比。如果这次演练比上次慢了,或者出现了新的报错,说明系统有退化或变更没同步,就需要立刻排查。高可用不是一个静态的东西,它是持续运营出来的。
结尾
做了这么多年 MySQL 运维,我最大的感受是:高可用方案永远没有银弹。MHA 成熟稳定但架构偏老,Orchestrator 灵活强大但需要学习成本,InnoDB Cluster 先进但限制也不少。与其纠结哪个方案最好,不如先想清楚自己的业务到底能容忍多少数据丢失、多长中断时间,然后根据这个底线去选方案、配参数、做演练,把每一环都扎扎实实落实。
最后想分享一个很多文档不会提的细节:高可用方案搭建完成后,一定要把日常运维的文档写清楚,包括切换流程、联系人、权限清单、常见问题处理方式。一旦真正故障发生,现场往往是紧张和混乱的,一份清晰的操作手册比任何“大神”都靠谱。部署高可用是为了让业务睡得着觉,但运维流程如果一团糟,反而会让人更焦虑。这个教训是我用无数次凌晨惊醒换来的,希望你能少走些弯路。
