最近在整理中间件相关的东西顺手把 Atlas 的调研笔记重新捋了一遍越捋越觉得这东西值得单独写一篇。很多团队分库分表第一时间想到的就是 ShardingSphere 或者 Cobar但 Atlas 这个由 360 开源的 MySQL 中间件在“轻量级分库分表 读写分离”这个场景下至今仍然有很强的参考价值。尤其是在你不想引入太重的基础设施、又不想被某个云厂商锁死的时候Atlas 的简单直接反而成了最大的优点。这篇文章我打算从 Atlas 解决什么真实问题开始一直讲到它的架构思路、路由原理、配置落地最后把我在实际部署和运维里踩过的坑一并列出来。如果你正在做数据层架构选型或者已经在用但有些细节没搞清楚这篇应该能帮你省不少时间。1. 项目整体认知Atlas 到底是什么它解决了什么问题1.1 数据量上来以后单库单表的真实困境先说一个很多团队必经的过程。业务早期一张 user 表、一个 MySQL 实例扛住几千 QPS 完全没问题。等到业务开始有起色数据量到了几千万行你会发现几个非常现实的问题第一单表查询越来越慢。即使建了索引B 树的层级变高磁盘 IO 压力变大很多查询从毫秒级退化到几百毫秒甚至秒级。第二写入吞吐量有上限。单实例的写入能力受限于磁盘和 CPU一旦有大批量导入或者高峰期写入innodb_log 和 binlog 的落盘压力会迅速放大。第三备份和恢复的时间越来越长单次物理备份动辄一个小时以上一旦实例挂了恢复时间直接影响业务。这时候你去问 DBA 怎么办答案无非就是分库分表、读写分离、缓存前置。但这些方案实施起来都有个共同的前提应用层要能感知到数据分布规则或者中间层要做透明的路由转发。Atlas 干的事情就是把这个逻辑收敛到数据库访问层让应用层感知不到背后的分片和主从结构。1.2 Atlas 在设计中解决的问题清单Atlas 是 360 在 MySQL-Proxy 基础上二次开发的产物核心定位是位于应用和 MySQL 实例之间的代理层。它帮你处理的事情有下面这几类分库分表根据预先配置的 sharding 规则把对逻辑表的 SQL 自动改写并路由到正确的物理库和物理表上。读写分离自动识别 select 和写操作把读请求分发到从库写请求只走主库支持多个从库之间的负载均衡。连接池管理对前端应用统一提供连接池避免应用直连数据库时因为连接数过多压垮实例。主从切换配合 ZooKeeper 做集群状态管理主库故障时可以自动把读写请求切换到新的主库做到数据库层面的高可用。SQL 过滤与基础安全支持拦截特定类型的 SQL也可以限制 ip 白名单提供基础的访问控制能力。这整套能力对业务代码几乎是透明的应用只需要把数据源地址从原来的 MySQL 地址改成 Atlas 的地址原来的 JDBC 连接串、MyBatis 或者 Hibernate 都不需要改动。对于我一个特别讨厌为了搞分片而把业务代码改得面目全非的人来说这个透明性是最对胃口的地方。1.3 它和 ShardingSphere、MyCat 的差异在哪里现在很多人选型时会把 Atlas 和 MyCat、ShardingSphere-JDBC 放在一起对比我直接说结论性的差异ShardingSphere-JDBC 以 jar 包的形式嵌在应用里不走独立代理模式。好处是性能损耗低SQL 改写都在进程内完成坏处是你每个应用都要引入依赖而且分片规则的变更需要重新发布应用。MyCat 和 Atlas 一样是独立代理进程但 MyCat 的功能更偏向分布式数据库网关对分布式事务、全局序列号这类能力支持得更完善。Atlas 的定位则更纯粹它就是针对 MySQL 主从复制场景做的读写分离和分库分表整个架构非常清晰部署也轻量。如果你有一个已经跑得很稳的 MySQL 主从集群只是需要一个轻量代理把读写分离和分片路由做掉Atlas 是性价比极高的选择。它轻到只有几个核心进程没有太多依赖也没有复杂的控制台理解起来比那些全家桶方案轻松得多。2. 架构原理拆解站在数据库前面的一层“智能网关”2.1 一条 SQL 请求的完整生命周期要理解 Atlas最直观的方式就是跟踪一条 SQL 请求从进入到返回的全过程。我上一套测试环境的时候直接在 Atlas 节点上抓包看完整链路整个请求处理流程大概是这样的客户端应用发起连接先是 TCP 握手之后做 MySQL 协议握手此时 Atlas 会扮演一个伪装的 MySQL 服务端向客户端发送自身的握手包。客户端以为自己在连数据库其实连的是 Atlas。然后客户端发送 SQLAtlas 收到后先做词法解析和语法解析这一步是为了搞明白这条 SQL 的类型、涉及的表、过滤条件、排序字段等关键信息。接着进入路由计算阶段。如果这条 SQL 命中了配置好的分片规则Atlas 会根据路由字段的值算出它应该落在哪个物理库的哪张物理表然后对原始 SQL 做改写把逻辑表名替换成物理表名再把改写后的 SQL 发到对应的后端 MySQL。收到结果后Atlas 可能还需要做简单的结果合并比如跨分片的 order by 和 limit 就需要在中间层做归并排序然后统一返回给客户端。整个过程对客户端完全透明你甚至可以通过 Atlas 看到后端实例的真实状态但应用层毫不知情。这也是中间件代理模式最大的优势数据库拓扑变化对应用零感知。2.2 连接管理的核心窍门前后端分离的连接池机制很多人第一次接触 Atlas 时会被它的连接管理方式绕一下我一开始也没完全转过弯来。Atlas 的连接管理是前后端分离的也就是前端连接客户端到 Atlas和后端连接Atlas 到 MySQL是独立的两个池子。前端连接池负责接收客户端的连接请求池子大小由配置项 client-ips 和 max-idle-connections 等共同控制。后端连接池则维护着 Atlas 到每一个 MySQL 实例的连接默认使用连接池模型来避免每次 SQL 执行都要新建连接。这样做最直接的好处是应用层的连接数压力全部由 Atlas 承接数据库只需要维护和 Atlas 之间的少量长连接即可不会被几千个应用连接瞬间打垮。这里有个细节值得注意Atlas 的后端连接是根据目标实例维度分池子的也就是说如果你有 1 主 2 从Atlas 至少会维持三组连接池。每组池子内部再按读连接和写连接做区分避免长事务把写连接占满了导致读请求也排队。这个设计看似朴素但在高并发场景下对数据库的连接保护非常有效。2.3 分库分表的核心路由逻辑Atlas 的分片逻辑是基于分片字段的哈希或范围映射来做的和多数中间件思路一致配置好分片键之后SQL 的 where 条件里必须带上这个字段才能路由到确定的物理分片。我举个例子假设逻辑表叫 order分片字段是 user_id设置了 4 个物理分片表 order_0、order_1、order_2、order_3。Atlas 在路由时会对 user_id 做哈希然后按照哈希值模 4 得到目标分片序号。如果 SQL 里的 where 条件根本没有 user_idAtlas 只能把这条 SQL 广播到所有的物理分片上去执行然后再做结果聚合。这个行为的开销非常大所以设计分片规则的时候一定要强制把分片字段带在查询条件里。路由的匹配过程并不只是简单地算哈希它还要处理很多边界条件。比如带引号的字符串和整型的比较方式不同、字符集不一致导致哈希结果不同、null 值的特殊处理等等。这些细节 Atlas 在底层都处理掉了但你如果直接去读后端日志看到某些条件下 SQL 跑偏了就要先从这些边界条件开始排查。2.4 读写分离与主从切换的实现机制读写分离的实现可以说是 Atlas 最省心的部分。它内部维护了一份后端实例状态表通过心跳检测不断更新主从状态。一条 select 进来时只要没有开启事务、没有使用 select ... for update 这类锁读语句Atlas 就会把它路由到某个从库上一旦开启事务后续全部走主库避免出现主从延迟导致读到旧数据。主从切换在这里要特别讲一下。很多初用 Atlas 的人会以为它做了切换后数据不会丢实际上 Atlas 的自动切换主要依赖 MySQL 主从复制状态和 ZooKeeper 的协调它做的事情是让后端连接池的流量不再打到已经不可用的旧主库而不是去伪造一个数据同步过程。如果旧主库和其他从库之间本来就有复制延迟切换后这部分延迟数据就是丢失风险需要业务层面或者备份机制兜底。所以我在生产环境里一直强调主从切换是故障转移不是数据恢复工具。切换本身要配合 MySQL 半同步复制来缩小数据丢失窗口而不是依赖 Atlas 自己去弥补复制延迟的缺口。3. 核心配置与实操要点从零搭建一个最小可用环境3.1 部署拓扑与前置准备在开始配置之前先规划一下最小可用的拓扑。我演示的环境是三台机器分别是一台 Atlas 节点、一台 MySQL 主库、一台 MySQL 从库。为了做高可用演示我又加了两个 ZooKeeper 节点但这个不是 Atlas 强依赖单机模式也可以跑我建议生产环境还是至少部署两个 ZooKeeper避免状态协调单点。具体的前置条件如下操作系统CentOS 7 或 Ubuntu 18.04 以上版本都行Atlas 依赖 libevent、glib 等基础库。MySQL 版本5.6/5.7/8.0 均可兼容但 8.0 需要特别注意认证插件问题这个后面我会单独讲。Atlas 二进制包官方 GitHub 上有编译好的 tar 包下载后直接解压到 /usr/local/atlas 就行。ZooKeeper用于集群模式的状态管理可以参考官方文档分配内存其实 2G 内存足够跑多个节点了。3.2 配置文件整体结构Atlas 的配置主要分成两个核心文件一个是控制服务自身参数的 main.cnf另一个是定义分片规则的后端映射配置文件通常叫 shard.xml不同版本文件名略有差异但结构相似。我贴一个典型的 main.cnf 片段方便你对照理解[mysql-proxy] admin-username admin admin-password 123456 proxy-backend-addresses 192.168.1.101:3306 proxy-read-only-backend-addresses 192.168.1.102:33061,192.168.1.103:33062 proxy-address :1234 admin-address :2345 daemon true event-threads 8 proxy-connect-timeout 3 proxy-read-timeout 10 proxy-write-timeout 10 log-level message log-path /usr/local/atlas/log这段配置里proxy-backend-addresses 是主库地址proxy-read-only-backend-addresses 是从库地址列表后面跟的 1、2 是权重表示从库之间的读负载比例。proxy-address 是 Atlas 对应用提供服务的地址admin-address 是管理接口的地址这两者要分开不要混用。event-threads 这个参数很容易被忽略它决定 Atlas 处理请求的线程数量官方建议设置为 CPU 核心数的 2 倍。我实测下来线程数开太高反而会因为上下文切换损耗性能所以还是按 CPU 来比较合理。3.3 分片规则配置的落地写法分片规则是 Atlas 配置里最需要细抠的部分。我先说明一下Atlas 的分片是按表级别来配置的也就是说并不是所有的表都需要分片你可以让一部分表分片一部分表不做分片直接路由到主库。下面是一个简化的分片规则配置示例shard schema nametest_db shard-tableorder shard-column nameuser_id typeinteger/ shard-method namehash param nameshard-count value4/ /shard-method physical-table nameorder_0 databasetest_db host-groupgroup_0/ physical-table nameorder_1 databasetest_db host-groupgroup_0/ physical-table nameorder_2 databasetest_db host-groupgroup_0/ physical-table nameorder_3 databasetest_db host-groupgroup_0/ /schema /shard这里有几个关键点。shard-column 必须和业务查询中最常见的条件字段保持一致如果这个字段选错了后面所有查询都会变成全分片广播性能直接崩掉。shard-count 必须和物理表的数量一致不然路由计算结果会落到不存在的表上。type 是 integer 时Atlas 会按整型值做哈希如果是 varchar就会先做字符到整型的映射这个映射规则要和业务侧约定的规则一致否则同样的字符串可能路由到不同分片。还有一个经常踩的坑是 range 和 hash 两种方式的取舍。hash 分片的好处是数据分布均匀但没有办法支持按时间范围做连续扫描。range 分片适合按时间归档的场景比如按月份分片但容易发生热点问题。像订单类数据我建议用 user_id 或 order_id 做 hash 分片然后再配合一个时间字段的二级索引来查列表这样既能分散热点又能保证范围查询不至于太慢。3.4 启动、验证和连接调试配置完成后第一次启动 Atlas 时我习惯先做一次前台启动验证方便直接看到日志输出/usr/local/atlas/bin/mysql-proxy --defaults-file/usr/local/atlas/conf/main.cnf如果配置文件有语法错误前台启动会在几秒内报错退出日志里会明确告诉你哪一行有问题。确认没问题后再把 daemon 设为 true让它后台运行。验证读写分离是否生效最简单的办法是在主库和从库上分别把 general_log 打开然后从客户端执行一条 select 和一条 insert。去从库的日志里应该能看到 select去主库的日志里应该能看到 insert。如果发现 select 也跑到了主库上大概率是哪里配置了强制走主库或者客户端在发 select 前先执行了 set autocommit0 之类开启事务的语句。管理接口的调试也值得说一句。通过管理端口连上 Atlas 后运行select * from backends;可以查看后端实例的状态online 表示正常offline 表示已摘除。这个命令在排查问题时非常好用建议收藏。4. 实战中的常见问题与排查技巧4.1 连接数满了怎么办我遇到的第一类高频问题就是连接数被打满。如果你没在 Atlas 层面做连接池控制那么应用侧每个连接都会对应 Atlas 到 MySQL 的一条后端连接当应用实例扩容到了几十个节点每个节点又有几十个连接池连接时后端的 MySQL 连接数很快就会触顶。排查思路是先用管理接口看后端的连接分布确认是哪个实例的连接数异常。然后区分两种情况一种是应用侧连接根本没有复用每次新建连接这种问题要去应用侧修连接池配置另一种是 Atlas 后端连接池的 max-idle-connections 设置过小导致频繁新建连接这种问题就需要调大后端连接池的空闲上限。我通常会把 Atlas 后端的连接池调得比前端的连接池略大因为一个后端连接可能被多个前端连接短时间交叉复用太小了容易抖动。另外可以在应用侧强制设置一个合理的连接最大生命周期避免连接长时间不断开导致 MySQL 侧出现大量 TIME_WAIT。4.2 跨分片查询导致 limit 结果不准第二个非常容易踩的坑是分片场景下的 limit 问题。假设你要查每个分片的前 10 条数据Atlas 会对每个分片都执行 limit 10然后汇总后排序再取前 10看起来没问题。但如果你的需求是跨全部分片取全局前 10而各个分片内部已经按时间倒序排好了直接在每个分片取前 10 再合并很有可能出现漏数据的情况。举个例子分片 0 里前 10 条都是昨天的数据分片 1 里前 10 条是今天的数据你要的全局前 10 按时间排序应该全部来自分片 1。但 Atlas 广播到分片 0 时已经把昨天的数据取出来了这些数据在全局排序时会被丢弃最终结果其实是正确的因为 Atlas 会在归并时排序。真正有问题的是如果你在每个物理分片内部先做了 group by 或者子查询的分页在这种嵌套查询的场景下Atlas 无法保证每个分片取出来的数据覆盖全局分页的所有情况。这个问题的解法很朴素业务侧不要试图用简单的 limit 跨分片做深度分页而是把分页改成基于游标的方式。比如记录上一页最后一条数据的 id 和时间戳下一页查询时用 where 条件把这条数据过滤掉然后每个分片只取少量数据归并即可。这样既避免了深分页的损耗也不会出现结果错乱。4.3 主从切换后写入失败这个坑是怎么绕过去的Atlas 的主从切换依赖 ZooKeeper 做协调但很多人配置的时候只配了 Atlas 到 MySQL 的连接忘了在 ZooKeeper 上注册对应的回调。我遇到过的情况是MySQL 主库宕机后Atlas 管理端已经可以看到新主库但业务端的写入还是持续报错。排查之后发现原因在于 ZooKeeper 上用于主从切换的状态机没有成功触发因为 Atlas 默认的切换逻辑是“检测到主库不可用后自动把一个从库提升为主库并且把 proxy-backend-addresses 指向这个新主库”。但如果 ZooKeeper 的连接串写错或者权限不足这个切换动作就无法完成。如果不想依赖 ZooKeeper 做自动切换也可以把主从切换逻辑放到外部管理平台比如脚本定期检测主库状态检测到异常后自动修改 Atlas 的配置并重启服务。这种方式简单粗暴但需要保证重启过程中连接中断的业务是可接受的。我的建议是如果你的集群规模不大先手动切换将 Atlas 配置里的写地址改到新主库然后平滑重启验证最后再上自动化脚本。别一上来就全自动出了问题很难定位。4.4 MySQL 8.0 的兼容性坑如果你用的是 MySQL 8.0我强烈建议先验证一下 Atlas 和后端 MySQL 的认证方式是否兼容。8.0 默认的 caching_sha2_password 认证插件在 Atlas 的老版本中是不支持的导致连接 MySQL 8.0 时一直报认证失败。解决办法有两种。一种是在 MySQL 侧创建一个使用 mysql_native_password 插件的账号给 Atlas 连接用CREATE USER atlas_user% IDENTIFIED WITH mysql_native_password BY your_password; GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO atlas_user%;另一种是下载新版本的 Atlas 编译包官方在后续版本中已经补齐了对 caching_sha2_password 的支持。我建议能用新版本就用新版本别在旧版本上硬扛。除了认证MySQL 8.0 对 sql_mode 的默认配置更严格Atlas 在改写 SQL 时如果出现了ONLY_FULL_GROUP_BY不允许的语句执行会直接报错。遇到这种问题优先检查业务 SQL 是否符合规范而不是为了绕过而把 sql_mode 调宽松否则以后升级数据库还是要踩坑。5. 执行性能调优和运维监控要点5.1 性能瓶颈的定位方法Atlas 本质上是一个转发代理所以它性能消耗主要在三个方面SQL 解析、路由计算、结果合并。在多数场景下SQL 解析和路由计算是很轻量的瓶颈往往出现在大结果集的归并排序和跨分片的 join 上。我习惯用三种手段定位性能瓶颈打开 log-level 为 debug观察每条 SQL 在 Atlas 内部的处理时间注意把日志级别调回 message否则日志量会让你崩溃。在前后端分别抓包对比时间戳如果前端收到请求到后端发出请求间隔很大说明瓶颈在路由计算如果后端返回后前端迟迟不回包瓶颈就在结果合并。用状态监控命令查看每个后端连接的 active 数量如果某个分片的连接长期处于 active 状态说明这一侧的 SQL 偏重需要从分片键或者 SQL 本身去优化。5.2 对分片键设计的再次强调运维做久了你就会意识到分片键的设计决定了 Atlas 的效能上限。我见过太多团队在分片键上随意选一个字段结果上线后大量查询变成全分片广播性能比不分片还差。判断分片键是否合理可以看这几个指标分片键的基数是否足够大、是否稀有到大部分查询都带着它、以及不同分片上的数据量是否均匀。如果这三个指标有一个不达标建议趁早换分片键。分片键一旦在线上跑起来再想换代价就非常大因为你不仅要把历史数据重新分布还要在 Atlas 规则上做灰度切换。5.3 监控指标建议Atlas 自带的监控信息比较朴素但它提供的 TCP 管理端口可以查询到关键状态。我会把这些状态定期采集到监控平台后端各实例连接数防止某个实例连接数倾斜。前后端连接池的活跃比例用于判断是否需要扩容连接池。SQL 路由分布如果大量 SQL 走了 broadcast 路由说明分片键命中率在下降。主从复制延迟虽然这个是 MySQL 层面的指标但它直接影响 Atlas 的读写分离正确性。如果 Atlas 前端还有一层 LVS 或云负载均衡还要把 Atlas 节点的健康检查加上避免单个 Atlas 节点挂着但负载均衡还在继续分发流量。6. 总结一些沉淀下来的经验最后聊几句偏个人经验的东西。Atlas 不是万能的它不像 ShardingSphere 生态那样有一堆分布式事务方案也没有完整的全局唯一 ID 生成这让它在很多互联网新项目里显得不够“性感”。但正因为功能克制它的稳定性反而很好掌握只要把分片键设计好、主从复制监控到位它能以非常低的运维成本支撑住相当规模的业务。我在实际使用中最满意的点是它对应用代码的侵入性几乎为零。有一次我们做数据层迁移应用侧只需要改一个数据库连接地址发布过程中连代码都不用重建。对于随时可能面临重构压力的业务团队来说这种透明性带来的安全感是实打实的。如果你正处在分库分表选型的初期我建议先把 Atlas 的源码和设计文档过一遍不用急着定方案。把分片键选好、读写分离理解透再去对比其他中间件你会发现很多高级功能的底层思路其实都是相通的。后面我可能还会写一篇关于 Atlas 和 ShardingSphere 的对比压测记录如果你也在调研这块可以持续关注。