聊到数据库中间件,很多人第一反应是 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 再 SELECT,Atlas 为了安全起见会把它当作写操作全部转发到主库,避免读到旧数据,虽然少了点性能收益,但一致性是对的。这套模型的最大好处是简单直观,只要理解了“SELECT 走读、其余走主、锁查询走主”这三条规则,就能推测出线上大部分流量的走向。2. 部署:从编译到第一行读流量路由2.1 编译安装与 glibc 的坑Atlas 官方提供了编译好的二进制包,但我强烈建议你在自己的环境里重新编译一遍,原因后面讲。当时我们拿到的机器是 CentOS 6,glibc 版本偏旧,直接跑官方二进制会报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_0,1000 万-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.7,Keepalived 或者 MHA 把高可用兜底好,它其实还能再稳很久。而你要做的,就是想清楚什么时候让它体面地退到二线,换上更适应未来的新方案。
聊到数据库中间件,很多人第一反应是 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 再 SELECT,Atlas 为了安全起见会把它当作写操作全部转发到主库,避免读到旧数据,虽然少了点性能收益,但一致性是对的。这套模型的最大好处是简单直观,只要理解了“SELECT 走读、其余走主、锁查询走主”这三条规则,就能推测出线上大部分流量的走向。2. 部署:从编译到第一行读流量路由2.1 编译安装与 glibc 的坑Atlas 官方提供了编译好的二进制包,但我强烈建议你在自己的环境里重新编译一遍,原因后面讲。当时我们拿到的机器是 CentOS 6,glibc 版本偏旧,直接跑官方二进制会报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_0,1000 万-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.7,Keepalived 或者 MHA 把高可用兜底好,它其实还能再稳很久。而你要做的,就是想清楚什么时候让它体面地退到二线,换上更适应未来的新方案。