☰
Atlas数据库中间件实战:读写分离与分库分表部署踩坑全记录
2026/10/19 13:09:22 网站建设 项目流程

做后端和数据库运维的同学,大概率都经历过这么一段:业务日活两万的时候,单库单表稳如老狗,等某次活动一上线,读写比直接冲到十几比一,主库 CPU 告警一个接一个,慢查询日志刷得飞快。我接手这套系统时就是这么个局面——订单库单表几千万行,主库带宽被打满,从库只有一台还在同步,时不时延迟几分钟。当时能选的方案不少:代码层做读写分离、上 MyCat、切 ShardingSphere,或者部署一个专门做数据库代理的中间件。Atlas 就是我最终选定的那一个。

Atlas 是 360 开源的一款数据库中间件,基于 MySQL-Proxy 0.8.2 深度改造,核心能力是读写分离、分库分表、连接管理、IP 过滤和 SQL 黑名单。它的使用姿势很简单:应用层的 JDBC 地址从 MySQL 实例改成 Atlas 地址,SQL 逻辑不用改,路由规则全部写在配置文件里。当时我做选型时最看重的一点就是它足够轻,部署成本低,介入业务层少,非常适合中小团队先解决“读库被压垮”这个最痛的问题。这篇文章我会把从选型、部署、配置、压测到踩坑的全过程,按照我当时实际操作的顺序完整还原一遍。如果你是正在纠结要不要上中间件、或者已经选型 Atlas 但部署时遇到问题的同学,这篇应该能帮你少走不少弯路。

1. 业务量上来之后,为什么我会在一堆中间件里盯上 Atlas

1.1 我们当时遇到的压力曲线

先说说背景。那套系统主营业务是 B 端订单和用户账户,QPS 不算夸张,峰值大概一千多,但有两个硬伤:第一,订单表单表数据量到了几千万行,二级索引经常失效,慢查询一抓一把;第二,业务是典型的读多写少,读占比超过 90%,数据库连接数被应用实例吃得很紧。而且当时主库已经扛了大部分读流量,binlog 同步经常被查询拖慢,主从延迟从几十秒一路飙到十几分钟。

把问题拆开看,其实是两层:一层是单表数据量过大导致的查询效率问题,另一层是读写没有分离导致的单点压力问题。表数据量大可以靠分库分表解决,但直接上分库分表风险很高,因为业务 SQL 里带 join、带子查询,贸然拆表会引发大量兼容问题。而读写分离是成本最低、见效最快的一步——只要把读流量从主库摘走,主库压力就剩下写和实时读,整个链路的稳定性立刻不一样。

1.2 Atlas 解决的核心问题清单

Atlas 覆盖的需求点很明确,这里我列一个当时评估用的对照表:

需求是否解决实际体验
读写分离支持SELECT 自动走从库,写操作自动走主库,业务无感
多从库负载均衡支持多个从库之间按轮询方式分发读请求
分库分表基础支持能做简单的 hash/range 分表,复杂 join 需要业务规避
连接池复用支持前端业务连接与后端 MySQL 连接解耦,降低连接风暴风险
IP 白名单支持可限定来源 IP 访问,减少暴露面
SQL 黑名单支持能拦截无 where 的 update/delete 等危险 SQL
主从切换不支持主库故障时需要外部介入,靠 HA 方案补齐

从这张表能看出来,它更像一个“轻量数据库网关”,而不是全家桶式的中间件。正因为职责边界清晰,部署和运维才简单,这是我在选型阶段最看重的一点。

1.3 选型时为什么没有直接选 MyCat 或 ProxySQL

我不是没考虑过其它方案。MyCat 功能全面,分库分表、全局序列、分布式事务都有,但当时我们团队对 Java 体系的中间件运维经验不足,而且 MyCat 相对重,光配置文件就要理解很久。ProxySQL 做读写分离和查询路由很强,但它不擅长分片,对于我们的单表膨胀问题帮助有限。ShardingSphere 需要改业务数据源或引入客户端依赖,当时业务方没有太多人力配合做代码改造。

Atlas 的优势在于代理模式,业务无侵入,部署起来就像装一个服务,改一下数据库地址就能接入。而且它是 C 实现的,单机代理性能在普通物理机上能支撑几千 QPS,对当时我们这种规模绰绰有余。当然,后来我也知道 Atlas 的维护节奏放缓了,但在“先解决读瓶颈”这个目标下,它当时确实是性价比很高的选择。

2. 一条查询从进入 Atlas 到返回结果集的完整流转链路

2.1 Atlas 把自己伪装成了谁

理解 Atlas 工作机制,先要理解它在链路中的位置。对应用层来说,Atlas 表现得像一台 MySQL 服务器,监听一个端口,业务用 JDBC 连上来,发的是标准 MySQL 协议;对后端 MySQL 实例来说,Atlas 又像一个 MySQL 客户端,它自己去连接主库和从库,把 SQL 按规则转发过去。这种“两边都不露馅”的设计,最大好处是业务侧零改动。

连接过程大致是:业务发起 MySQL 握手协议,Atlas 收到后做鉴权,通过则在前端维持这条连接;Atlas 内部维护一个到后端 MySQL 的连接池,按需分配后端连接执行 SQL。所以业务侧连接数再多,到了 Atlas 这里会被收敛成有限的池化连接,这也是它缓解 MySQL 连接数压力的关键。

2.2 读写分离的判定逻辑

Atlas 判定一条 SQL 走主库还是从库,核心是看 SQL 语句的类型。像 SELECT、SHOW 这类读语句,默认分发给从库;INSERT、UPDATE、DELETE 这类写语句,始终走主库。这个逻辑听起来简单,但实际场景里有些边缘情况需要特别注意,我后面踩坑部分会单独讲。

更准确地说,Atlas 会对 SQL 做一层词法解析,识别语句开头的关键字,然后按照预设路由规则分发。但它不会深入理解 SQL 背后的业务语义,所以在事务中执行读操作时,如果事务状态已经被标记为“需要主库”,那么事务内的所有语句都会保持在主库执行,避免出现读到事务外数据的逻辑错乱。实际使用中,我总结了一条经验:不要指望中间件能智能识别业务一致性要求,高一致性读要么走主库,要么在事务里完成,这才能在逻辑上自洽。

2.3 分库分表的路由与结果合并

分库分表是 Atlas 相对复杂的能力。它的做法是:在配置里定义分表键和分表规则,比如按用户 ID 取模分成 16 张表,或按时间范围分表。SQL 进来时,Atlas 解析出分表键的值,再通过规则计算目标表,最后把 SQL 改写成实际表名转发到对应后端。

这里有一个容易忽略的问题:中间件做分表后,跨分片的 JOIN、子查询、聚合查询就没法在数据库层直接完成了。Atlas 不会自动做复杂的分布式 JOIN,实际工程里我们也是尽量把查询按分片键收敛到单分片,或者由业务在应用层做二次聚合。换句话说,分库分表不是随便分一下就完事,它要求业务 SQL 先按照分片思路进行改造,中间件只是辅助,不能替业务扛下所有复杂度。

2.4 连接管理与 thread 模型

Atlas 的线程模型沿用了 MySQL-Proxy 的 event 驱动思路,使用 event-threads 控制处理线程数。这个参数不能拍脑袋设,太小会浪费机器性能,太大又会增加上下文切换开销。我当时的经验是,先按 CPU 核数设置一个初始值,比如 8 核机器先设 8,然后压测观察 CPU 和连接响应时间,再逐步上调或下调。连接管理上,它有一个 keepalive 机制,相当于定时向后端连接发送心跳,避免空闲连接被 MySQL 服务端清理掉。这个机制在实际运行中非常重要,后面踩坑里我会提到它和 MySQL 参数打架的场景。

3. 从零部署 Atlas 的完整操作记录

3.1 环境准备与版本选择

部署 Atlas 之前,先把底层环境理清。Atlas 发布较早,对 MySQL 8.0 的兼容性并不好,如果你后端是 MySQL 8.0,建议先把数据库认证插件调整为 mysql_native_password,或者考虑其它方案。我当时的集群是 MySQL 5.7 主从架构,主库一台,从库两台,操作系统是 CentOS 7 x86_64,Atlas 直接选择对应版本的二进制包。

下面这两件事是我每次装 Atlas 都会强制自己先做掉的:

  • 确认主从复制状态正常,SHOW SLAVE STATUS里的 Seconds_Behind_Master 已经归零。
  • 准备好专用的业务账号和只读账号,最小权限原则,避免 Atlas 拿去用的账号权限过大。

如果主从都没搭好,再折腾中间件就是空中楼阁,这点一定不要跳步。

3.2 安装步骤与目录结构

安装很简单,下载对应平台的 RPM 或二进制包后,执行 rpm 安装或解压。装完之后,核心目录是这样的:

/usr/local/mysql-proxy/ ├── bin/ │ └── mysql-proxy # 主程序 │ └── encrypt # 密码加密工具 ├── conf/ │ └── test.cnf # 默认配置文件 ├── lib/ └── log/

启动命令我习惯指定配置文件,方便一台机器上起多个实例时区分:

/usr/local/mysql-proxy/bin/mysql-proxy \ --defaults-file=/usr/local/mysql-proxy/conf/test.cnf \ &

装完后先不要急着接业务,跑一遍自测(后面会讲)确认基础能力正常,再切流量。

3.3 test.cnf 核心配置逐项解释

Atlas 的配置集中在 test.cnf,核心配置长这样:

[mysql-proxy] admin-username = admin admin-password = admin_pass proxy-address = 0.0.0.0:2345 proxy-backend-addresses = 192.168.1.10:3306,192.168.1.11:3306,192.168.1.12:3306 pwds = biz_user:这里填encrypt生成的口令 daemon = true keepalive = true event-threads = 8 log-level = message log-path = /usr/local/mysql-proxy/log

几个关键项我逐个说明:

proxy-address是 Atlas 对业务开放的地址和端口,业务 JDBC 就连接这个地址。proxy-backend-addresses填写后端 MySQL 实例列表,第一个地址会被视为写主库,其余是读从库,这个顺序很重要,不能写错。pwds是业务账号的口令配置,注意这里不要直接写明文,而是用安装目录下 bin/encrypt 工具生成密文。daemon表示以后台守护进程方式运行,keepalive开启连接保活,event-threads刚才说过,根据 CPU 核数初始设定。

这里要特别提醒:proxy-backend-addresses里主库顺序如果写错,会出现写流量被发到从库,后果非常严重。我习惯在配置里加注释说明哪个是主哪个是从,避免同事误改。

3.4 启动、验证与平滑重启

配置写好后就可以启动了。启动之后第一步,看端口有没有正常监听:

netstat -lnp | grep -E '2344|2345'

2345 是业务端口,2344 是管理端口。确认端口在听之后,先登录管理端口看后端状态:

mysql -h127.0.0.1 -P2344 -uadmin -padmin_pass -e "SELECT * FROM backends;"

正常情况下列出来的主库和从库状态都是 up。然后再用业务账号连一次业务端口:

mysql -h127.0.0.1 -P2345 -ubiz_user -p'password' -e "SELECT 1;"

基础连通性没问题后,最有价值的一步是验证路由到底对不对。打开后端 MySQL 的 general_log,或者在从库上执行SHOW PROCESSLIST,然后从业务端发一条 SELECT,观察请求是否真的落在从库;再发一条 INSERT,观察是否落在主库。这一步我每次做都会有收获,很多时候你以为配置没问题,实际路由结果和预期完全相反。

修改配置后,可以平滑重启 Atlas 让配置生效,不需要粗暴 kill 进程。用法大致是给 mysql-proxy 进程发送 HUP 信号,或者使用自带的 reload 参数,具体命令根据不同版本略有差异。平滑重启的好处是不中断业务连接,对在线的读写链路影响很小。

4. 生产环境里 Atlas 真实踩过的坑,以及我的排查过程

4.1 FOR UPDATE 被误路由到从库,锁形同虚设

先说一个让我印象极深的坑。某个抢单接口上线后,发现并发场景下出现库存超扣,起初怀疑业务事务没加锁,后来翻代码发现 SQL 写的是SELECT ... FOR UPDATE,在可重复读隔离级别下这应该能锁住行。但诡异的是,锁完全没生效。

排查过程很有意思。我先去从库的 processlist 看了一眼,发现这个带 FOR UPDATE 的查询居然出现在从库上。也就是说,Atlas 把它当成普通 SELECT 分给了从库。加锁读必须在主库执行才有意义,从库的锁既不影响主库写入,也无法阻止其它事务提交,等于这个锁被中间件“温柔地”消解掉了。

解决办法是让这类语句强制走主库。Atlas 这类中间件一般支持在 SQL 里加注释或关键字来强制路由,比如在语句里带上主库标记注释,或者把加锁读放入事务并确保事务内的读不被拆走。这次之后我定了一条规矩:所有带FOR UPDATE、LOCK IN SHARE MODE的语句,统一在 SQL 文本里加显式标记,同时 Code Review 阶段就要把关,不允许业务在无标记情况下裸写加锁读。

4.2 主从延迟导致用户读到“旧订单”

第二个坑和主从延迟有关。用户下单后跳转支付结果页,页面偶发显示“订单未支付”,过几秒刷新就正常了。我一开始怀疑是缓存问题,排查后发现是主从延迟:订单写主库后,结果页的查询走了从库,而从库还没同步到最新状态,所以读到的是旧数据。

这个问题本质上不是 Atlas 的 bug,而是读写分离架构下的经典问题。中间件不会感知主从延迟,也没有能力决定哪条读必须实时,这个判断只能业务来做。我当时的处理分了三层:

  • 强一致的读(支付结果、订单状态)放到事务里,或让数据源强制走主库。
  • 允许最终一致的读走从库。
  • 中间态数据增加 Redis 缓存,用缓存过期和失效策略降低延迟影响。

后来我还加了一个兜底策略:对核心订单状态查询,如果从库返回的状态和预期不符,应用层做一次二次校验或短暂重试。这个举措不能根治延迟,但能把用户可感知的错误率降到很低。

4.3 长连接被服务端断开,凌晨连接数像过山车

第三个坑是连接管理相关。某个凌晨,监控平台突然报警 MySQL 连接数冲到几千,然后过一会儿又掉下来,第二天查找时发现 Atlas 日志里出现大量“backend connection closed”的记录,同时后端 MySQL 的错误日志里有大量 aborted connections。

排查链路是这样的:先看时间点,发现问题集中在凌晨低峰期,正好是 MySQL 的 wait_timeout 周期节点。MySQL 默认 wait_timeout 是 8 小时,空闲连接超过这个时间会被服务端主动断开。Atlas 虽然有 keepalive 保活机制,但如果 keepalive 间隔大于服务端超时时间,或者连接被网络设备清理,后端连接池里就会出现大量半死连接。等到白天流量起来,Atlas 重新建立连接,建连风暴瞬间把连接数打满。

处理方案有三条腿走路:把 MySQL wait_timeout 调大,缩小 Atlas keepalive 间隔,同时把应用连接池的 validationQuery 打开,确保拿到连接时先做废弃连接检测。三个参数配合调整之后,这个坑才算彻底填平。后来我把这套参数检查加进了部署检查清单,每次搭新环境都先核对一遍。

4.4 SQL 黑名单误伤与漏网,规则要颗粒化

Atlas 的 SQL 黑名单是个好用但容易出问题的功能。我当时希望靠它拦截无 where 的 UPDATE、DELETE 这类高危 SQL,于是配置了一些宽泛规则。结果上线没多久,业务方反馈某个定时任务的批量更新失败了。排查发现 SQL 确实没有 where 条件,但它用的是批量更新并且通过主键列表更新,属于合法操作,被我那条“拦截无 where 更新”的黑名单规则一刀切了。

这次教训让我调整了配置策略:黑名单规则不能只看“有没有 where”,还要看有没有 LIMIT、有没有主键约束、是不是明显全表更新。规则的颗粒度越小越安全,宁可多配几条精确规则,也不要一条大正则误伤业务。另外,黑名单改动后一定要在测试环境跑一轮核心业务回归,把误伤的代价前置到上线之前。

4.5 前端有 LVS 时,IP 白名单直接失效

还有一个部署层面的坑:Atlas 支持 IP 白名单,我原本打算限制只允许应用服务器访问,结果配置后业务全部连不上,数据库访问日志里来源 IP 全是 LVS 的虚拟 IP。原因很简单,业务到 Atlas 的链路中间加了 LVS 做负载均衡,源地址被 NAT 成了 VIP,Atlas 看到的自然全是同一个地址。

解决方案是让 LVS 开启透传客户端真实 IP 的功能,或者在 Atlas 白名单里放行 LVS 转发网段。这个坑虽然不深,但如果没意识到是 LVS 的转发机制,很可能在排查链路里绕很久。

5. Atlas 与 MyCat、ShardingSphere、ProxySQL 的定位差异

5.1 四种方案的形态对比

很多人把数据库中间件混为一谈,实际它们的定位差异很大。我按自己用下来的理解列了一张对比表:

方案形态核心能力运维成本适合阶段
Atlas数据库代理读写分离、基础分表低中小团队快速解决读瓶颈
MyCat数据库代理分库分表、全局序列、分布式事务中需要强分片能力的 Java 体系
ShardingSphere-JDBC客户端增强分片、读写分离、分布式事务中应用侧可改造、链路可控
ProxySQL数据库代理查询路由、读写分离、连接池低读多写少、规则复杂的流量治理

从开发语言上看,Atlas 是 C 实现,MyCat 和 ShardingSphere 是 Java 生态,ProxySQL 是 C++。Java 生态中间件胜在功能丰富,社区活跃;Atlas 和 ProxySQL 则更像“基础设施”,稳定、安静、不需要太多业务配合。

5.2 分布式事务与全局序列的取舍

涉及分库分表后,最头疼的是分布式事务和全局序列。这块 Atlas 能力很弱,基本靠业务自己想办法,比如用应用层事务消息、唯一 ID 生成器来兜底。MyCat 和老版本 ShardingSphere 对分布式事务支持也比较简化,想做得严谨需要额外接 Seata 这类事务框架。ProxySQL 不走分片路线,所以基本不涉及分布式事务问题。

回顾我们的选择,Atlas 没有全局序列和强分布式事务能力,反而让我在设计阶段就刻意避开了跨库写事务,所有写操作都按用户维度收敛到单库单表,从根上规避了分布式事务难题。这也算是一种“能力不足带来设计约束”的思路。

5.3 什么时候选 Atlas,什么时候坚决不选

总结一个选型判断标准。如果满足以下条件,用 Atlas 会很舒服:

  • 核心痛点是主库读压力大,MySQL 主从架构已经搭好。
  • 业务 SQL 相对规整,不需要跨库 join。
  • 团队没有太多精力伺候 Java 中间件。
  • 暂时不需要分布式事务和自动主从切换。

反过来,如果业务已经明确要做多分片、全局二级索引、弹性扩缩容,或者对分布式事务有硬性要求,那就不要考虑 Atlas 了,直接上 ShardingSphere 这类更完整的方案。选型最怕的不是选错工具,而是拿着“够用”的工具硬扛“不够用”的场景,最后把系统搞得不伦不类。

6. 如果让我重新做一次数据库架构改造,我会怎么用 Atlas

6.1 接入顺序:先读写分离,再考虑分表

按我的经验,中间件改造最忌讳“一步到位”。第一次接入 Atlas 时,先把读写分离拉开,让主库只承担写流量和强一致读,观察两周。这个阶段主要看三件事:主库 QPS 是否明显下降、从库负载是否均衡、业务侧有没有因为主从延迟出现新问题。

确认读写分离稳定后,再评估是否要做分表。分表前必须先做数据迁移方案,比如按用户维度 hash 分片,提前想好数据回填、增量同步、回退预案,而不是直接改中间件配置把表拆了。我曾经见过一个团队分表后业务大量报错,原因是分片键在业务查询里根本不存在,所有查询都变成全分片扫描,性能比单表还差。分表这件事,一定要让业务查询模式先“适应分片”,而不是让中间件硬扛。

6.2 监控指标与告警项

Atlas 上线后,监控是最后一道防线。我整理了一下当时实际在用的监控项,分三层来看:

  • 主机层:Atlas 所在机器的 CPU、内存、文件句柄数、网络连接数。代理是长连接服务,文件句柄打满会导致新连接全部失败。
  • Atlas 层:通过管理端口查询后端节点状态,看 backends 表里主从是否都在线;关注连接数、请求量、路由失败次数。
  • MySQL 层:主从延迟 Seconds_Behind_Master、慢查询数、锁等待数、连接数。

这些指标不需要全部堆在一起,只要把“连接数”“主从延迟”“路由失败次数”这几项配上即时告警,就能覆盖大部分故障场景。

6.3 快速排障的工具箱

真出问题时,按这个顺序排查通常最快:

  1. 先看 Atlas 端口是否存活,业务 JDBC 能不能握手成功。
  2. 登录管理端口查 backends 状态,确认主从节点是否 up。
  3. 看 Atlas 日志和 MySQL general_log,确认 SQL 实际落在哪个节点。
  4. 确认是不是主从延迟导致的读旧数据。
  5. 确认是不是 SQL 语法兼容问题,尤其注意存储过程、批量操作、特殊注释。

排查过程中,TCP 抓包是一个非常有效的武器。有一次我抓包后发现业务 JDBC 发出了一些奇怪的 SET 语句,Atlas 把它们也当作写操作路由到了主库,导致主库多了一堆无效负载。这种问题看代码和配置不一定能发现,但抓包一看就明明白白。

6.4 退出策略:向更复杂方案迁移的过渡方案

技术演进是必然的。Atlas 解决的是“读瓶颈”和“基础分表”,如果业务走向复杂化,比如需要分布式事务、全局索引、自动故障切换,就要考虑迁移到 ShardingSphere-JDBC 或 ProxySQL 这类方案。迁移时不能一把梭,我的建议是先做数据访问层封装,把数据源切换做成配置开关。这样 Atlas 和新方案可以共存,流量按接口维度灰度切流,切过去之后保留回退开关,观察几天没问题再彻底摘掉旧连接。

这个过程中还有两件事要做:把 Atlas 里积累的路由规则、SQL 黑名单规则整理成文档,对照迁移到新方案,避免规则丢失;把业务侧所有带强制主库注释的 SQL 检查一遍,确认新方案能正确识别这些标记。迁移不是换一个组件那么简单,实际上是换一套路由规则体系,规划越细,风险越可控。

6.5 最后再分享一点我的体会

技术选型没有银弹。Atlas 这两年确实不够活跃了,我后来也因为业务复杂度升级,逐步把一部分核心链路迁到了其它方案。但直到今天,我仍然觉得 Atlas 在“中小团队、MySQL 主从、核心痛点是读瓶颈”这个场景里是极具性价比的选项。它不完美,但它在那个阶段实实在在顶住了流量,也为我理解数据库中间件打下了基础。最后说一个小习惯:任何中间件接入前,我都会先在测试环境用 TCP 抓包把 SQL 流向确认一遍再上生产,这个习惯帮我避免了很多次想当然的翻车。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询