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 节点划分与参数设计
在做任何操作之前,先把节点规划搞清楚。我建议的部署形态是:
| 节点角色 | 主机地址 | 组件 |
|---|---|---|
| 数据库节点1 | 10.0.0.11 | MySQL、MMM agent |
| 数据库节点2 | 10.0.0.12 | MySQL、MMM agent |
| 监控节点 | 10.0.0.13 | MMM monitor |
| 代理节点 | 10.0.0.14 | ATLAS |
那个年代服务器资源普遍紧张,所以 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 = 2node2 的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: 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是初始角色,实际运行中会随故障切换而变动。读 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.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 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 STATUS | IO/SQL 线程非 Yes 时告警 |
| 复制延迟 | 心跳表时间差 | 超过 10 秒告警 |
| MMM 节点状态 | mmm_control show | 非 online 告警 |
| ATLAS 后端状态 | SELECT * FROM backends | offline 比例超过 50% 告警 |
| 写 VIP 归属 | 绑定 IP 检查 | 不在预期节点告警 |
监控脚本我是用 Shell 写的,每 30 秒拉一次状态,异常就告警,同时也把状态输出到本地日志方便事后回溯。性能上没有任何压力,关键是逻辑必须覆盖“主主双通道”和“VIP 角色”这两个核心状态。我曾经遇到过一个情况:MMM 显示两台 MySQL 都是 online,但复制实际已经断开,SHOW SLAVE STATUS里面Slave_IO_Running已经是 Connecting。原因是一个长时间未提交的事务锁导致 IO 线程卡死,这种状态不是宕机,MMM 不会触发切换,但数据已经处于不一致的边缘。所以监控里对复制状态的盯防,要像做手术时盯着心跳一样仔细。
5.3 常见问题速查表
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| MMM 启动失败,状态为 unmonitored | agent 服务未启动 / 网络不通 | 检查 agent 进程、mmm_control ping连通性 |
| 故障切换后原主库恢复,一直处于 catchup | 复制延迟过大,binlog 未追平 | 等待追平后再set_online,必要时手动补日志 |
| ATLAS 显示后端 offline,但数据库正常 | 健康检查超时配置过短 | 调大check_interval参数,或重启 ATLAS 重新探测 |
| 写请求路由到旧主库,业务报只读错误 | MMM 已切换但 ATLAS 缓存了后端状态 | 在 ATLAS 管理端手动SET ONLINE新主,或重启代理 |
| 主主复制频繁出现主键冲突 | auto_increment_increment配置不当 | 统一所有节点步长为节点总数,并规划好 offset |
| 连接池报 Too many connections | ATLAS 到 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 分钟之内跑起来?这个答案如果是否,那么不管用什么架构都值得警惕。后面我们在团队里养成了一个习惯:每季度做一次全链路故障演练,把切换、回切、数据校验、监控报警全部走一遍,演练过程中发现的问题比任何文档都更有价值。
这套架构也许终将被更现代的方案取代,但它教会我们的分层设计思路、监控意识和切换方法论,放在今天任何一套高可用系统里都不会过时。