MySQL中间件Atlas深度解析:读写分离与分库分表实战指南
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.9MySQL 是 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 admin123 # 后端主库 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_idhash 取模后分到 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 分表规则命中查询全扫描前面提过分片键缺失会触发全分片广播。但是在排查时我们需要区分是业务真的没传分片键还是中间件解析不出来分片键。为什么会有解析不出来的情况看这条 SQLselect * 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 优化也替代不了合理的业务建模。把这些问题想透你就能让中间件真正成为那根扛起并发压力的柱子而不是成为又一个藏在数据库前面的定时炸弹。