MySQL 5.7高可用改造:Orchestrator + ProxySQL一主三从实战
先把结论写在前面这套方案我落地过不是从零开始搭的新系统而是给一套已经在线上跑了好几年的 MySQL 5.7 老集群做高可用改造。数据量在 TB 级库里有核心交易数据业务要求“平时别动它、坏了必须快速恢复”所以我对方案的第一个要求不是功能多炫而是改动面小、可回滚、故障时能明确交代“数据丢了多少、多久恢复”。最终定下来的组合就是标题里写的MySQL 5.7 一主三从Orchestrator 负责主从切换和拓扑管理ProxySQL 负责读写分离和流量调度。这篇文章把这套方案的选型理由、部署细节、切换链路、以及我在真实演练时踩过的几个坑完整记录下来适合正在给存量 MySQL 5.7 做高可用改造、或者在 MHA 和 Orchestrator 之间犹豫的兄弟们参考。1. 为什么这个场景仍然锁死 MySQL 5.7 一主三从1.1 存量业务的现实约束很多文章一上来就讲“新项目如何设计高可用”但现实里大量需求是老系统跑得好好的数据已经积累到 TB 级代码里又用了不少 5.7 时代的隐式转换和 SQL 写法直接升 8.0 的风险评估做了半年也没敢动。我这次面对的就是这个情况。先说几个硬约束应用代码是历史遗留项目几千条 SQL 没有全量测试覆盖升 8.0 后认证插件、隐式排序、字符集行为都有变化代价太大。单实例数据已经超过 1TB迁移窗口非常紧全量导出导入不现实。业务要求改造过程中不能长时间停机只能通过网络层零散切换来完成。所以“继续用 5.7把架构升级到具备高可用能力”是唯一合理路径。MySQL 5.7 虽然已经 EOL但它在生产环境里的稳定性和生态工具成熟度仍然够用重点是把 5.7 的复制、GTID、半同步这些老而可靠的能力用好。1.2 一主三从的四个节点角色划分主从数量不是拍脑袋决定的。两从在某些场景下也够但三从在这个项目里有非常具体的分工节点角色核心用途10.0.0.11主库承担全部写流量和部分读流量10.0.0.12从库-候选半同步复制作为切换时的首选新主10.0.0.13从库-报表专门给 BI 查询、统计任务用允许慢查询10.0.0.14从库-备份只做 xtrabackup 备份和数据校验备份不影响线上三个从库的用途完全不同候选从库要保持和主库数据实时一致报表从库可以接受秒级延迟备份从库则故意让它承担 pt-table-checksum 这类重操作。如果只搭两个从库切换后容错空间很窄报表和备份就得混在一起互相拖累。1.3 高可用目标先量化再谈方案高可用不是一句“挂了自动切换”就完事。我的可量化目标是RPO 趋近 0主库瞬间宕机时最多丢几秒内的事务不能接受丢 binlog。RTO 60 秒内从故障发现到业务恢复写流量控制在 1 分钟以内。故障类别覆盖MySQL 进程挂、宿主机宕机、网络分区三类都必须有明确应对。这个目标直接决定了软件选型半同步复制用于保证 RPOOrchestrator 用于快速切换ProxySQL 用于让应用无感知切换。三者缺一不可。2. 方案选型Orchestrator 和 ProxySQL 是怎么胜出的2.1 主从切换层MHA、keepalived、Orchestrator 三选一在定 Orchestrator 之前我列了三个候选方案优点缺点MHA老牌、文档多、切换脚本成熟没有拓扑管理界面故障后需要人工或额外脚本补齐从库对新主的复制关系维护方早已停止更新keepalived VIP简单粗暴VIP 漂移对网络抖动敏感容易脑裂MySQL 健康检测脚本要自己写可靠性取决于脚本质量Orchestrator自动发现拓扑、WEB UI、故障检测和切换联动机制完善搭建初期有学习成本依赖后端元数据库MHA 我实际用过功能没问题但在 TB 级一主三从上有个尴尬点切换完成后剩下两个从库的复制重建需要自己在脚本里写逻辑而 Orchestrator 对“新主选出后其余从库重新指向新主”这个过程是自动完成的。keepalived 则把脑裂问题留给了应用层我对 VIP 模式在数据库这种强一致场景下没太多信心。最终选 Orchestrator核心原因是它把“检测、决策、执行、补偿”整个链路都接住了。2.2 访问层ProxySQL 为什么比 MyCat、Atlas 更合适读写分离中间件我也对比过一轮MyCat定位偏分库分表规则配置重而且它自己有状态如果挂在 DB 前面反而成了新的单点。Atlas360 出的那个针对 MySQL 的读写分离和分表但项目停更多年5.7 的新特性支持不好。MaxScale功能不错但实际项目文档和排查经验少遇到问题能搜到的资料太少。ProxySQL最打动我的一点是它天然就是“面向 MySQL 的流量入口”连接池、读写分离、动态配置、SQL 路由全都有而且是真正无状态的前面挂 LVS 或者 F5 就可以横向扩展。后面我会详细讲 ProxySQL 的配置但选型阶段最关键的结论是读写分离中间件必须能配合 Orchestrator 做“故障态联动”而 ProxySQL 的 admin 接口和动态配置机制让这种联动很简单——切换时用脚本把主库身从 hostgroup 里换掉LOAD TO RUNTIME 就生效不需要重启也不用改应用连接串。2.3 这套组合对老库更友好的地方很多方案喜欢把“自动切换”和“应用感知”焊死在一起比如用 VIP 漂移让应用无感。但 TB 级老系统里应用连接数多、连接池分散在几百台机器上VIP 漂移之后长连接并不会自动重建反而因为 TCP 连接断不掉导致应用卡死。Orchestrator ProxySQL 的思路不一样应用连的是 ProxySQLProxySQL 连的是 MySQL。主库切换后ProxySQL 用 OFFLINE_SOFT 优雅排空旧连接、把新主加入主库组应用和 ProxySQL 之间的连接不受影响。这样改动面最小也是老系统最需要的“带病作业”能力。3. 一主三从落地参数、初始化、TB 级数据追平3.1 硬件规划和基础环境这套环境的服务器是物理机配置我给个参考CPU32 核起步如果并发写高建议 48 核以上。内存128G其中 InnoDB buffer pool 分配 80G 左右剩余给系统页缓存和连接线程。磁盘SSD 是必须的RAID10TB 级数据 binlog 增长磁盘空间至少预留数据的 1.5 倍。网络万兆内网因为 xtrabackup 全量同步和后续复制拉取 binlog 对带宽消耗很大。系统层面我统一用 CentOS 7.9MySQL 版本 5.7.44所有节点时间用 chrony 同步这一点容易被忽略——Orchestrator 判断复制延迟和故障时间依赖各节点时钟一致。3.2 参数配置直接抄我这版主库和候选从库的配置基本一致只有 server_id 不同。关键参数如下[mysqld] server_id 1 port 3306 datadir /data/mysql socket /data/mysql/mysql.sock pid-file /data/mysql/mysql.pid log_error /data/log/mysql/error.log # InnoDB innodb_buffer_pool_size 80G innodb_flush_log_at_trx_commit 1 innodb_flush_method O_DIRECT innodb_file_per_table 1 # 二进制日志与 GTID log_bin /data/mysql/binlog/binlog binlog_format ROW binlog_row_image MINIMAL sync_binlog 1 gtid_mode ON enforce_gtid_consistency ON log_slave_updates ON # 复制与半同步 skip_slave_start 1 slave_parallel_type LOGICAL_CLOCK slave_parallel_workers 8 slave_preserve_commit_order 1 rpl_semi_sync_master_enabled 1 rpl_semi_sync_master_timeout 1000 rpl_semi_sync_slave_enabled 1 # 其他 character_set_server utf8mb4 collation_server utf8mb4_bin max_connections 2000几个参数值得展开说binlog_row_image MINIMAL只记录被修改的列TB 级大表上能显著减少 binlog 体积和网络传输量。gtid_mode ONenforce_gtid_consistency ON5.7 的 GTID 已经比较成熟切换时不再依赖 File 和 Position 去定位只要从库收到过这个事务就能自动追平。强烈建议 5.7 集群从第一天就把 GTID 打开。slave_parallel_workers 8并行复制对延迟的改善非常明显。后面我会专门说大事务场景下的延迟坑。报表从库可以额外开long_query_time 2和slow_query_log ON用来抓报表同学的慢 SQL。备份从库则建议把innodb_buffer_pool_size调小一些因为备份主要走物理读。3.3 从库初始化Xtrabackup 全备 binlog 追平TB 级数据初始化第一个从库时绝对不能直接 mysqldump时间太长而且会造成主库压力。我用的是 Percona XtraBackup 2.4 版本可以和 5.7 匹配。基本流程如下在备份从库上直接执行备份xtrabackup --backup \ --target-dir/backup/full-bak \ --host10.0.0.11 \ --userbackup_user \ --passwordxxx \ --slave-info \ --parallel8 \ --throttle200--throttle200这个参数很关键它限制每秒 IO 操作数避免备份把主库磁盘 IO 打满。TB 级备份期间主库 IO 被打爆是初学者最容易犯的错。备份完成后先做恢复准备xtrabackup --prepare --target-dir/backup/full-bak然后把数据目录 Rsync 到从库再在从库上执行一次xtrabackup --copy-back。启动 MySQL 后通过备份目录里的 xtrabackup_binlog_info 文件可以看到备份点对应的 binlog 文件名和位置但在 GTID 模式下更简单直接用CHANGE MASTER TO MASTER_HOST10.0.0.11, MASTER_USERrepl, MASTER_PASSWORDxxx, MASTER_AUTO_POSITION1; START SLAVE; SHOW SLAVE STATUS\G重点看Seconds_Behind_Master是否从很大的值逐渐归零。如果从库数据量已经追到非常接近主库可以等Seconds_Behind_Master到 0 后再做个快速校验。3.4 追平过程中的实际体验TB 级数据第一次初始化从库整个过程用了大概 6 到 8 小时其中全备约 4 小时binlog 追平约 2 小时。期间主库的 IO 压力稳定在可控范围业务侧没有出现明显慢查询。这个时间窗口是可以接受的。另一个细节初始化过程中建议每隔 10 分钟看一次SHOW SLAVE STATUS里的Master_Log_File和Read_Master_Log_Pos确认拉取 binlog 的速度能跟上主库生成速度。如果一直拉不平大概率是网络带宽瓶颈而不是 MySQL 配置问题。4. Orchestrator 接入与自动切换核心配置与原理4.1 安装和基础配置我这里用的是 Orchestrator 3.2.6直接下载二进制包解压部署。需要注意两点第一Orchestrator 自身需要一个后端元数据库存储拓扑信息和故障检测记录。这个库规模很小可以单独建一个 MySQL 实例也可以复用集群里的某个节点但建议独立创建不要和业务库混在一起。第二Orchestrator 需要一个账号来探测所有 MySQL 实例权限如下GRANT SUPER, PROCESS, REPLICATION SLAVE, RELOAD, SELECT ON *.* TO orc10.0.0.% IDENTIFIED BY xxx;如果希望 Orchestrator 通过SHOW SLAVE HOSTS自动发现拓扑还需要REPLICATION CLIENT权限。这里有个容易漏的点需要给 Orchestrator 后端元数据库也单独建一个账号权限只需要 SELECT、INSERT、UPDATE、DELETE、CREATE、ALTER。核心配置文件 orchestrator.conf.json 里我重点改了这些{ Debug: false, ListenAddress: :3000, MySQLTopologyUser: orc, MySQLTopologyPassword: xxx, BackendDB: { Host: 10.0.0.10, Port: 3306, User: orc_backend, Password: xxx, Database: orchestrator }, DefaultInstancePort: 3306, DiscoverByShowSlaveHosts: true, InstancePollSeconds: 5, DetectClusterAliasQuery: SELECT CONCAT(cluster-, port), DetachLostSlavesAfterSeconds: 30, RecoveryPeriodBlockSeconds: 60, FailMasterPromotionOnExpiredGtid: true, PostFailoverProcesses: [ /opt/scripts/update_proxysql_after_failover.sh ] }几个参数解释InstancePollSeconds 5每 5 秒探测一次实例。这个值和 RTO 直接相关如果你把 RTO 目标定在 30 秒内探测间隔就不能大于 10 秒。DetachLostSlavesAfterSeconds 30故障发生 30 秒后仍然找不到 master 的从库会被标记为 detached避免它们在错误的状态下继续复制。FailMasterPromotionOnExpiredGtid true如果选中的新主存在缺失的 GTID 事务则放弃提升避免因为数据空洞造成新的不一致。这个必须开尤其在有半同步和异步混合的场景下。4.2 手动切换演练graceful-master-takeoverOrchestrator 装好后会自动拉取拓扑登录 WEB UI 能看到 10.0.0.11 为主、三个从库挂在下面。做计划内切换时用命令行orchestrator-client -c graceful-master-takeover -alias cluster-3306Orchestrator 会自动完成这些事把主库设为只读。等待所有从库追上最新 GTID。从候选从库中选出延迟最低、数据最全的那个提升为新主。其余从库自动 CHANGE MASTER 指向新主使用 MASTER_AUTO_POSITION1。触发 PostFailoverProcesses 里的脚本。整个过程在数量级上比 MHA 的脚本可控性高很多MHA 做完切换后还得自己手动调整其他从库的复制链路。4.3 自动切换链路和误判规避自动切换的链路是探测超时 → 进入故障检测流程 → 判定 master 是否真的不可用 → 触发恢复 → 选择新主 → 重搭复制 → 调用外部脚本。这里最容易出问题的是误判。网络抖动导致 Orchestrator 连着几次 ping 不到主库就会触发切换而这个切换在业务高峰期代价很大。我做了三个防御探测网段和管理网段分离Orchestrator 通过独立管理网访问数据库节点避免业务高峰网络拥塞影响探测。RecoveryPeriodBlockSeconds 60同一集群故障恢复后 60 秒内不会再次自动切换防止“切换刚落定又被误判”的二次抖动。维护窗口内手动设置 Orchestrator 维护模式告诉它“这是计划内操作不要干预”。维护模式用下面这个命令orchestrator-client -c begin-maintenance -i 10.0.0.11:3306 --reasonplanning switchover操作结束后再 end-maintenance 释放。这个机制在做索引重建、大事务提交前尤其有用避免 Orchestrator 因为主库暂时“响应慢”误以为是故障。4.4 切换后其他从库的一致性处理还有一个容易被忽略的点切换完成后原来的主库如果只是宕机恢复它重新上线时会尝试连接新的主库继续复制。如果新主上多了很多它没有的事务Orchestrator 会基于 GTID 自动尽量追平但它会明确标记这个节点为“lagg”状态。对于原来那台主库我更建议直接把它降级为普通从库让它从备份节点拿一次全量重搭复制链路不要尝试让它保留旧数据继续跑。5. ProxySQL 读写分离与故障态切换联动5.1 ProxySQL 的三层配置模型ProxySQL 的配置分三层runtime当前生效、memory内存查询、disk持久化任何修改都要显式执行 LOAD 和 SAVE 才会依次生效。第一次配置的人容易漏掉LOAD ... TO RUNTIME导致改了不生效然后一顿排查其实问题就在这。装好 ProxySQL 后用 admin 接口连入mysql -uadmin -padmin -h127.0.0.1 -P6032 --promptProxySQL 5.2 后端实例和 hostgroup 规划ProxySQL 的逻辑核心是 hostgroup。我规划了 0 组为主库组1 组为从库组。插入后端节点INSERT INTO mysql_servers (hostgroup_id, hostname, port, status, weight, max_connections) VALUES (0, 10.0.0.11, 3306, ONLINE, 100, 2000), (1, 10.0.0.12, 3306, ONLINE, 100, 1000), (1, 10.0.0.13, 3306, ONLINE, 100, 1000), (1, 10.0.0.14, 3306, ONLINE, 100, 800);注意我暂时没把主库加到从库组。实际中是否让主库承担读取决于读多还是写多。我这个场景写并发高主库的 IO 已经比较饱和所以从库组只放三个从库。5.3 读写分离规则别把 FOR UPDATE 路由到从库查询路由规则是 ProxySQL 最核心的部分。我的规则大致如下INSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply) VALUES -- (1, 1, ^SELECT.*FOR UPDATE, 0, 1) 这样写不完善看下面完整版 ;实际推荐用多条件组合先把写语句和锁定读匹配出来INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply) VALUES (1, 1, ^SELECT .* FOR UPDATE, 0, 1), (2, 1, ^SELECT , 1, 1), (3, 1, ^SHOW , 1, 1), (4, 1, ^UPDATE |^INSERT |^DELETE , 0, 1);这里有几个坑SELECT ... FOR UPDATE必须在纯 SELECT 规则之前匹配否则会被当成普通读分摊到从库。这个是最经典的线上事故。ProxySQL 的规则匹配基于正则^SELECT不会匹配(SELECT或带前缀的语句联表子查询里可能漏。建议对慢查询日志和 ProxySQL stats 里的语句做一轮验证确认没有查询被错误路由。如果应用有事务内“读后写”的场景必须设置规则让事务里的所有语句保持在同一主机上执行。也就是在 mysql_users 表里为对应用户开启UPDATE mysql_users SET transaction_persistent1 WHERE usernameapp_user;这个参数很关键事务内第一条语句命中了哪个 hostgroup后续整个事务都走同一个节点否则会出现“在从库读到了旧数据回主库去更新”的错乱。5.4 创建应用用户ProxySQL 对外暴露的用户名和密码可以跟 MySQL 不同但转发到后端时必须映射真实账号。INSERT INTO mysql_users (username, password, default_hostgroup, transaction_persistent, max_connections) VALUES (app_user, app_pass, 0, 1, 500);注意default_hostgroup0这样没有规则匹配的语句默认走主库宁慢勿错。5.5 和 Orchestrator 的切换联动脚本现在还差最后一块Orchestrator 切换完成后如何让 ProxySQL 知道“主库变了”。我的方案是在 orchestrador 的PostFailoverProcesses里挂一个脚本切换时自动更新 ProxySQL 的 hostgroup 配置。脚本核心逻辑如下#!/bin/bash NEW_MASTER_HOST$1 NEW_MASTER_PORT$2 OLD_MASTER_HOST$3 OLD_MASTER_PORT$4 PROXYSQL_ADMINmysql -uadmin -padmin -h127.0.0.1 -P6032 -Nse $PROXYSQL_ADMIN REPLACE INTO mysql_servers (hostgroup_id, hostname, port, status, weight, max_connections) VALUES (0, ${NEW_MASTER_HOST}, ${NEW_MASTER_PORT}, ONLINE, 100, 2000); $PROXYSQL_ADMIN UPDATE mysql_servers SET statusOFFLINE_SOFT WHERE hostgroup_id0 AND hostname${OLD_MASTER_HOST} AND port${OLD_MASTER_PORT}; $PROXYSQL_ADMIN LOAD MYSQL SERVERS TO RUNTIME; $PROXYSQL_ADMIN SAVE MYSQL SERVERS TO DISK;这个脚本的巧妙之处在于OFFLINE_SOFT状态ProxySQL 会立刻停止向旧主库发送新请求但已经建立的连接会继续保留直到事务完成或连接自然断开。相比 OFFLINE_HARD直接杀连接应用无感程度高得多。为了安全脚本里我加了一步校验先确认新主库真的能连接再执行上面的操作参数用 Orchestrator 传入的前两个位置。实际跑下来从 Orchestrator 触发到 ProxySQL 完成切换耗时不超过 3 秒。6. 切换演练与长期维护的几个提醒6.1 演练一计划内割接所有配置上线后第一件事不是祈祷它不坏而是主动做一次计划内切换验证整条链路。我的操作顺序提前通知业务方进入维护窗口。给 Orchestrator 设置 begin-maintenance计划内操作手动执行。执行 graceful-master-takeover。观察 ProxySQL 是否自动把新主加入主库组、旧主是否被置为 OFFLINE_SOFT。执行一轮读写检查确认 SELECT 到了从库、INSERT/UPDATE 到了新主。二次切换切回原主库确认整个链路是可逆的。我实测的 RTO 从发起到写流量恢复大约 45 秒其中 20 秒花在 Orchestrator 等从库追上 GTID、10 秒花在 ProxySQL 动态更新 hostgroup其余是探测时间。6.2 演练二模拟主库进程崩溃计划内验证完还要模拟真正的故障。我把主库实例直接kill -9观察 Orchestrator 的表现第 5 秒探测失败。第 15 秒触发故障检测发现主库不可达。第 25 秒完成新主提升和数据补偿检查。第 35 秒ProxySQL 联动脚本执行完毕写流量切到新主。应用报错窗口约 20 秒内少量写请求失败需要应用层重试。这个结果基本满足当初定义的 RTO 目标。但演练也暴露了一个问题由于主库配置是半同步rpl_semi_sync_master_timeout1000在极端情况下半同步会退化成异步极端宕机可能丢掉最后 1 秒内的数据。所以我又额外加了 relay log 的保留策略确保候选从库尽量多保留中继日志减少数据丢失概率。6.3 TB 级场景下的几个长期维护提醒大事务延迟。TB 级表上跑一个UPDATE ... WHERE扫全表的事务会产生巨大的 binlog从库就算开了并行复制也容易落后。我踩过一次凌晨一个报表任务 DELETE 老数据扫了 30 分钟三个从库 Seconds_Behind_Master 全部冲到几千秒。后来规定所有大变更必须用 pt-osc 或 gh-ost且禁止生产从库直接执行大范围 DML。复制延迟不能只看 Seconds_Behind_Master。这个指标在某些情况下会失真尤其从库 IO 线程堆积时。我的做法是额外建一张心跳表主库每秒更新从库每 5 秒查一次用两边的当前时间差估算真实延迟。比自带指标可靠得多。定期数据校验。TB 级集群跑了一段时间后最怕静默的数据不一致。我每周在备份从库上跑一次 pt-table-checksumpt-table-checksum \ h10.0.0.14,uchecksum_user,pxxx \ --databasescore_db \ --replicatept.checksums \ --chunk-size1000 \ --max-lag3 \ --throttle50然后对比三个从库的 checksum 是否有差异。如果发现不一致再用 pt-table-sync 修复。注意这几个命令本身会占用资源必须放在备份从库上跑不要在候选从库上跑。6.4 应用层配合高可用改造的几个点这套架构搭完了如果应用层不配合切换时照样会出问题。我在改造时给开发团队提了几条要求其实也适合所有高可用场景连接池要配置最大生命周期建议 10 到 30 分钟确保切换后不再依赖旧连接。写请求失败要有重试机制但必须做幂等控制否则一次超时重试可能导致重复下单。“写完立刻读”的业务要么走主库要么接受短时间不一致。建议在 SQL 前缀加注释让 ProxySQL 强制走主库比如/*master*/ SELECT ...规则里匹配^/\*master\*/路由到 0 组。应用侧不要尝试连具体 MySQL 节点 IP统一走 ProxySQL 对外地址这样高可用对应用才真正透明。6.5 最终经验总结这套 Orchestrator ProxySQL 方案上线后运行时间已经不短整体非常稳。真正遇到过的生产问题反而是配置层面的小坑比如 ProxySQL 规则匹配漏掉了FOR UPDATE、Orchestrator 维护模式忘关导致后续切换被阻断、备份从库磁盘空间不足导致 xtrabackup 失败。这些坑单看都不难但组合在一起会在故障时放大处理难度。最后分享一个实操技巧切换脚本里除了更新 hostgroup我还会往 ProxySQL 的 mysql_servers 里写入当前主库的注释信息并同步写一份到/tmp/failover_history.log记录每次切换的时间、发起原因、新旧主节点。故障复盘和追溯时这些日志比任何监控截图都值钱。如果你正准备给存量 MySQL 5.7 上高可用我的建议是不要急着看新特性先把“数据不丢、切换快、能回溯”这三件事做扎实。这套方案可能不是最炫的但对于 TB 级老系统来说它是真正能让你在凌晨 3 点接到告警电话时还能保持冷静的方案。