ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MySQL读写分离实战:Atlas中间件部署、配置与故障排查全解析

MySQL读写分离实战:Atlas中间件部署、配置与故障排查全解析

如果你维护过一套 MySQL 主从架构,大概率遇到过这个场景:主从复制跑得稳稳当当,从库的负载却上不去,主库反而被一波一波的读请求压得喘不过气。我最早处理这个问题时,选择在应用代码里手动区分读库和写库,结果每个服务都要配两套数据源,后期加一台从库还要改一堆连接配置,想想都头大。后来我把 Atlas 引进架构,才算把读写分离从“应用层负担”变成了“中间件能力”。这篇内容围绕 Atlas 这个 MySQL 代理中间件,把部署、配置、故障切换、分表规则和排障过程完整讲一遍,适合正在选型读写分离方案的 DBA、后端开发,以及所有想了解 MySQL 代理层原理的人。

1. 读写分离为什么需要 Atlas 这层代理

1.1 主从复制解决不了“流量打在主库”的问题

先聊一个很基础但经常被忽略的问题。MySQL 主从复制解决的是数据冗余、灾备、以及“读多写少”场景下的扩展问题,但它本身不会帮你做流量调度。主库和从库都立在那里,应用程序怎么知道哪条 SQL 该走哪台机器?答案是:不知道。

大多数团队一开始的写法是,在服务里配置一个主库连接串和一个从库连接串,然后自己写一个读写分离的工具类,根据 SQL 前缀判断是 SELECT 还是其他操作,再选择不同的数据源。这方案不是不能跑,而是后面维护起来极其痛苦。举个例子,业务从 5 个服务涨到 20 个服务,每个服务的连接池参数、从库地址都要独立维护,哪次从库扩容漏改了一个配置,那部分流量就继续压在主库上,等于白扩了。

我当时的判断是,必须在应用和 MySQL 之间加一层统一的代理,让所有数据库访问都走同一道门,由这道门来分发读写流量。这也是引入 Atlas 最核心的原因:不是因为它多花哨,而是它能把“读写分离”从每个服务各自的私活,收拢成整个架构层面的统一能力。

1.2 Atlas 和其他中间件的定位差异

Atlas 是 360 基于 MySQL-Proxy 0.8.2 二次开发的一个开源数据库中间件,2013 年前后开始在 GitHub 上维护,核心能力就是读写分离、负载均衡、单库分表和管理接口。这里得说清楚,它跟现在更流行的 MyCat、ProxySQL 不是一个路子。

我画个简单对照你感受下:

组件核心定位读写分离分库分表动态管理上手难度
AtlasMySQL 代理,偏读写分离强,开箱即用只支持单库分表,不支持分库通过管理接口在线切换后端状态低
MyCat分布式数据库中间件支持支持分库分表,能力强需要改配置重启为主中高
ProxySQL高性能 SQL 代理支持,且规则灵活主要通过路由规则 + 后端分片很强,支持在线改配置中

Atlas 的优势在于“简单”。部署结构简单、配置项少、路由逻辑纯粹。它不打算帮你做复杂的分布式事务,也没想替你把跨节点的 join 全都接管,它就是把读写分离和单库分表这两件事做好。如果你的需求很简单:主库写、从库读、从库之间轮询分摊读流量,那 Atlas 是个非常顺手的选择,运维起来也不用养一个专门的中间件团队。

1.3 Atlas 在架构里的典型位置

在实际部署中,Atlas 就是一个无状态的代理层,放在应用和 MySQL 之间。应用只认识 Atlas 的地址和端口,至于后端有几台从库、主库是谁,都不需要关心。它默认会占用两个端口:一个是代理端口,默认 1234,专门给业务连接用;另一个是管理端口,默认 2345,给 DBA 或者运维系统查状态、做切换用。

无状态这个特性很关键,意味着你可以同时跑两个 Atlas 实例,互相之间不需要同步任何会话信息。配合 keepalived 挂一个虚拟 IP,谁挂了 VIP 自动漂到另一台,应用那边根本感知不到。后面我会专门讲这个高可用方案。

2. 部署 Atlas 时最容易忽略的两个细节:版本与目录权限

2.1 版本选择与安装包准备

Atlas 的官方源码和预编译包都在 GitHub 上,项目地址是 Qihoo360/Atlas。这里我想先强调一个经验:能用预编译包就别自己编译源码。Atlas 本身依赖比较老,自己编译会碰上一堆 glibc、libevent、luajit 的兼容问题,折腾半天不如直接下载对应系统的编译产物。

下载解压后,整个目录会被平铺到/usr/local/mysql-proxy下,这个路径是默认的,后面所有配置、日志都基于它。如果你手动改安装目录,那启动命令、脚本路径、PID 文件路径都要跟着改,没必要。

我把安装后的目录结构列一下,心里有个数:

  • /usr/local/mysql-proxy/bin/mysql-proxy:主程序
  • /usr/local/mysql-proxy/conf/mysql-proxy.cnf:核心配置文件
  • /usr/local/mysql-proxy/lib/mysql-proxy/lua/:Lua 脚本,读写分离和分表的逻辑在这里
  • /usr/local/mysql-proxy/log/:日志目录,SQL 日志和运行日志都在这里

2.2 配置文件权限这个坑,我替你们踩过了

安装完第一件事,不是直接改配置,而是先处理配置文件权限。Atlas 对mysql-proxy.cnf的权限非常敏感,如果权限大于 600,启动时会在日志里报file not readable之类的问题,然后拒绝启动。

我第一次部署时没注意这茬,从本地把配置文件传到服务器后还是默认的 644 权限,来回检查配置语法半天,最后才发现是权限问题。所以在改配置之前,先执行:

chmod 600 /usr/local/mysql-proxy/conf/mysql-proxy.cnf

这个操作还有个额外作用:配置文件里有密码字段,虽然是加密后的,但把权限收紧到只有属主可读写,总归符合安全习惯。

2.3 启动、关闭与状态确认

Atlas 启动很简单,直接执行主程序就行:

/usr/local/mysql-proxy/bin/mysql-proxy

前提是配置文件里的daemon = true,这样进程才会在后台常驻。启动后我习惯按顺序确认三件事:

  1. 进程是否在跑:ps -ef | grep mysql-proxy;
  2. 端口是否监听:netstat -an | grep 1234和grep 2345;
  3. 日志里有没有报错:tail -f /usr/local/mysql-proxy/log/sql.log。

关闭进程也别直接 kill -9,Atlas 的 PID 文件默认在/usr/local/mysql-proxy/conf/下,用脚本关闭最稳妥。如果发现 PID 文件缺失,就需要手动查进程号再停,但这种情况一般只出现在非正常退出之后。

3. 读写分离配置逐项拆解:每一行 conf 都不能瞎写

3.1 从一份主从分离配置说起

先给出一份我在测试环境里验证过的配置示例,再用管理员口吻逐项解读。配置文件的格式是 INI 风格:

[mysql-proxy] admin-username = atlasadmin admin-password = 这里是加密后的密码 admin-address = 0.0.0.0:2345 proxy-address = 0.0.0.0:1234 proxy-backend-addresses = 192.168.10.10:3306 proxy-read-only-backend-addresses = 192.168.10.11:3306,192.168.10.12:3306 daemon = true event-threads = 4 max-connections = 1024 log-level = message log-file = /usr/local/mysql-proxy/log/sql.log sql-log = ON charset = utf8

3.2 核心字段说明

proxy-backend-addresses只有一个,就是主库地址,所有写操作都会转发到这里。proxy-read-only-backend-addresses可以写多个,逗号分隔,Atlas 会在这些只读后端之间做轮询负载均衡。

这里有个很值得注意的点:Atlas 默认把读请求按轮询方式分发到各个从库,所以在配置从库之前,你必须确认这些从库的数据是一致的。如果主从延迟很大,轮询分发会让业务读到旧数据,看起来就像“数据丢失”一样。我见过不止一次线上事故,最后追踪到主从延迟已经有好几分钟,而读写分离代理还在傻乎乎地轮询,应用读到的全是过期数据。这个不是 Atlas 的锅,而是引入读写分离前必须建立主从延迟监控。

event-threads是工作线程数,不是越大越好。我一般按 CPU 核数的 2 到 4 倍设置,比如 4 核机器配 8 到 16。线程太多反而会在高并发下增加上下文切换开销。

max-connections控制 Atlas 接受的最大客户端连接数。这个值要根据后面挂多少应用来估算,不能拍脑袋。我通常把所有接入服务的连接池上限加起来,再留 20% 到 30% 的余量。

3.3 管理密码不是明文,是加密串

配置里的admin-password不能直接填明文,需要先通过 Atlas 自带的加密工具生成密文再填进去。工具路径在bin目录下,用法很简单:

/usr/local/mysql-proxy/bin/encrypt 123456

执行完之后会输出一串加密后的字符串,把它填到admin-password字段里。注意每次执行生成的结果大概率是不同的,因为加密过程引入了随机因子,所以只需要保证“当前配置文件里的密文是对应明文加密得来”的即可。

3.4 配置完成后如何验证读写分离真的生效

配置改完重启 Atlas,很多同学的验证方式就是连上去跑一条 SELECT 一条 UPDATE,看到没报错就认为 OK 了。这远远不够。

我的验证流程是这样的。首先在 Atlas 日志里确认 SQL 确实经过代理:

tail -f /usr/local/mysql-proxy/log/sql.log

然后开两个终端,一个在从库机器上用通用查询日志监控实际进来的 SQL,另一个在应用端执行操作。执行几条 SELECT,去从库的日志里看语句有没有到;执行一条 UPDATE,再去主库看。两边都能对上,才叫验证通过。

另外还可以通过管理接口看后端状态。Atlas 管理端口支持 MySQL 协议,直接拿 mysql 客户端登录:

mysql -h127.0.0.1 -P2345 -uatlasadmin -p

登录后执行:

SELECT * FROM backends;

这个命令会列出所有后端实例的地址、类型、状态等信息。type字段里rw代表主库,ro代表只读从库,看到状态是UP就说明 Atlas 的心跳检测已经通过,节点可用。

4. 从库掉线、主库切换:把故障挂在 Atlas 管理接口上

4.1 从库掉线时,Atlas 会怎么做

在 Atlas 的设计里,每个后端节点都有健康状态,它内部会定期检测后端可用性。检测方式就是向 MySQL 发起一个轻量的 ping 连接,如果失败就标记节点不可用。

当一个从库掉线时,Atlas 不会把读流量继续发到故障节点,而是自动在剩余可用从库之间重新分配流量。这个感知过程可能有一个短暂的时间窗口,如果应用在那个瞬间发起查询,会报一个连接错误,但重试之后就能正常路由到其他从库。

这里我要特别提醒:不要把从库掉线完全交给 Atlas 的自动检测,你仍然需要对后端做外部监控。原因有两个:一是 Atlas 的状态感知有延迟,生产环境里这个时间窗口可能造成个别请求失败;二是如果所有从库都挂了,读流量会全部打回主库,这时候主库压力会陡增,必须有告警通知到人。

4.2 主库故障时的手动切换流程

Atlas 不像 MHA 那样会自动提升新主库,主库挂掉之后,写流量会直接失败,这时候需要人工介入做切换。我整理了一套比较稳妥的流程:

  1. 确认新主库数据已经补平。如果原来有几台从库,通常选数据最新的一台接管。
  2. 在 Atlas 管理接口里,先把旧主库摘除,让它不再接收流量。执行:
SET offline 1;

这里的1是对应旧主库的 backend id,具体看SELECT * FROM backends;返回结果的顺序。

  1. 在 MySQL 层面把新主库的角色提升为主库,并调整其他从库的主从复制关系。
  2. 修改 Atlas 配置文件里的proxy-backend-addresses,把地址改成新主库的 IP 和端口。
  3. 重启 Atlas 进程,重新在管理接口里确认新主库状态为UP。

这套流程看起来不长,但每一步都有坑。最典型的是第二步和第三步的顺序,如果你先改了 MySQL 复制关系,再处理 Atlas,中间那段时间新主库可能还在 Atlas 的只读后端列表里,读流量和新主库状态会暂时不一致。所以我的习惯是:先摘掉 Atlas 侧的状态,再改 MySQL 侧的角色,最后再让 Atlas 重新加载配置。

4.3 两个 Atlas 实例做高可用,应用才不会断

既然 Atlas 本身是无状态的,那完全可以通过两实例冗余实现中间件层的高可用。我常用的做法是 keepalived 配虚拟 IP。

两台服务器都装 Atlas,配置完全一致,唯一区别是 keepalived 角色不同。正常情况只有一台绑定了虚拟 IP,所有应用都连这个虚拟 IP。如果这台机器宕机,keepalived 会自动把虚拟 IP 漂到另一台,应用重新建立连接后就走新的 Atlas 实例了。

这个方案的关键点是:应用连接中间件时不要用长连接永不断开,最好在连接池里设置一个合理的连接存活时间,否则当虚拟 IP 切换后,旧连接会一直卡在超时状态,反而影响恢复速度。我习惯把连接池的最大存活时间设成 30 到 60 分钟,这样 IP 漂移最多影响一小部分正在使用的连接。

5. 分表是最后一张牌:Atlas 分表规则与业务约束

5.1 分表规则到底怎么写

Atlas 的分表能力与 MyCat 不同,它只做“单库分表”,也就是说所有子表都在同一个数据库实例里,不会跨库。这对很多中型业务来说其实是更合理的选择,因为跨库查询的复杂度会指数级上升。

分表规则写在配置文件的末尾,格式大致是:

tables = testdb.user.id.4-0

这段配置的含义是:testdb库下的user表,按照id字段分 4 张子表,子表编号以0开始。也就是说,业务想真正使用分表,需要提前在数据库里把user_0、user_1、user_2、user_3这 4 张物理表都建好,Atlas 只负责路由,不负责建表。子表的表结构也要保持一致,这是基本前提。

至于某一行的数据具体落到哪张表,Atlas 对分表键做哈希后取模。只要id相同,同一行数据永远会落到同一张子表。这个特性对后续查询、更新很关键,因为路由可以精确命中一张物理表,而不是全表扫描。

5.2 分表键选不好,后面全是坑

我在生产环境里见过最典型的问题,就是分表键选了一个业务上无法稳定获取的字段,或者选了一个查询频率不高的字段。比如按订单号分表,但业务里大量查询都是根据用户 ID 查订单,那这些查询就没法通过用户 ID 定位到具体某张子表,只能广播到所有分表去搜一遍,性能反而比不分表还差。

选分表键要同时满足三个条件:

  • 业务查询基本都会带上这个字段的等值条件;
  • 这个字段的值几乎不会变更;
  • 值的分布足够均匀,不能集中在某几个值上。

大多数场景我会优先推荐用户 ID、租户 ID、业务流水 ID 这类天然带有隔离性的字段。分表数量也别贪多,4 到 16 张通常是大多数团队可以接受的复杂度。

5.3 哪些查询能走分表,哪些走不了

Atlas 的分表路由不是万能的。带分表键等值条件的查询、插入、更新、删除,都能精确命中分表;但如果条件里没有分表键,Atlas 会把 SQL 广播到所有分表上执行,然后合并结果。小数据量看影响不大,一旦单表数据量上去了,广播查询足以拖垮整个实例。

聚合类查询也要特别小心。例如不带分表键的COUNT(*)、SUM()、多表JOIN,在分表场景下会变得非常复杂。虽然 Atlas 在简单场景下能做一些结果合并,但我不建议在核心链路里依赖这个能力。

我参与过的几个分表项目,后面基本都演化出一条经验:分表解决的是“单表写入压力”和“单表体积过大”的问题,不是所有 SQL 问题都适合靠分表来解决。如果业务里有大量慢查询和复杂关联,先把慢查询治理好,再评估分表是否必要。分表一旦上了,业务代码的改造量远超预期,所以它真的只能是最后一张牌。

6. 跑了半年才遇到的坑:Atlas 生产环境排障实录

6.1 连接数被打满,应用全部超时

上线几个月后第一次遇到大故障,表现是应用侧数据库连接池的活跃连接数不断上涨,错误日志里频繁出现Too many connections。

刚开始怀疑 MySQL 的连接数满了,但到 MySQL 上一查,连接数远没有到上限。于是回头查 Atlas,才发现是 Atlas 的max-connections配置太小。原因是新业务接进来后,应用连接池的连接数估算做得过于乐观,多个服务一起跑满后,Atlas 先撑不住了。

排查手段很简单,在 Atlas 机器上执行:

netstat -an | grep 1234 | grep ESTABLISHED | wc -l

看当前与代理端口建立的连接数,再对比配置里的max-connections。调整建议是:应用连接池总数上限加总的 1.3 倍左右,同时要在 Atlas 前面做好连接数监控,不能等爆了再去加。

这次故障给我一个很深的教训:中间件的连接数上限不是随便填一个差不多的值,它应该是整个后端连接规划里的一等公民。

6.2 应用连不上代理端口,怎么一步步排查

还有一个高频问题,就是应用突然连不上 Atlas,报连接超时或者连接被拒绝。排查要按链路一层一层来,千万不要一上来就怀疑配置,那会浪费大量时间。

先在 Atlas 所在的机器上,用 MySQL 客户端本地连一次:

mysql -h127.0.0.1 -P1234 -utest_user -p

本地能连上,说明代理进程本身没问题,问题大概率在网络链路或权限上。再跨机器 telnet 一次:

telnet atlas_ip 1234

不通的话,检查防火墙、安全组、网络策略。如果通,但应用还是报认证失败,那就要检查用户名密码配置和授权 SQL。Atlas 会把后端的用户校验往 MySQL 上传,所以也要同步确认 MySQL 里的用户权限和访问来源条件。

这一套排查下来看着简单,但在实际事故中最容易犯的错是跳步。比如直接在客户端验证不了就改动配置,结果越改越乱。按顺序来,每一步都有一个明确结论,才能快速收敛问题。

6.3 SQL 日志不开,事后分析全靠猜

我特别建议线上环境把 SQL 日志打开。Atlas 的 SQL 日志对于分析慢查询、排查分表路由、确认读写分离是否生效都很有用。

配置里需要同时打开两个点:

log-level = debug sql-log = ON

log-level至少要debug级别,sql-log控制是否记录 SQL 内容。如果只开message级别,很多细节会丢失。日志写满的问题可以通过外部 logrotate 解决,但排查问题的收益远大于那点磁盘消耗。

后来踩过一次坑,某天从库负载很高,但谁都说不清是哪条 SQL 打过来的。打开 SQL 日志分析后,发现是某个报表任务在凌晨批量扫表,而且每条查询都没带分表键,导致广播到所有分表执行。如果没有 SQL 日志,这种问题基本无从下手。

6.4 长事务和连接泄漏,是慢查询之外的隐形杀手

最后说一个比较隐蔽的问题。有段时间我观察到后端 MySQL 的 sleep 连接数持续增长,而且集中在 Atlas 的代理连接上。一开始以为是正常现象,但增长趋势一天比一天陡,最后还是 OOM 之前被监控叫醒的。

排查后发现,是应用框架的连接池在异常情况下没有及时释放连接,加上个别长事务一直不提交,导致 Atlas 和后端之间的连接一直被占用。处理手段分两层:应用侧给连接池配置空闲连接回收策略,设置合适的——IDLE_TIMEOUT类参数;MySQL 侧把wait_timeout和interactive_timeout调到合理范围,让长时间空闲的连接被服务端主动回收。

这类问题不能用单一手段解决,必须应用和数据库两头同时治理。我是先收紧空闲连接超时,再用夜里低峰期观察 sleep 连接曲线,确认不再上涨之后才算告一段落。

维护 Atlas 这大半年,我最深的体会是:中间件只负责路由,它不会替你把 SQL 质量变好,也不会自动消化设计上的不合理。读写分离让你少操主库的读压力,但主从延迟监控、连接数规划、慢 SQL 治理这些基本功一样都不能省。如果你打算在项目里引入 Atlas,我的建议很直白:先从读写分离用起,跑稳两三个月再考虑分表。分表的配置确实简单,但它对业务的侵入比想象中大得多,前期把 SQL 梳理清楚,比自己闷头写一堆分表规则有用得多。

RELATED READING

延伸阅读

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