ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL+ATLAS+MMM高可用架构实战:读写分离与主主切换

MySQL+ATLAS+MMM高可用架构实战:读写分离与主主切换

1. 集群架构设计与选型思路

1.1 为什么是这三件套

先聊个现实情况:很多团队一提到高可用,第一反应就是上云、用托管数据库,但在过去很长一段时间里,自建机房和物理服务器的场景才是主流。哪怕放到今天,私有化交付、信创改造、内网隔离环境里,你依然绕不开“自己搭一套数据库高可用”的需求。而 MySQL+ATLAS+MMM 这套组合,就是那个年代沉淀下来、被无数生产环境验证过的经典方案。

先说三者的分工,简单比喻一下:MySQL 是真正干活的人,负责存储数据和处理事务;MMM(Master-Master Replicator)是监工,盯着两个 MySQL 是否健康,谁挂了就把流量切到另一台;ATLAS 则是前台接待,所有应用请求先到它这里,由它决定把写操作分给哪个后端、把读操作分给哪些后端,实现读写分离。三者各司其职,配合起来就是一套完整的读写分离+主主切换的高可用架构。

为什么选它而不是别的方案?在那个年代,LVS+Keepalived 只能解决 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必须合理配置,前者控制自增起始值,后者控制步长。例如两台节点:

  • node1:server-id = 1,auto_increment_offset = 1,auto_increment_increment = 2
  • node2:server-id = 2,auto_increment_offset = 2,auto_increment_increment = 2

目的是避免主主双向复制时自增主键冲突。原理很简单:步长为 2,node1 只生成奇数主键,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 = 2

node2 的my.cnf,除了server-id和 offset 不同,其余一致。这里把binlog_format设为 ROW 是经验之谈,早期的 statement 格式在主主复制中容易出现数据不一致,尤其是使用 UUID()、NOW() 这类非确定性函数时,row 格式才是最稳妥的。

创建复制专用账号(两个节点都需要):

GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.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: Yes
  • Slave_SQL_Running: Yes
  • Seconds_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是初始角色,实际运行中会随故障切换而变动。读 VIP(reader_ip)会指向当前所有存活节点,写 VIP(writer_ip)永远指向当前 writer。配置完成后执行:

mmm_control start mmm_control show

看到所有节点状态为online,VIP 处于正常绑定状态,才算真正就绪。这里有个雷区:MMM 的auto_set_online = 60表示节点故障后自动恢复时间为 60 秒。在数据复制延迟很严重的情况下,自动恢复过快可能导致刚恢复的节点带着旧数据抢回 VIP。生产上有过切回后数据丢失的事故,所以我建议把这个值调大,比如 300 秒,等数据追平确认无误后再让 MMM 自动把节点拉回。

3.3 手动切换与故障演练

MMM 支持手动切换。你想要在维护前主动把流量切走,执行:

mmm_control move_roles role=writer to db2 mmm_control move_roles role=reader 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.ini

ATLAS 管理接口是个亮眼的设计,你不需要重启就能实时查看和调整后端状态:

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 URL:

jdbc:mysql://10.0.0.14:3307/webdb?characterEncoding=utf8&connectTimeout=2000&socketTimeout=5000&autoReconnect=true

在引入读写分离后,要特别注意“写完立即读”的场景。比如用户注册后跳转到登录页,刚写入的账号信息可能还没来得及同步到读节点,而读请求刚好被分发到了延迟的节点上,就会遇到查不到数据的尴尬。解决方案通常有两种:一是让这类关键读也走写库(比如在 SQL 前加注释强制走主库,ATLAS 支持通过注释语法强制主库);二是通过调整 MMM 的复制延迟监控,确保读节点不会接收明显延迟的数据。更实用的做法是应用层做补偿,比如注册后强制等待几百毫秒或重试一次。

连接池参数方面,因为中间层会增加一次网络跳转,建议把connectTimeout保持在 2-3 秒,socketTimeout不能设太短,否则数据库慢查询时客户端会误判为连接异常。还有autoReconnect=true是针对故障切换的保命配置,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 标记为failed,VIP 已经在 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 要清醒认识它的局限

MySQL+ATLAS+MMM 这套方案,在早期确实扛住了大量中小公司的核心数据库压力,但放在今天的视角看,它有几个明显的边界:

第一,MMM 对 GTID 的支持不好,它的核心机制基于 binlog 位点,一旦主主复制出现位点漂移,手工恢复会很痛苦。现在很多 DBA 转向 GTID 自动定位复制,这在可用性上是大跃迁;第二,MMM 的切换决策依赖于 monitor 节点的健康状态,monitor 本身又是单点,要给它再叠加高可用方案,架构复杂度就上去了;第三,ATLAS 不支持跨库分片的分布式事务,对复杂业务支持有限。

所以我通常建议,如果你现在要重新搭建一套数据库高可用架构,而不是维护一套已经运行多年的旧系统,可以优先考虑 MHA(Master High Availability)或者 MySQL 官方在 5.7+ 时代推荐的半同步复制+自动故障转移方案。但话又说回来,技术评估要结合实际场景。很多企业的基础设施改造都是渐变式的,现有系统跑得好好的,硬要推翻重构风险反而更大,这种情况下先了解这套经典架构的原理和运维方法,依然有很实际的价值。

6.2 可平滑过渡到的新形态

如果要在不重建系统的情况下做演进,比较顺滑的路径是这样的:

数据库层,把 MMM 替换成 MHA,MHA 对 GTID 和半同步复制支持更好,切换更快,数据丢失风险更低。代理层,把 ATLAS 替换成 MyCat 或 ShardingSphere,它们不仅支持读写分离,还支持真正的分库分表、分布式事务,业务增长后不用再换架构。这一步可以做到应用无感知,因为连接方式和协议都没变,只是换了一个中间层入口。

有人担心这套架构当年是几十台服务器厂内部大量使用的,现在替换会不会丧失稳定性。但实际上,你只要把角色理清楚:中间件的适配层可以暂时保留,数据库核心库自己先换,再逐步过渡应用连接,这种灰度式的演进风险可控。

6.3 运维体感与长期主义

最后分享一点运维感受。在维护这套架构几年后,我深刻的体会是:高可用不是指系统“永不宕机”,而是“宕了能快速恢复、恢复后数据还是对的”。MySQL+ATLAS+MMM 这套体系,核心价值就在于它把“切换”这个动作从小时级提升到了分钟级,把“人为误操作”的空间压缩到了最小。但同样的,它也在不断提醒我们:架构只是兜底,真正的可靠性来自于每一天的监控巡检、备份验证和故障演练。

我在做每一次演练时都会问自己一个问题:如果现在这台机器永远起不来了,我的恢复手册是否还能让业务在 10 分钟之内跑起来?这个答案如果是否,那么不管用什么架构都值得警惕。后面我们在团队里养成了一个习惯:每季度做一次全链路故障演练,把切换、回切、数据校验、监控报警全部走一遍,演练过程中发现的问题比任何文档都更有价值。

这套架构也许终将被更现代的方案取代,但它教会我们的分层设计思路、监控意识和切换方法论,放在今天任何一套高可用系统里都不会过时。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进