"atlas"这个名字,圈内人一听就知道不简单。从希腊神话里扛天的巨神,到数据库世界里扛并发的中间件,这个名字从来都代表着"承重"。今天想聊的,是我最近在折腾的一个以 atlas 命名的 MySQL 中间件项目——如果你正在为数据库读写压力发愁、被分库分表方案搞得头大,这篇东西应该能帮你少走不少弯路。我尽量把思路、原理和实操过程揉碎了讲,适合刚接触中间件的朋友,也适合那些已经在用但想搞懂"为什么"的人。
1. 项目整体设计与思路拆解
1.1 这个项目到底解决了什么问题
先还原一下我最初遇到的场景。当时我负责的一个业务系统,单库单表已经扛到了每天千万级的读写量。监控面板上,主库的 CPU 常年 70% 以上,连接数时不时冲顶,慢查询一条接一条往报警群里刷。最难受的不是性能本身,而是架构上根本不敢动——业务代码里写死了数据库连接,几百个接口直接操作主库,别说分库分表,连读写分离都只能靠 DBA 手工拉只读实例然后让开发改代码。
这时候 atlas 这类中间件的价值就出来了。它干的事,本质上是在应用和 MySQL 之间插一层透明代理。业务层不需要改 SQL、不需要改连接方式,只要把数据库地址换成中间件的地址,剩下的读写分离、分库分表、连接池管理,全部交给中间件去处理。
这套思路和我之前用过的其他方案有个根本区别。早期我们试图在应用层做个"数据访问层"来解决读写分离,虽然也能跑,但每个项目都要重复造轮子,不同团队写的访问层质量参差不齐,出了问题要翻几个项目的代码才能定位。中间件方案则是把这块能力下沉到独立进程里,统一收口、统一治理,业务侧真正做到无感。
1.2 为什么选中间件架构而不是直接改业务代码
这里牵扯到一个很关键的架构取舍问题。很多人会问:市面上那么多 ORM 框架,里面自带读写分离功能,为什么还要单独搞一个中间件?
我的看法是,业务代码里的读写分离只是个"功能",而中间件是一个"平台"。功能解决的是有没有的问题,平台解决的是好不好运维、能不能扩展的问题。举个例子,如果走 ORM 框架层方案,每接入一个新服务,都要配置一遍数据源、配置一遍主从关系、配置一遍路由规则;一旦域名变更或者实例扩容,所有服务的配置都要跟着改。而中间件方案下,这些全部收敛到一处,业务开发完全不感知数据库拓扑的变化。
另外还有一个很重要的点——强一致性和透明性问题。中间件作为代理,可以统一控制事务边界、统一处理主从延迟带来的数据不一致风险,甚至可以在中间这一层做 SQL 拦截和改写。这是在业务代码里很难做到的事情,因为没有哪个团队能保证几百个服务里的每个查询都遵循同一套规范。
1.3 核心模块的职责划分
从架构设计上看,atlas 的核心模块大致可以拆成四块,这些模块的划分方式也是我在其他中间件里见过的最典型的一种。
第一块是连接管理模块。数据库连接是有成本的,频繁创建销毁连接非常耗资源。中间件在这里维护了一个连接池,对业务侧表现为一个永不关断的"假数据库",实际上内部复用真正的后端连接。这个模块还负责连接的健康检查、自动重连,以及超时后的清理。
第二块是 SQL 解析与路由模块。这是整个中间件的技术核心。业务发过来一条 SQL,中间件会先做词法分析,判断是读操作还是写操作,判断查询涉及哪张表,然后根据配置的路由规则决定发往哪个后端节点。
第三块是负载均衡与高可用模块。多个从库的情况下,读请求怎么分发?主库挂了怎么办?这些策略都在这个模块里实现。常见的轮询、权重分配、最少连接数分配,都属于这块的职责。
第四块是管理监控模块。包括对外的管理接口、日志输出、状态指标统计。这一块虽然看起来不起眼,但真正生产环境下,没有它你连排查问题都无从下手。
2. 核心原理与关键技术点解析
2.1 读写分离的实现机制与主从延迟陷阱
读写分离,这个概念说出来谁都懂,但真正做扎实不容易。atlas 的判断逻辑并不复杂:拿到 SQL 后先做词法解析,看第一个关键字是什么。是 select 开头的,认为是读请求,走从库;是 insert、update、delete、replace 开头的,认为是写请求,走主库。如果是事务操作,里面即使全是 select,也要整条事务全部走主库,否则就会读到不一致的数据。
但这里有个隐蔽的问题,也是我在实战中踩过最深的一个坑——主从延迟。MySQL 的主从复制默认是异步的,主库写完 binlog 后,从库什么时候应用完这个 binlog,中间是有时间差的。如果业务上刚插入一条数据,立刻去查询,恰好这条查询被路由到了从库,而延迟还没消除,那就查不到,表现出来就是"数据丢了"。
网上很多人给出的方案是让业务层加"强制走主库"的标记,像 MyBatis 里的 @Transactional 注解里加 hints。这种方式可行,但对业务侵入太强。我当时的做法是在中间件层加了一个"最近写入表"的缓存机制——如果某张表在最近 500 毫秒内发生过写操作,那么针对这张表的读请求自动路由到主库。这样既保证了大多数场景的读写分离效果,又规避了延迟带来的脏读问题。这个思路在做中间件二次开发的时候非常值得参考。
2.2 分库分表的规则与路由逻辑
分库分表是 atlas 这类中间件最核心的卖点,也是技术含量最高的部分。在搞懂它的路由逻辑之前,得先明白一个基础概念:分库分表的本质是"按某个维度把数据分散到多个物理存储节点"。
以用户订单表为例。假设我们要按 user_id 的 hash 值分 4 个库、每个库 4 张表,总共 16 张分表。一条 SQL 进来,中间件需要做两件事:第一,解析出 SQL 里的分片键值;第二,用这个值做 hash 取模运算,算出应该路由到哪个库、哪张表。
这里的难点在于 SQL 解析。一条简单的select * from order_info where user_id = 123还算好办,因为分片键直接在 where 条件里。但如果是select * from order_info where order_no in (xxx, yyy)这种多分片键的查询,中间件就得把 SQL 改写成发往多个分片的语句,然后再把结果合并返回。如果查询里完全没有分片键,像select * from order_info where status = 1,那就只能做全分片广播,把这条 SQL 发给所有分片,最后汇总。
为什么业界一直强调分库分表之后要严格约束查询条件必须带分片键?原因就在这里。不带分片键的查询,代价是全部节点扫描,性能损失呈指数级放大。
2.3 连接管理与高可用设计
连接池这部分,很多人容易忽略,觉得就是个"复用连接的容器",真没必要为它单独设一个章节。但我可以负责任地说,生产环境里大量诡异的问题,根源都出在连接管理上。
举个真实案例。我们有一次线上服务突然大面积超时,排查了半天,最后发现是后端的 MySQL 实例做了主从切换,老连接全部失效,而中间件里的连接池没有及时感知到,还在向业务层提供这些已经断掉的连接。业务请求拿到坏连接,执行 SQL 时报错,部分框架会自动重试,重试又拿到新坏连接,形成雪崩。
atlas 在连接管理上做得比较好的地方在于它有主动探活机制。每隔一段时间会向后端发一个 ping,如果连续几次没有响应,就把这条连接标记为不可用,同时触发重连。但这套机制有几个参数需要调优,探活间隔不能太短,太短会放大后端压力;也不能太长,太长失效连接存的太久。我生产环境里一般设置 30 秒探活间隔,连续 2 次失败判定为失效,这个组合在稳定性和及时性之间比较平衡。
高可用方面,主库的自动切换依赖后端配合。中间件本身不做数据同步,它只负责在检测到主库不可用时,把读流量全部切到从库,并把写流量暂时拒绝或切换到新主库。这里的关键点在于,切换后旧主库重新恢复时,怎么避免脑裂(两个主库同时存在、同时接收写请求)。我的经验是,中间件的检测逻辑一定要配合 MySQL 半同步复制来用,并且要设置合理的切换阈值,不能因为一次网络抖动就触发切换,否则会产生比故障本身更大的问题。
2.4 Lua 脚本扩展机制
atlas 让我比较惊喜的一点,是它提供了基于 Lua 的脚本扩展能力。这个设计思路就有点像 Nginx 之于 Web 服务,通过嵌入一个脚本引擎,让使用者能在不改中间件核心代码的前提下,实现自己的定制逻辑。
举个例子。默认的读写分离规则是按 SQL 类型判断的,但我们有些特殊查询,比如报表类的统计 SQL,虽然也是 select,却非常消耗资源,我们想把它直接路由到专门的统计从库,而不是和业务读流量混在一起。这时候就可以写一段 Lua 脚本,匹配到特定的表名或者特定的 SQL 特征时,改写路由规则。
Lua 扩展带来的价值,不只是功能上的灵活。更核心的价值在于,它把"通用中间件"和"业务特殊需求"之间那条缝给填上了。没有这套机制的时候,你只能去 fork 源码改代码,然后自己维护一个跟社区不同步的分支;有了脚本扩展,你所有个性化需求都可以外置,中间件本体完全可以保持跟上游同步升级。
3. 实操部署与核心环节实现
3.1 环境准备与编译安装
先说说我们当时的生产环境。操作系统是 CentOS 7.9,MySQL 是 5.7 系列,中间件跑在一台独立的 4 核 8G 云主机上。为什么单独给中间件一台机器?因为它承载着所有数据库流量的转发,如果和其他业务应用混部,容易互相干扰。这个钱不能省。
atlas 基于 MySQL proxy 二次开发而来,安装方式可以拉源码编译,也可以直接用编译好的二进制包。我建议直接下载官方编译好的发行包,省时省力。下载下来后解压,目录结构大概分几个部分:bin 目录放着启动脚本,conf 目录放配置文件,lib 目录是依赖的库文件,log 目录用来存放运行日志。
这里有个小的注意事项:中间件运行需要 root 权限来绑定 3306 端口吗?并不一定。如果不想用 root 跑服务,可以修改配置文件里监听的端口,比如默认是 1234,改成 13306 之类的 1024 以上端口,就可以用普通用户运行了。从安全角度看,我建议始终用专门创建的低权限用户来运行中间件进程,而不是图省事直接塞进 root 的 crontab 里。
3.2 配置文件详解
配置文件是使用 atlas 的重中之重,就像 Nginx 的 nginx.conf 一样。一个最基础的主从分离配置大概长这样:
[mysql-proxy] # 中间件监听的地址和端口,业务侧连这个地址 proxy-address = 0.0.0.0:1234 # 管理接口的地址和端口,用于查看状态、下发指令 admin-address = 0.0.0.0:2345 admin-username = admin admin-password = admin@123 # 后端主库 proxy-backend-addresses = 192.168.1.10:3306 # 后端从库,可以配多个 proxy-read-only-backend-addresses = 192.168.1.11:3306,192.168.1.12:3306 # 主库和从库的账号密码 pwds = user:EncryptedPwd # 工作线程数,建议等于 CPU 核心数 event-threads = 4 # 日志级别 log-level = message log-path = /usr/local/atlas/log sql-log = ON配置里最容易踩坑的是pwds这项。它不支持明文密码,需要用自带的加密工具把密码加密后填进去。我第一次用的时候不知道这个机制,直接抄了个明文,结果进程怎么都不认账。所以大家在安装完第一件事,是先找到目录里提供的加密工具(不同版本位置不一样,一般在 bin 或 lib 目录下),生成加密串再填进去。
另外proxy-read-only-backend-addresses里配置的是只读从库节点。多个从库之间,atlas 会按照负载均衡策略自动分发读流量。至于分发的算法,默认是轮询,你可以在源码或扩展脚本里改成加权轮询。我们生产上有两台从库,一台性能强一台弱,低配的那台权重调低一点,避免因为响应慢拖累了整体延迟。
3.3 启动运行与验证
配置改好后,启动就很简单了。执行启动脚本,然后看两个地方来确认是否正常:一个是日志文件,另一个是进程状态。
# 启动 /usr/local/atlas/bin/mysql-proxy --defaults-file=/usr/local/atlas/conf/proxy.conf # 确认进程在跑 ps -ef | grep mysql-proxy # 确认端口在监听 netstat -lnp | grep 1234进程起来后,可以用 MySQL 客户端直接连中间件验证:
mysql -h 127.0.0.1 -P 1234 -u user -p能连上,说明中间件本身通了。再顺手执行一句select @@version,如果返回的版本号和直接连后端 MySQL 一样,说明转发链路也正常。
这里要特别提一点:初次验证的时候,千万不要用生产业务去测,先自己在命令行敲几条 SQL 确认读写分离和基本路由正确。我强烈的建议是建立一个专门的测试账号,权限只给到测试库,用这个账号来验证中间件功能。
那么怎么确认读写分离真的生效了?最简单的办法是在 MySQL 的 general log 里观察,或者在后端主库和从库上分别查看连接来源。如果你看到读请求都打到了从库,写请求都打到了主库,那就是配置生效了。顺着这条思路继续拓展的话,可以再配一台节点专门用来跑慢查询分析,把中间件 sql-log 开启,把所有经过中间件的 SQL 收集起来,定期做审计,这个价值在后期运维中非常大。
3.4 分库分表实际配置示例
分库分表的配置和读写分离的配置在同一个文件里,但需要额外指定分表规则。以我们当时的订单表为例,分片键选的是 user_id,hash 取模后分到 4 个库、每个库 4 张表:
# 分表规则,表名:分片键:分片数量 proxy-part-tables = order_info.user_id.16 # 分库规则,需要结合 SQL 里的库名前缀处理 proxy-db-tables = order_0.order_info,order_1.order_info,order_2.order_info,order_3.order_info上面的配置含义是,order_info 表按 user_id 分成 16 个分片(即 4 库 x 4 表)。中间件收到 SQL 后,解析出 where 条件里的 user_id,做 hash 取模得到 0~15 的编号,比如编号 5 对应 order_1 库里的 order_info_5 表,然后改写 SQL 并路由过去。
这里的难点在于,你写入数据时表名必须和分片规则对应,比如应用程序连的还是同一个逻辑库名,但中间件会改写实际表名。对于业务侧来说,你仍然写insert into order_info(...) values(...),中间件自动改写为insert into order_info_5(...)。这种透明性,正是中间件方案的魅力。
但透明归透明,业务侧还是要遵循一些规则。比如事务里的多条 SQL,必须确保所有 SQL 都命中同一个物理分片,否则就会出现跨分片事务问题。另外 join 查询如果涉及多张分片表,并且分片键不一致,那这个 join 就没办法在数据库层完成,中间件也不会帮你做 merge,因为成本和复杂度都太高了。
生产环境做分库分表,我个人的建议是:上线前先做充分的数据迁移方案和回滚方案,不要只盯着中间件本身的配置。我自己遇到过最痛的一次,就是分片规则配好了、数据也迁过去了,结果业务查询里有一类 SQL 没带分片键,在中间件层做了全分片广播,十几亿的数据量直接把所有节点打崩。这个事故之后,我们定了条硬规矩:新接入分表的查询必须带分片键,不带就直接在架构评审时打回。
4. 常见问题与排查技巧实录
4.1 连接报错:"Access denied for user"
这个错误应该是所有人第一次部署都会遇到的。排查时要区分两种不同情况:第一种是中间件和后端 MySQL 之间的认证失败,第二种是客户端连中间件时的认证失败。
如果是第一种,问题基本出在pwds配置上。刚才说了,这项配置需要加密后的密文,不是明文,检查一下是否用了加密工具生成、密文有没有填错。
如果是第二种,说明中间件本身没有配置客户端接入账号,或者配置了但权限不对。很多人在中间件上只配了一个后端 MySQL 连接串,但忘了配客户端接入账号。注意,中间件的用户体系是独立的,你需要专门配置哪些用户可以从客户端连上来,并映射到后端账号。
4.2 读写分离不生效,所有流量都在主库
这种情况的排查思路,先不要着急怀疑中间件,先自己确认一下测试方式是不是有问题。你是不是在一个事务里执行了查询?如果开启了事务,中间件为了保证一致性,会把事务内的所有 SQL 都路由到主库。这是预期行为,不是故障。
排除了事务因素后,再看是不是配置文件的格式问题。proxy-read-only-backend-addresses这项如果配错了,比如少写了端口或者写了多个重复地址,中间件可能直接把它忽略掉了。
还有一种容易被忽略的情况:SQL 语句大小写和前置空白。有些中间件对 SQL 的判断去掉了大小写,但个别版本有 bug,小写的 select 能识别,大写的 SELECT 识别不了,会误判为写操作。这种情况升级版本就好。
4.3 分表规则命中"查询全扫描"
前面提过,分片键缺失会触发全分片广播。但是在排查时,我们需要区分是"业务真的没传分片键"还是"中间件解析不出来分片键"。
为什么会有解析不出来的情况?看这条 SQL:select * from order_info where user_id + 1 = 100。从业务含义上看,user_id 是一个分片键,但它写在了一个表达式里面,解析器识别不出来。类似的还有where substring(user_id, 1, 3) = 'abc',这类对分片键做函数运算的写法,是中间件路由的大敌。
我们在制定开发规范时严格要求:分片键必须是等于某个具体值这种最简单的条件,不能有任何函数嵌套、不能有运算。连带规范是禁止对分片键做范围查询,除非你能接受全分片广播的代价。这些规范看着粗暴,但确实是从事故里总结出来的教训。
4.4 性能调优的一些心得
中间件的性能瓶颈通常不在 CPU,而在文件描述符、线程数和网络缓冲区。
先说文件描述符。中间件本质上是一个高并发的网络代理程序,每条连接至少占用一个 fd。如果系统的 ulimit 设置太小,连接数一上来就会出现 "Too many open files" 错误。我的建议是直接把系统的 nofile 调到 65535 以上,并且在 systemd 或启动脚本里也同步设置。
再看线程数。event-threads参数默认值很保守,如果机器是 8 核而你只配了 4 个线程,等于一半能力都在闲着。但也不是线程越多越好,因为线程切换和锁竞争也会产生开销。我做过几次不同线程数下的压测,得出的结论是:事件线程数设置成 CPU 核心数的 1~2 倍之间是最理想的区间,超过 2 倍后吞吐量不再上升,延迟反而会小幅恶化。
网络缓冲区影响的主要是大结果集的查询。如果业务经常要查比较大的数据量,建议调高 socket 的收发缓冲区大小,避免因为内核缓冲区不足导致的数据分片传输,这样可以显著降低大查询的延迟。此外还可以在系统层面调整 tcp_tw_reuse、tcp_max_tw_buckets 等参数,优化中间件和后端 MySQL 之间的长连接复用情况。
5. 从"能用"到"好用"的进阶建议
5.1 让中间件适配你的团队而非反向适应
很多人把中间件部署上线后,就觉得"万事大吉"了,但以我的经验来看,这恰恰是运维噩梦的开始。中间件是一个切入点,真正的工作量在后续的规范制定和流程建设上。
第一个要建的是数据库访问规范。哪些 SQL 允许直连、哪些必须走中间件、分片键的使用约束、事务的边界定义,这些都要以文档形式固化下来。我们团队内部流传一句话:中间件不管你是谁,只认你写的 SQL。如果开发同学写了一条不规范 SQL,线上出了故障,中间件不会替业务兜底。
第二个要建的是监控告警。前面提到 atlas 有管理接口,里面可以看到连接数、流量、路由情况等指标。我建议把这些指标全部接入公司现有的监控体系,比如 Prometheus。中间件自身的存活、与后端 MySQL 的连通性、连接池水位、平均查询延迟,这几个指标每一项都要配告警。尤其连接池水位,它像水库的水位一样是"温水煮青蛙"式的隐患——一开始很安全,慢慢涨到 80%、90%,突然某天流量高峰一来,连接池被打满,整个应用立马雪崩。
5.2 灰度发布与快速回滚
中间件一旦上线,影响面是"所有接入的业务",所以它的变更风险等级非常高。我们在生产上做了一个很有用的实践:把中间件集群分成两套,一套供内部系统和周边工具使用,另一套供核心交易链路使用。任何配置变更和版本升级,先在内部系统那套上跑 3 到 5 天,没有任何告警,再推到核心链路。
另外,回滚预案一定要提前准备好。配置文件本身是个文本文件,改动前先用 git 管理起来,留下历史版本。万一新配置有问题,直接切换回旧配置文件重启进程。整个过程最好能在 1 分钟内完成。
还有个很多人忽略的地方:中间件的版本升级前,一定要搭建一套一模一样的测试环境,把线上真实的请求流流量录制下来回放一遍。因为不同版本对 SQL 解析的行为可能有细微差别,比如表名大小写处理方式变了,某些历史 SQL 在新版本下可能会路由出错。一次版本升级引发大规模查询故障的事故,我在圈内可是听过不止一例。
5.3 扩展方向
如果你已经顺利把 atlas 用起来了,我建议继续往这几个方向做深。
一个是配合数据库运维平台做自动化的拓扑感知。比如新加一台从库,不用手工改中间件配置,而是从平台统一下发。另一个是把中间件的日志做结构化解析,自动提取出全量 SQL 的指纹、耗时分布、扫描行数,然后回传到开发效能平台,让开发同学自己在上面看到自己 SQL 的健康度评分。
还有一个方向是做多租户支持。如果你所在的公司有多条业务线,或者你是做 To B 的中间件服务平台,那就需要考虑按租户隔离连接池和路由规则。虽然 atlas 原生配置没有直接支持,但你可以结合它的 Lua 脚本扩展机制,在上层做一层多租户识别和隔离逻辑。
从长远角度看,如果团队对中间件的依赖越来越深,我会建议在 atlas 之上再封装一层高可用的接入服务——比如通过 keepalived 或者云负载均衡来暴露中间件集群的虚拟 IP,前端应用永远只连这个虚拟 IP,真正的中间件节点可以随时伸缩、重启、替换,而不让业务感知到任何抖动。这层加上之后,中间件才真正进入了"基础设施"的行列。
我个人在实际运维中的体会是,中间件这类组件,真正考验人的不是部署和配置,而是你能不能想清楚它的边界——它解决哪些问题,不解决哪些问题。atlas 能帮你把读写分离、分库分表的复杂度挡在业务之外,但它不负责数据一致性方案,不负责慢 SQL 优化,也替代不了合理的业务建模。把这些问题想透,你就能让中间件真正成为那根扛起并发压力的柱子,而不是成为又一个藏在数据库前面的定时炸弹。