1. 集群架构设计与选型思路1.1 为什么是这三件套先聊个现实情况很多团队一提到高可用第一反应就是上云、用托管数据库但在过去很长一段时间里自建机房和物理服务器的场景才是主流。哪怕放到今天私有化交付、信创改造、内网隔离环境里你依然绕不开“自己搭一套数据库高可用”的需求。而 MySQLATLASMMM 这套组合就是那个年代沉淀下来、被无数生产环境验证过的经典方案。先说三者的分工简单比喻一下MySQL 是真正干活的人负责存储数据和处理事务MMMMaster-Master Replicator是监工盯着两个 MySQL 是否健康谁挂了就把流量切到另一台ATLAS 则是前台接待所有应用请求先到它这里由它决定把写操作分给哪个后端、把读操作分给哪些后端实现读写分离。三者各司其职配合起来就是一套完整的读写分离主主切换的高可用架构。为什么选它而不是别的方案在那个年代LVSKeepalived 只能解决 IP 漂移问题管不了 MySQL 的复制状态MySQL 官方提供的主从复制本身也不具备故障检测和自动切换能力而商业方案如 Oracle RAC 成本太高普通团队根本承受不起。MMM 免费开源、部署简单、对 DBA 友好ATLAS 则是基于 MySQL Proxy 二次开发的中间件支持读写分离和负载均衡。组合起来用最少的成本解决“数据冗余、自动切换、读写分离”三个核心问题。1.2 整体拓扑与数据流向这套集群的典型拓扑如下两台 MySQL 节点互为主主复制MMM 也支持主从模式但主主是经典配置MMM 在三台机器上部署两台分别跑在各 MySQL 节点上一台作为 monitor 监控节点ATLAS 一台或多台部署在应用层和数据库层之间对外提供一个写 VIP虚拟IP和一个或多个读 VIP应用只认 IP不感知后端拓扑变化数据流向是这样的应用发起请求 → ATLAS 接住 → 判断是读还是写 → 写请求发给 MMM 指定的 writer 节点读请求根据负载策略发给各读后端 → MySQL 之间通过主主复制保持数据一致 → 一旦某台 MySQL 宕机MMM monitor 感知后将写 VIP 漂移到存活节点同时通知 ATLAS 将后端标记为 offline。注意一个关键点ATLAS 可以同时接入两个 MySQL 节点但真正的写流量永远只到当前持有 writer 角色的节点。MMM 通过管理命令切换角色而不是 ATLAS 来做这个判断。所以在设计上ATLAS 和 MMM 的“沟通”不是实时的而是依靠 IP 和端口状态来完成。这也是为什么后面我们在做故障切换时要确保 MMM 的切换动作和 ATLAS 的后端状态更新是顺序执行、避免竞态的。2. 环境规划与主主复制搭建2.1 节点划分与参数设计在做任何操作之前先把节点规划搞清楚。我建议的部署形态是节点角色主机地址组件数据库节点110.0.0.11MySQL、MMM agent数据库节点210.0.0.12MySQL、MMM agent监控节点10.0.0.13MMM monitor代理节点10.0.0.14ATLAS那个年代服务器资源普遍紧张所以 ATLAS 可以和 MMM monitor 放在一台或者 ATLAS 也部署到两个数据库节点上做双活的但我不建议这么干。ATLAS 是纯转发本身无状态但如果你把它和 MySQL 混部一旦 MySQL 出问题很可能影响 ATLAS 进程增加排查难度。生产环境哪怕再省资源也要保证 ATLAS 独立部署。MySQL 版本我以 5.7 为例再老一点的 5.6 也适用流程一致。主主复制的核心参数有两个server-id必须不同auto_increment_offset和auto_increment_increment必须合理配置前者控制自增起始值后者控制步长。例如两台节点node1server-id 1auto_increment_offset 1auto_increment_increment 2node2server-id 2auto_increment_offset 2auto_increment_increment 2目的是避免主主双向复制时自增主键冲突。原理很简单步长为 2node1 只生成奇数主键node2 只生成偶数主键两边各写各的永远不会撞车。如果以后要扩展到三节点或更多步长设置为节点总数即可这里有个现场经验改动auto_increment_increment前一定要确认当前业务表的最大自增 ID 落在哪个范围内否则切换后可能出现主键重复触发写入失败。2.2 主主复制配置要点在两台节点上都开启 binlog且配置必须保证复制链路是双向的node1 的my.cnf关键项[mysqld] server-id 1 log-bin mysql-bin binlog_format ROW expire_logs_days 7 auto_increment_offset 1 auto_increment_increment 2node2 的my.cnf除了server-id和 offset 不同其余一致。这里把binlog_format设为 ROW 是经验之谈早期的 statement 格式在主主复制中容易出现数据不一致尤其是使用 UUID()、NOW() 这类非确定性函数时row 格式才是最稳妥的。创建复制专用账号两个节点都需要GRANT REPLICATION SLAVE ON *.* TO repl10.0.0.% IDENTIFIED BY StrongPass123; FLUSH PRIVILEGES;然后分别在 node1 和 node2 上指向对方-- 在 node1 上执行 CHANGE MASTER TO MASTER_HOST 10.0.0.12, MASTER_USER repl, MASTER_PASSWORD StrongPass123, MASTER_LOG_FILE mysql-bin.000001, MASTER_LOG_POS 154; START SLAVE; -- 在 node2 上执行 CHANGE MASTER TO MASTER_HOST 10.0.0.11, MASTER_USER repl, MASTER_PASSWORD StrongPass123, MASTER_LOG_FILE mysql-bin.000001, MASTER_LOG_POS 154; START SLAVE;MASTER_LOG_FILE和MASTER_LOG_POS分别从各自SHOW MASTER STATUS中获取千万别死搬硬套固定值。特别提醒在生产环境初始化主主关系时我习惯先只搭建单向复制等数据完全追平、确认两边一致后再开启反向复制这样出问题更容易定位。2.3 复制状态自检与延迟排查搭建完成后用SHOW SLAVE STATUS\G检查重点看这几项Slave_IO_Running: YesSlave_SQL_Running: YesSeconds_Behind_Master: 0如果 IO 线程报错多半是网络不通、账号权限不足或者MASTER_LOG_FILE位置不对如果 SQL 线程报错则是已经执行过的 SQL 在主库上产生了冲突常见原因是重复执行了写入语句或两边都执行了CREATE TABLE。处理方法找到报错日志位置手动补齐缺失的数据后执行SET GLOBAL SQL_SLAVE_SKIP_COUNTER 1; START SLAVE;跳过。但注意这种“跳过”一定要在确认不影响数据一致性的前提下使用否则后患无穷。主主复制还有个隐患复制延迟。哪怕Seconds_Behind_Master为零也不能掉以轻心因为 row 格式下大事务的同步时间可能会很长。我这里有一种比较稳妥的监测方式在业务表里建一张心跳表定期插入时间戳通过对比主备的时间差来评估真实的延迟。虽然笨但很可靠。3. MMM 高可用监控与故障切换3.1 MMM 的核心机制MMM 分为两个核心组件mmm2_monitor部署在监控节点负责收集各节点状态、做决策mmm2_agent部署在数据库节点负责执行本地检查和状态上报。两者通过 TCP 通信monitor 线程一般每 2-5 秒检查一次后端状态。检查方式很直接agent 定时执行SHOW SLAVE STATUS、SHOW MASTER STATUS并更新本地状态文件/var/lib/mysql-mmm/monitor 则读取这些文件来判断节点存活、复制是否正常。如果某个节点连续 N 次无响应monitor 将其标记为online之外的catchup或failed状态然后执行 VIP 切换。MMM 切换的核心能力是管理三类角色writer可写节点、reader只读节点、backup备份节点。在主主架构中默认设置一台为 writer另一台为 reader两台同时对外提供读能力只有 writer 能接受写流量。当 writer 故障时monitor 会把写 VIP 漂移到另一台并通知原 writer 降级。3.2 配置文件与角色策略MMM 的配置文件在/etc/mmm/mmm2.conf核心内容如下[monitor] interface 0.0.0.0 monitor_interval 2s ping_interval 5s auto_set_online 60 [db1] hostname 10.0.0.11 ip 10.0.0.11 port 3306 pid /var/run/mysqld/mysqld.pid binlog mysql-bin master db2 role writer [db2] hostname 10.0.0.12 ip 10.0.0.12 port 3306 pid /var/run/mysqld/mysqld.pid binlog mysql-bin master db1 role reader [reader_ip] ip 10.0.0.100 port 3306 backends db1, db2 [writer_ip] ip 10.0.0.200 port 3306 backends db1, db2这里role是初始角色实际运行中会随故障切换而变动。读 VIPreader_ip会指向当前所有存活节点写 VIPwriter_ip永远指向当前 writer。配置完成后执行mmm_control start mmm_control show看到所有节点状态为onlineVIP 处于正常绑定状态才算真正就绪。这里有个雷区MMM 的auto_set_online 60表示节点故障后自动恢复时间为 60 秒。在数据复制延迟很严重的情况下自动恢复过快可能导致刚恢复的节点带着旧数据抢回 VIP。生产上有过切回后数据丢失的事故所以我建议把这个值调大比如 300 秒等数据追平确认无误后再让 MMM 自动把节点拉回。3.3 手动切换与故障演练MMM 支持手动切换。你想要在维护前主动把流量切走执行mmm_control move_roles rolewriter to db2 mmm_control move_roles rolereader to db1这条命令在各节点脚本中执行会把对端节点上的错位 VIP 移除再在目标节点上绑定。我实测中的感受是整个切换过程大概 5-10 秒期间写 VIP 会出现短暂中断取决于网络的 ARP 缓存清理时间。应用侧如果配置了连接池重连机制这个时间窗口基本无感如果客户端没有重连逻辑连接直接挂掉需要应用层自行重试。所以在应用侧做好重连策略比任何数据库层面的高可用都重要。来做一次故障演练模拟我们直接 kill 掉 db1 的 MySQL 进程。systemctl stop mysqld mmm_control show可以看到 db1 状态逐渐变为failed随后mmm_control move_roles被自动执行写 VIP 漂移到 db2同时 monitor 通过 SSH 在 db2 上执行ip addr add绑定 VIP。整个时序约 10 秒左右。重点说下“脑裂”问题如果 db1 和 db2 之间的网络被切断但两边 MySQL 都活着MMM monitor 会不会让两台同时成为 writer答案是 MMM 有 quorum 机制。monitor 只有在能连接到多数节点时才执行切换网络上一般只有一台 monitor所以它的决策是单点的。为避免 monitor 单点故障影响到全局生产环境可以用 keepalived 再给 monitor 节点做一个 HA但要注意避免两个 monitor 同时决策引发冲突。还有一点网络分区时如果 db1 仍持有写 VIP应用流量继续写入 db1而 MMM 无法确认 db2 的存活此时不建议自动切换宁可让服务短暂不可用也不要双写风险。这一点操作手册上不会写是实战经验。4. ATLAS 读写分离与连接管理4.1 ATLAS 与官方 Proxy 的差异ATLAS 是 360 在 MySQL Proxy 基础上改造的中间件相比原生版本最直观的改进是性能更稳、功能更贴合业务支持后端节点动态上下线、平滑重启、读写分离比例调整这些都是生产环境非常需要的特性。举个例子官方 MySQL Proxy 的 Lua 脚本写起来费劲不说跑在高并发下内存还容易爆。ATLAS 直接用 C 实现核心转发逻辑性能表现要好很多。当年我用普通一台 4C8G 的虚拟机跑 ATLAS能扛住每秒几千次查询的转发CPU 不过在 30% 上下这个表现相当能打。需要注意的是ATLAS 只做转发不做 SQL 解析和结果集的合并。它的分表功能也只是基于分表键做路由不涉及跨库 JOIN 和分布式事务所以业务设计上要把复杂的联表查询留在应用层做不要指望 ATLAS 帮你解决所有问题。4.2 配置文件与启动细节ATLAS 部署极简解压后就是一个目录。核心配置文件是/usr/local/mysql-proxy/conf/mysql-proxy.ini[mysql-proxy] admin-username proxyadmin admin-password proxyadmin proxy-backend-addresses 10.0.0.11:3306, 10.0.0.12:3306 proxy-read-only-backend-addresses 10.0.0.12:3306 pwds webuser:加密密码串 daemon true keepalive true event-threads 8 proxy-address 10.0.0.14:3307 admin-address 10.0.0.14:2345 charset utf8解读几个关键参数proxy-backend-addresses写后端地址ATLAS 会把写操作发给这里列出的第一个可用节点proxy-read-only-backend-addresses只读后端所有读请求按权重分发到这些节点pwds需要预先用 ATLAS 自带的encrypt命令生成加密密码串不能直接用明文event-threads工作线程数建议设为 CPU 核心数的 2 倍太少了高并发下会成为瓶颈keepalive进程守护子进程崩了会自动拉起配置完成后启动/usr/local/mysql-proxy/bin/mysql-proxy -c /usr/local/mysql-proxy/conf/mysql-proxy.iniATLAS 管理接口是个亮眼的设计你不需要重启就能实时查看和调整后端状态mysql -uproxyadmin -pproxyadmin -h10.0.0.14 -P2345进去后执行SELECT * FROM backends;可以看到每个后端的角色、状态、权重。要临时摘除某个节点做维护不需要停服务执行SET OFFLINE 1;维护完再让它重新上线SET ONLINE 1;这条命令对业务完全透明连接不会被断开新请求自动路由到其他节点。我在实际运维中非常依赖这个特性比如做 MySQL 版本升级的时候先把节点在 ATLAS 上设为 offline等升级验证完再拉回来整个过程应用无感知这在无法接受停机的场景里太关键了。4.3 应用侧接入与连接池调优应用接入 ATLAS使用 MySQL 驱动直接把连接地址指向 ATLAS 即可。例如 Java 的 JDBC URLjdbc:mysql://10.0.0.14:3307/webdb?characterEncodingutf8connectTimeout2000socketTimeout5000autoReconnecttrue在引入读写分离后要特别注意“写完立即读”的场景。比如用户注册后跳转到登录页刚写入的账号信息可能还没来得及同步到读节点而读请求刚好被分发到了延迟的节点上就会遇到查不到数据的尴尬。解决方案通常有两种一是让这类关键读也走写库比如在 SQL 前加注释强制走主库ATLAS 支持通过注释语法强制主库二是通过调整 MMM 的复制延迟监控确保读节点不会接收明显延迟的数据。更实用的做法是应用层做补偿比如注册后强制等待几百毫秒或重试一次。连接池参数方面因为中间层会增加一次网络跳转建议把connectTimeout保持在 2-3 秒socketTimeout不能设太短否则数据库慢查询时客户端会误判为连接异常。还有autoReconnecttrue是针对故障切换的保命配置MMM 切换期间旧连接会被打断没有这个参数应用报错会很频繁。5. 故障案例复盘与排查手册5.1 一次真实主库宕机全程回放有一次凌晨大促db1 所在的物理机内存报警系统自动重启连带 MySQL 一起被杀掉。整个故障链路是这样的MMM monitor 在 2 秒内发现 db1 心跳丢失启动切换流程先把写 VIP 漂移到 db2同时发指令给 db1 的 agent 把本地 VIP 摘掉。此时 ATLAS 仍然把 db1 当作可用后端但由于 db1 的写 VIP 已经不在本机连接实际上已经失败ATLAS 会自动把失败请求转发给其他后端。关键点ATLAS 对后端是否可用的探测是惰性的它不会主动去 ping 数据库而是在请求失败时才标记超时。所以如果短时间内大量请求被路由到已宕机的 db1会造成部分请求直接报连接超时。这种问题我在架构上补了两个保险一是把 ATLAS 的后端探测间隔调小让它尽快感知后端状态二是在连接层做容错让应用在拿不到连接时重试多次而不是立即失败。切换完成后MMM 会把 db1 标记为failedVIP 已经在 db2 上业务恢复正常。等到物理机重启完成MySQL 也周期性进程拉起此时 MMM 不会自动让它上线需要手动确认数据一致性后执行mmm_control set_online db1上线前我必做的一件事是把 db1 上缺失的 binlog 日志补全确保复制追平到和 db2 一致的位点再开启 MMM 的监控。千万别偷懒自动上线数据差错一旦发生比宕机更麻烦。5.2 监控体系与告警配置高可用不是说挂了能切就行日常的主动监控才能避免被动故障。我在生产上维护这套架构时监控重点在以下几个维度监控项检查方式告警阈值MySQL 进程存活系统进程检查异常立即告警主主复制状态SHOW SLAVE STATUSIO/SQL 线程非 Yes 时告警复制延迟心跳表时间差超过 10 秒告警MMM 节点状态mmm_control show非 online 告警ATLAS 后端状态SELECT * FROM backendsoffline 比例超过 50% 告警写 VIP 归属绑定 IP 检查不在预期节点告警监控脚本我是用 Shell 写的每 30 秒拉一次状态异常就告警同时也把状态输出到本地日志方便事后回溯。性能上没有任何压力关键是逻辑必须覆盖“主主双通道”和“VIP 角色”这两个核心状态。我曾经遇到过一个情况MMM 显示两台 MySQL 都是 online但复制实际已经断开SHOW SLAVE STATUS里面Slave_IO_Running已经是 Connecting。原因是一个长时间未提交的事务锁导致 IO 线程卡死这种状态不是宕机MMM 不会触发切换但数据已经处于不一致的边缘。所以监控里对复制状态的盯防要像做手术时盯着心跳一样仔细。5.3 常见问题速查表现象可能原因处理方法MMM 启动失败状态为 unmonitoredagent 服务未启动 / 网络不通检查 agent 进程、mmm_control ping连通性故障切换后原主库恢复一直处于 catchup复制延迟过大binlog 未追平等待追平后再set_online必要时手动补日志ATLAS 显示后端 offline但数据库正常健康检查超时配置过短调大check_interval参数或重启 ATLAS 重新探测写请求路由到旧主库业务报只读错误MMM 已切换但 ATLAS 缓存了后端状态在 ATLAS 管理端手动SET ONLINE新主或重启代理主主复制频繁出现主键冲突auto_increment_increment配置不当统一所有节点步长为节点总数并规划好 offset连接池报 Too many connectionsATLAS 到 MySQL 后端连接数超限增大后端max_connections同时调小 ATLAS 连接池保留连接数这张表是多年踩坑浓缩出来的几乎每一行都有真实事故对应。比如最后一条那次是开发同学在代码里没关连接连接池泄露ATLAS 把后端连接打满MySQL 直接拒绝新连接。排查半天发现是应用问题但如果没有 ATLAS 级的连接数限制整个数据库会被拖垮。6. 这套架构的边界与演进思考6.1 要清醒认识它的局限MySQLATLASMMM 这套方案在早期确实扛住了大量中小公司的核心数据库压力但放在今天的视角看它有几个明显的边界第一MMM 对 GTID 的支持不好它的核心机制基于 binlog 位点一旦主主复制出现位点漂移手工恢复会很痛苦。现在很多 DBA 转向 GTID 自动定位复制这在可用性上是大跃迁第二MMM 的切换决策依赖于 monitor 节点的健康状态monitor 本身又是单点要给它再叠加高可用方案架构复杂度就上去了第三ATLAS 不支持跨库分片的分布式事务对复杂业务支持有限。所以我通常建议如果你现在要重新搭建一套数据库高可用架构而不是维护一套已经运行多年的旧系统可以优先考虑 MHAMaster High Availability或者 MySQL 官方在 5.7 时代推荐的半同步复制自动故障转移方案。但话又说回来技术评估要结合实际场景。很多企业的基础设施改造都是渐变式的现有系统跑得好好的硬要推翻重构风险反而更大这种情况下先了解这套经典架构的原理和运维方法依然有很实际的价值。6.2 可平滑过渡到的新形态如果要在不重建系统的情况下做演进比较顺滑的路径是这样的数据库层把 MMM 替换成 MHAMHA 对 GTID 和半同步复制支持更好切换更快数据丢失风险更低。代理层把 ATLAS 替换成 MyCat 或 ShardingSphere它们不仅支持读写分离还支持真正的分库分表、分布式事务业务增长后不用再换架构。这一步可以做到应用无感知因为连接方式和协议都没变只是换了一个中间层入口。有人担心这套架构当年是几十台服务器厂内部大量使用的现在替换会不会丧失稳定性。但实际上你只要把角色理清楚中间件的适配层可以暂时保留数据库核心库自己先换再逐步过渡应用连接这种灰度式的演进风险可控。6.3 运维体感与长期主义最后分享一点运维感受。在维护这套架构几年后我深刻的体会是高可用不是指系统“永不宕机”而是“宕了能快速恢复、恢复后数据还是对的”。MySQLATLASMMM 这套体系核心价值就在于它把“切换”这个动作从小时级提升到了分钟级把“人为误操作”的空间压缩到了最小。但同样的它也在不断提醒我们架构只是兜底真正的可靠性来自于每一天的监控巡检、备份验证和故障演练。我在做每一次演练时都会问自己一个问题如果现在这台机器永远起不来了我的恢复手册是否还能让业务在 10 分钟之内跑起来这个答案如果是否那么不管用什么架构都值得警惕。后面我们在团队里养成了一个习惯每季度做一次全链路故障演练把切换、回切、数据校验、监控报警全部走一遍演练过程中发现的问题比任何文档都更有价值。这套架构也许终将被更现代的方案取代但它教会我们的分层设计思路、监控意识和切换方法论放在今天任何一套高可用系统里都不会过时。