
聊到数据库中间件很多人第一反应是 Mycat、ShardingSphere、ProxySQL但在我职业生涯里真正打交道最久、踩坑最多的反而是 Atlas——对就是奇虎 360 开源的那个 C 语言版 MySQL 代理。今天想把这几年在线上用 Atlas 的经验完整梳理一遍从部署、分表规则到事故排障一次性把该说的坑都说清楚。如果你正在维护一套底层还是 MySQL 5.7 的系统既要读写分离、又想顺手把单表拆小一点这篇文章应该能帮你少走不少弯路也能让你判断 Atlas 到底适不适合你现在的架构。1. 拿 Atlas 解决的问题是什么1.1 业务代码里散落的“伪中间件”我接手这套系统的时候读写分离和分表逻辑是散落在业务代码里的每个服务自己配了两个数据源读接口连从库写接口连主库订单表做了 4 张分表分表规则在各服务里各写一份有的用 uid 取模有的用订单号 hash同一个字段在不同服务里规则居然还能不一致。这种做法的痛点不用我多说规则一改要通知所有团队新同学入职先花两周熟悉“哪张表在哪个库”一旦某个服务忘了带分表键全表扫描直接打到主库上。更难受的是从库数量一多连接管理就成了灾难每个服务都得维护一堆从库地址扩容缩容要发公告。所以当时我们的目标很明确在应用和 MySQL 之间加一层代理让上层只感知一个逻辑数据源。代理负责两件事一是把读流量自动分发到只读实例二是把对分表的访问路由到具体的物理表。这层代理我们最终选了 Atlas。1.2 为什么最终选了 Atlas 而不是其他方案做选型的时候我们也认真对比过几类方案。第一类是直接用 LVS 或 HAProxy 做数据库负载均衡但那只解决了连接分发问题完全不懂 SQL读写分离和分表逻辑依然要应用层自己做。第二类是当时的 Cobar、Mycat功能确实全但 Mycat 是 Java 写的团队对 JVM 那套运维体系不太熟而且 Mycat 在早期版本的分表规则配置和跨节点事务处理上出现过一些不太可控的问题。第三类是数据库本身的主从方案比如 MySQL Replication 自研路由那就更远了等于把中间件重写一遍。Atlas 打动我们的点有三个一是它是 C 语言写的基于 MySQL Proxy 0.8.2 改造性能损耗比 Java 版代理小得多线上跑起来很轻二是它本身就支持读写分离和分表而且分表规则配置是静态文件逻辑透明出问题容易排查三是它在当时已经有不少互联网公司在生产环境用踩坑记录多我们不是第一个吃螃蟹的人。现在回头看这个选择在当时是对的。Atlas 的运维模型足够简单二进制就一个 mysql-proxy配置就一个 conf 文件没有复杂的外部依赖出了问题要么看日志要么用管理端口查状态排查链路非常短。这种“简单到可以掌控”的感觉在线上出故障时太宝贵了。1.3 Atlas 的核心代理模型读写分离的路由流程Atlas 的架构不复杂。客户端连的是 Atlas 的代理端口Atlas 背后维护两组后端地址一组是主库proxy-backend-addresses一组是只读库proxy-read-only-backend-addresses。它把读和写的判断逻辑内置在 C 代码里不依赖 Lua 脚本这也是它和原版 MySQL Proxy 最大的区别——原版 Proxy 还要靠 Lua 脚本实现路由逻辑性能差而且容易写崩Atlas 直接把常用逻辑写死了稳定但灵活性低。具体来说Atlas 拿到一条 SQL 后先做简单的词法分析如果语句以 SELECT 开头且不是SELECT ... FOR UPDATE也不是SELECT ... INTO就路由到只读库否则一律走主库。这个判断规则非常朴素没有基于代价的复杂优化所以像SELECT ... FOR UPDATE这种需要加锁的查询会被正确识别出来走主库。我们线上还遇到过一种情况就是存储过程里先 UPDATE 再 SELECTAtlas 为了安全起见会把它当作写操作全部转发到主库避免读到旧数据虽然少了点性能收益但一致性是对的。这套模型的最大好处是简单直观只要理解了“SELECT 走读、其余走主、锁查询走主”这三条规则就能推测出线上大部分流量的走向。2. 部署从编译到第一行读流量路由2.1 编译安装与 glibc 的坑Atlas 官方提供了编译好的二进制包但我强烈建议你在自己的环境里重新编译一遍原因后面讲。当时我们拿到的机器是 CentOS 6glibc 版本偏旧直接跑官方二进制会报GLIBC_2.14 not found之类的错误。官方 README 里其实提到了一个参数叫--with-mysql和--with-libevent但你真正需要关心的是编译环境里的 glibc 版本。我的经验是先在干净环境里装好依赖再执行经典的编译三步./configure --prefix/usr/local/mysql-proxy --with-mysql/usr/local/mysql --with-libevent/usr/local/libevent make make install编译过程中最容易踩的坑是 libevent 版本不匹配。Atlas 依赖 libevent 来做网络事件驱动如果你系统里装的是 libevent 2.x 的老版本编译时会报一些诡异的类型错误。我当时是手动下载 libevent 1.4.14b 编译安装到独立目录再把它指给 Atlas 用的这样不会污染系统里的其他组件。编译完成之后安装目录下会生成bin/mysql-proxy和bin/mysql-proxyd后者是官方封装好的启停脚本。还有一点需要提醒Atlas 对 MySQL 的版本很敏感当时我们后端接的是 MySQL 5.5/5.6完全没问题但如果你的 MySQL 是 8.0那大概率连认证都过不了这个后面专门讲。2.2 配置文件的每个关键项Atlas 的配置文件是conf/mysql-proxy.conf格式类似 INI但有很多坑。我直接给一份我们线上用过的核心配置[mysql-proxy] admin-address 127.0.0.1:2345 admin-username admin admin-password admin123 proxy-address 0.0.0.0:3307 proxy-backend-addresses 10.0.0.2:3306 proxy-read-only-backend-addresses 10.0.0.3:3306,10.0.0.4:3306 pwds myapp:MyPass123,readonly:ReadOnlyPass456 daemon true event-threads 4 charset utf8 log-level message log-path /data/atlas/log sql-log 1逐个说关键项。proxy-backend-addresses是主库地址只能配一个或多个Atlas 会把写请求发到第一个可用的主库上所以如果你想配双主它不是那种能帮你做双写仲裁的方案需要上层自己保证数据一致性。proxy-read-only-backend-addresses是只读库地址多个地址用逗号分隔Atlas 会做简单的负载均衡但它是基于连接级别的不是基于请求级别的。也就是说同一个客户端连接如果建立了长连接后续的 SELECT 会一直路由到同一个只读库除非这个只读库挂掉或连接重建。这一点在流量不均匀的时候特别明显我后面会细说。pwds是客户端连接 Atlas 时使用的账号密码注意它和后端 MySQL 的账号是分开的。Atlas 校验完客户端身份后会用同一个账号去连接后端的 MySQL 实例。所以你要保证这些账号在后端所有 MySQL 实例上都存在且权限一致否则会出现“连得上 Atlas 但执行 SQL 报错”的诡异问题。event-threads是处理线程数我们用 4因为 Atlas 本身很轻瓶颈一般在后端数据库。如果机器核心数多可以适当调大但没必要一上来就 16、32。sql-log设置为 1 会记录每一条 SQL这个在生产环境要慎重流量一大日志磁盘分分钟写满后面我会给出我的用法。2.3 配置 admin 管理端口与状态查询Atlas 提供的 admin 管理端口很多人会忽略但它绝对是排查问题的神器。配置里的admin-address和admin-username/admin-password就是控制入口。启动 Atlas 之后可以用 MySQL 客户端连上去mysql -h127.0.0.1 -P2345 -uadmin -padmin123连上后执行SELECT * FROM backends;能看到所有后端的地址、类型和状态----------------------------------------------------------------- | backend_nd | address | state | type | slave_connections | ----------------------------------------------------------------- | 1 | 10.0.0.2:3306 | up | rw | 0 | | 2 | 10.0.0.3:3306 | up | ro | 5 | | 3 | 10.0.0.4:3306 | up | ro | 5 | -----------------------------------------------------------------这里的state字段是 up 表示正常如果是 down 就说明 Atlas 探活已经失败了。type字段里 rw 是主库ro 是只读库。还有一个很实用的管理命令是SELECT * FROM help;它会列出支持的管理语句比如把某个后端置为下线SET OFFLINE 2;这个命令会在不重启 Atlas 的情况下把第 2 个后端临时摘除非常适合在做只读库维护时用。等维护完了再SET ONLINE 2;加回来。这个功能当时帮了我们大忙不用改配置重启代理流量完全无感知。2.4 验证读写分离是否生效配置好之后第一件事不是急着接流量而是验证路由逻辑是否符合预期。我的验证方法很土但很有效在后端 MySQL 实例上开启 general log然后从 Atlas 连上去执行几条不同类型的 SQL再去后端的日志里看 SQL 到底落到了哪个实例。比如我执行SELECT * FROM user WHERE uid 1; UPDATE user SET name test WHERE uid 1; SELECT * FROM user WHERE uid 2 FOR UPDATE;如果配置正确第一条 SELECT 应该只在只读库的 general log 里出现UPDATE 和带 FOR UPDATE 的 SELECT 应该只在主库出现。还有一个坑要提醒Atlas 日志里记录的“路由到哪个后端”未必和实际执行结果一致因为它内部有自己的连接池连接可能是之前就建好的。所以最可靠的验证方式还是看后端 MySQL 的 general log 或者 performance_schema 里的语句统计而不是只看 Atlas 自己的日志。第一次验证通过的时候那种“原来中间件就是这么回事”的感觉是很爽的。但后面的分表才是真正让人头疼的部分。3. 分库分表规则的配置逻辑与扩展过程3.1 先搞清楚你需要的分表方式hash 还是 rangeAtlas 支持两种分表思路hash 取模和 range 范围。这个选择直接影响后续的数据迁移复杂度。hash 分表的思路是对分表字段做一个散列比如uid % 10决定数据落到 user_0 还是 user_9。它的优点是数据分布基本均匀缺点是你得知道分表总数而且一旦总数变了比如从 10 张扩到 20 张绝大多数行的物理位置都要变必须做数据迁移。range 分表的思路是按字段值的范围划分比如0-1000 万放 user_01000 万-2000 万放 user_1。它最大的优点是扩展方便新加一张表只是新增一个范围段老数据完全不用动缺点是如果数据写入不平均可能出现某张表特别大另外几张表基本闲着的情况。我们当时的订单表选的是 hash 分表因为订单写入高频且均匀用范围分表很容易出现热点。而用户表最终用了 range因为用户注册量是持续增长的用 range 可以自然扩表不用做大规模迁移。这个决策要提前做因为 Atlas 的规则一旦定了中途改起来非常痛苦。3.2 配置规则的写法与常见报错Atlas 的分表规则是写在配置文件里的不同版本字段会略有差异我用当时版本的大致格式举例tbl_rule { user: { col: uid, type: hash, num: 10 } }意思是user 表按照 uid 字段做 hash 分表分成 10 张表实际物理表名是 user_0 到 user_9。如果你用 range那就不是 num 一个参数而是要显式列出每个范围段对应的物理表。配置写好后要重启 Atlas 才会生效。这里有个特别容易踩的坑Atlas 启动时如果规则配置解析失败它不会直接告诉你“分表规则有错”而是启动的时候不报错但查询一过来就返回一个很晦涩的错比如ERROR 1105 (HY000): unknown error。我第一次遇到时还以为是后端 MySQL 的问题排查了一下午才发现是分表规则里字段类型写错了。所以我的建议是改完规则不要直接切流量先用一个测试账号连上 Atlas 执行一条带分表键的 SELECT确认能正常返回再逐步放量。3.3 从 4 张表扩到 10 张表的实际操作我们线上真正做过一次扩容把订单表从 4 张扩到 10 张。过程不复杂但每一步都得小心。因为订单表是 hash 分表总表数变了之后原来 hash 到 user_0 的数据有一部分必须迁到 user_1也就是说几乎全表数据要重新分布。我们当时的操作步骤是这样的先在每个后端 MySQL 实例上创建好新的物理表比如从 order_0 到 order_9。写一个数据迁移脚本扫描老表 order_0注意不是 Atlas 路由后的表名而是真实的物理表对每条数据重新计算order_id % 10再插入到对应的新表。迁移过程中业务还在写入所以不能直接改 Atlas 规则而是先做增量同步。我们用的办法是记录迁移开始时的 binlog position迁移完存量数据后再追平增量数据。追平增量后短暂停写确认数据一致再修改 Atlas 的分表规则把 num 从 4 改成 10重启 Atlas。启动后立刻用一条带分表键的查询验证几条已知数据确保位置正确然后才放开写流量。这套流程本质上是一个经典的“存量迁移 增量追平 切换”过程和很多数据库迁移方案是相通的。但有一点 Atlas 做得不够好它没有图形化工具没有在线迁移助手所有工作都要自己写脚本完成。好在我们当时的 RDS 环境支持创建只读实例直接基于只读实例做迁移减轻了对主库的压力。3.4 分表后的业务改造与自增 ID 问题分表之后最让人头疼的是自增 ID。原来 MySQL 的自增主键在单表下没问题分表之后如果还是用AUTO_INCREMENT不同物理表各自维护自增值就会出现全局重复的订单号。这肯定不行。我们的做法是引入了统一的 ID 生成服务用“时间戳 机器号 序号”的方式生成 ID在应用层作为订单号写入让分表字段变成这个业务生成的 ID而不是数据库自增列。这样一来同一个用户的所有订单可以按 uid 分表也可以按 order_id 分表两条路由都成立。另外一个容易被忽略的问题是分表字段不能随便换。比如你一开始用order_id分表后来业务想按user_id维度查这个用户的所有订单那么不带 order_id 的查询就无法定位到具体表Atlas 会直接报错或者把查询发到所有物理表做聚合。我们当时为了支持这个场景专门建了一张索引表记录 user_id 到订单号的映射查询时先查索引表拿到 order_id再路由到具体物理表。这个设计看起来绕但能保住核心链路的分表规则不变。4. 线上事故与排查实录4.1 只读库延迟后代理怎么办Atlas 不会智能判断只读库的复制延迟。它默认所有处于 up 状态的只读库都是“可读”的查询会继续往那边打。如果某个只读库因为大查询或大事务落后主库几秒甚至几分钟应用层读到旧数据的风险是真实存在的。我们遇到过最典型的一次是运营部门跑了一个全表统计的报表查询直接把一个只读库的 CPU 打满复制线程被饿着走不动延迟涨到 2000 秒。业务那边一直在反馈“数据不对”但我们查 Atlas 的 backends 状态那个库还是 up流量照常打过去。最后是通过 admin 端口把这个只读库SET OFFLINE摘除才让读流量自动转移到其他只读库上。这次之后我们加了一个监控脚本定期检查每个只读库的Seconds_Behind_Master超过阈值就自动去 Atlas 管理端口把该库置为 offline恢复后再 online。这个脚本本质上就是帮 Atlas 补上了一个“智能摘除”的能力因为它本身不提供这个功能。4.2 一次连接数被打爆的排查还有一次比较惊险的是连接数打爆。现象是应用层开始大量报Too many connections但奇怪的是后端 MySQL 的连接数还好反而是 Atlas 所在的机器出现了文件描述符耗尽Proxy 日志里全是accept failed。排查下来发现原因是我们给 Atlas 配置的event-threads只有 4而在高并发场景下每个新连接都要经过事件线程分发短连接模式下瞬间的并发建连请求超过了 Atlas 的处理能力导致 accept 队列堆满新的客户端连接进不来。解决方式分两步一是把event-threads调大到 16并相应调高系统层面的文件描述符上限二是推动应用层从短连接改成连接池方式减少频繁建连的开销。这里我特别想提醒对于任何基于代理的数据库架构应用层连接池一定要配置合理的最大连接数。如果每个应用实例都开 200 个连接背后挂 20 个实例那就是 4000 个连接打到 Atlas 上Atlas 再往后端转发后端 MySQL 的max_connections通常只有几千很容易被冲垮。4.3 sql 日志把磁盘写满之后前面提到我把sql-log设为了 1这在流量小的时候没什么感觉等到日均 QPS 涨起来之后日志文件像疯了一样膨胀。有一次直接写满了 /data 分区导致 Atlas 写不了日志进程整体 hang 住数据库访问全部超时。那次事故之后我把sql-log的策略改成了主力环境不开、排查问题临时开 5 分钟、开完立刻关闭。具体做法是平时保持sql-log 0需要查某个业务 SQL 是否走了正确分表时临时改成 1重启 Atlas采集到日志后再改回 0 重启。虽然要重启两次但至少不会把磁盘搞爆。如果你真的需要长时间记录 SQL建议用 logrotate 做日志轮转并且定期把历史日志清理掉。另外Atlas 日志目录里除了 sql 日志还有 warning.log 和 error.log。warning.log 里会记录很多后端连接异常、语句执行失败的线索是排查问题时的第一站。我建议你出道题就逼自己先去看 warning.log而不是先去看业务代码能省很多时间。4.4 主库宕机的半小时Atlas 的真实表现最严重的一次事故是主库硬件宕机。当时 Atlas 配置里只有一个主库地址主库挂了之后Atlas 并没有自动把只读库提升为新主库也没有任何“failover”的概念。所有写请求全部失败应用层报错整个写入链路瘫痪。我们当时的做法是手工把一个只读库提升为主库修改它的复制关系然后修改 Atlas 配置里的proxy-backend-addresses指向这个新主库重启 Atlas整个恢复过程大约花了半小时。这半小时里业务是不可写的。坦率说这次事故让我彻底意识到 Atlas 的定位它是一个轻量级的读写分离和分表代理不是高可用方案。高可用需要依赖外部的 MHA、Orchestrator 或云厂商的 HA 机制由它们负责主从切换切换完成后再用 admin 端口或改配置的方式让 Atlas 指向新主库。如果你想用 Atlas 撑起核心交易链路务必在架构里把高可用层单独做出来并且定期做切换演练别等到真宕机了才第一次尝试。5. 这只“擎天巨兽”也该换岗了转向新中间件5.1 Atlas 的局限它停更太久了Atlas 最大的问题不是它不好用而是它太老了。它的代码基础停留在 MySQL 5.x 时代官方 GitHub 仓库的更新基本处于停滞状态。这意味着四个问题第一不支持 MySQL 8.0 的新认证插件导致新版本 MySQL 根本接不进来第二不支持新的 SQL 语法和优化器特性比如窗口函数、CTE 等这些在 8.0 里是好东西但 Atlas 解析不了会把它们当成错误返回第三对 TLS/SSL 加密连接的支持很原始配置起来很麻烦第四社区已经不怎么活跃了遇到问题能搜到的资料基本都是几年前的旧帖子。如果你现在还在 5.7 上安稳跑着Atlas 或许还能再战几年但如果你的团队已经在推动 MySQL 8.0那 Atlas 就不是一个合适的选择了。5.2 备选中间件对比ProxySQL、ShardingSphere、Vitess接替 Atlas 的候选方案我当时重点评估了三个。ProxySQL 是纯代理层方案和 Atlas 的定位最接近但功能强太多支持在线修改配置、查杀会话、SQL 级路由规则、连接池管理、自动检测后端状态而且它本身就支持把“主库故障”时的流量自动切换到一个新的写节点。它最大的优势是轻量、高性能和 Atlas 一样不需要侵入业务的代码。ShardingSphere 是另一种思路它可以做应用层接入ShardingSphere-JDBC也可以做独立代理ShardingSphere-Proxy。它更偏向完整的分库分表生态对分表算法、分布式事务、数据脱敏等有非常多内置能力。缺点是它本质上是 Java 体系如果你对 JVM 和这套生态不熟悉运维成本会高一些。Vitess 则更适合超大规模场景它是为“把 MySQL 当成分布式 KV 存储”这种体量设计的。当时我们这种单库 QPS 在几千到几万的规模用 Vitess 属于杀鸡用牛刀而且它本身是一套复杂的编排系统需要引入专门的 operator 和拓扑管理工具。做个简单的对比表方案代理/嵌入是否支持 MySQL 8.0动态配置主从切换学习成本Atlas代理否部分支持否低ProxySQL代理是是自动/可编程中ShardingSphere-Proxy代理是是需配合 HA 组件中高ShardingSphere-JDBC嵌入是是需配合 HA 组件中高Vitess代理/集群是是是高5.3 一次平滑迁移的步骤模板我们最后选择了 ProxySQL原因很简单它最接近 Atlas 的使用习惯迁移路径最短而且是 C 写的性能表现和 Atlas 在同一梯队。迁移过程可以抽象成一个模板任何从 Atlas 到 ProxySQL 的迁移都可以参考第一步在 ProxySQL 里把后端的 MySQL 实例一个不落地配置好包括所有主库和只读库创建对应的mysql_users让应用可以通过新代理访问数据库但这一步先不切流量只是打通链路。第二步应用层把连接地址从 Atlas 切到一个负载均衡器负载均衡器后面同时挂着 Atlas 和 ProxySQL。最开始只把 1% 的流量分到 ProxySQL剩下 99% 继续走 Atlas观察几天的错误日志和延迟数据。第三步逐步增加 ProxySQL 的流量比例10%、30%、50%、80%、100%。每上调一个比例都要观察后端 MySQL 的负载和慢查询是否有异常。这个灰度过程取决于你对新代理的熟悉程度我们当时大概花了两个星期。第四步流量全部切到 ProxySQL 后保留 Atlas 进程再观察一段时间确认没有业务方的报错后再整体下线。这个模板的核心思想是“双跑 灰度 兜底”而不是直接改连接地址一把梭。数据库中间件的切换慢就是快。最后再分享一个切身感受Atlas 不是那种到处吹得天花乱坠的新物种它更像一个踏实肯干的老员工业务规则简单的时候能稳稳扛住但面对新环境和新需求就显得力不从心。如果让我在 2025 年给一个新项目做选型我不会再选 Atlas但如果你的系统已经跑在 Atlas 上只要 MySQL 版本不高于 5.7Keepalived 或者 MHA 把高可用兜底好它其实还能再稳很久。而你要做的就是想清楚什么时候让它体面地退到二线换上更适应未来的新方案。