看到“atlas”这个标题,我第一反应不是希腊神话里扛天的泰坦,也不是波士顿动力那个会后空翻的机器人,而是数据库圈子里一个相当经典的老伙计——MySQL Atlas。这是奇虎360在2012年开源的一个基于MySQL协议的数据库中间件,核心做两件事:读写分离和分库分表。可能年轻一点的开发都没听过它,但在我早年做电商系统重构的时候,Atlas真的是撑过不少大流量的场面。今天这篇就围绕Atlas,聊聊它的设计思路、部署方式,以及我实际运维中踩过的那些坑。
如果你正在维护一个读写比很高、主从架构已经搭好但还没有中间层的MySQL集群,或者你想搞清楚“应用层直连主从切换麻烦、读压力分散不均”这类问题该怎么解,这篇文章应该能给你一些可直接落地的参考。哪怕是刚接触数据库中间件的新人,把Atlas跑通一遍,对理解整个数据库访问链路也会有很大帮助。
1. Atlas是什么,为什么数据库架构里需要它
1.1 一句话认识Atlas
Atlas是奇虎360基于MySQL Proxy源码二次开发出来的一个中间件,位置处在应用和MySQL实例之间,对应用层暴露一个标准的MySQL协议端口。应用连Atlas就跟连一个普通的MySQL实例一样,不需要改代码,不需要换驱动,连接串改个端口就行。
它最核心的职责有三个:把写请求路由到主库,把读请求按权重分发到多个从库,以及把逻辑大表拆成多个物理小表来降低单表数据量带来的性能压力。用一句话总结就是:让应用层无感知地访问一个“虚拟MySQL”,实际背后站着一组真实的主从集群。
1.2 没有中间件时,读写分离是怎么做的
先看一个典型场景。你的业务有1000个并发请求,其中850个是查询,150个是写操作。数据库有一主两从,主库负责写入。没有中间件的时候,读写分离通常有三种搞法:
第一种是应用层硬编码,在代码里封装两个数据源,读走从库、写走主库。这个方案最直接,但问题也很明显:哪天从库扩到5台,你要改代码;某台从库挂了,你还要改代码做摘除。而且多个业务团队各搞一套,规则散落各处,后期维护非常痛苦。
第二种是借助Spring的AbstractRoutingDataSource这类动态数据源路由,通过AOP拦截方法名判断读还是写。这种方式比硬编码好一点,但本质上还是应用层逻辑,SQL一复杂就容易判断失误。
第三种就是引入独立的数据库中间件,由中间件来解析SQL、识别读写语义、完成后端节点的负载均衡和故障摘除。Atlas就是这个角色。应用层不需要知道背后到底有几台数据库,也不需要关心哪台挂了,中间件会把这一切都挡在外面。
第三种方案的优势在于“分流逻辑下沉”。数据库架构的变更从此不需要跟着应用版本走,DBA自己就能在中间件层面完成扩缩容。这对一个数据库实例几百上千套的中大型公司来说是刚需。
1.3 Atlas在整个架构里的位置
一个典型的Atlas部署架构大概是这个路子:LVS或Keepalived在最前面做虚拟IP漂移,后面挂两台Atlas节点(一主一备或双活),每台Atlas再通过配置连接后端的主库和若干从库。应用只连接LVS的虚拟IP,虚拟IP后面的实际节点变化对应用完全透明。
Atlas本身是单进程多线程模型,不依赖第三方组件,部署起来就是解压、改配置、启动三步。它的轻量是当时能火起来的一个重要原因。和同一个时代的Cobar、MyCat这类重一些的分布式数据库中间件相比,Atlas更偏向于“解决主从读写分离和单库分表”,而不是一开始就奔着“分布式事务、全局序列”那种大而全的方向去。选型的核心逻辑就是:你的问题域是什么,就选解决这个问题的工具。
2. 核心设计解析:读写分离、分库分表与连接管理
2.1 读写分离:SQL级智能路由
Atlas的读写分离实现思路其实不复杂:它解析客户端发过来的SQL语句,判断语句类型,然后决定把这条SQL发给主库还是从库。
判断规则大致是这样的:
SELECT语句默认走从库,但如果语句里带有FOR UPDATE或LOCK IN SHARE MODE这种显式锁语义,就必须走主库,因为从库是异步复制,可能还没拿到最新的行锁状态。INSERT、UPDATE、DELETE、REPLACE、DDL等写操作一律走主库。- 事务内的读操作,Atlas会确保在同一个后端连接上执行。也就是说,你在一个事务里先
UPDATE再SELECT,那这条SELECT不会跑到从库去,因为事务隔离级别要求读到本事务的修改,走从库就会出现“自己写的数据自己读不到”的问题。
这里面最关键、也最容易被忽略的是最后一点。我在早期使用Atlas的时候,就遇到过业务方反馈“刚更新的数据刷新不出来”,排查到最后发现是他们把多个查询塞进了同一个Spring声明式事务里,而这些查询被Atlas路由到了从库。事务内读操作必须和后端写连接绑定,这是中间件路由必须守住的底线。
2.2 分库分表:规则引擎怎么工作
Atlas的分表能力是它区别于很多纯读写分离方案的亮点。它支持两种分表方式:column分表和range分表。
column分表的配置格式是表名.分表键.子表数量,比如:
tables = orders.id.0-9这个配置的含义是:逻辑表orders按id字段分成10张物理子表(orders_0到orders_9),具体路由到哪张子表,由分表键的值计算哈希后取模得出。子表数量的写法是0-9,代表10张,数量可以自定义。
range分表则适合按时间或按区间数据的场景,比如订单归档表按月分表,配置里直接指定每个区间对应的子表名。这种方式的好处是查询条件只要带上分表键,中间件就能精确地只去对应的子表拿数据,不必全表扫描。
这里有个很重要的点:分表键必须出现在SQL的WHERE条件里,否则Atlas无法确定路由目标。拿上面的orders表举例,如果一条SQL是SELECT * FROM orders WHERE user_id = 123,id字段没出现在条件里,Atlas就只能把这条SQL广播到所有10张子表,再把结果合并返回。虽然功能上没问题,但性能会非常难看。所以设计分表键的时候,一定要想清楚业务上最核心的查询维度是什么,然后拿那个维度做分表键。
2.3 连接池与线程模型
Atlas作为一个中间件,本身必然要面对两个连接维度:应用侧连接和后端数据库连接。
应用侧连接就是客户端连上来的连接。Atlas默认配置里可以通过max-conn参数限制最大连接数,超过的部分会排队或直接拒绝,起到一个“连接闸门”的作用,保护后端数据库不被突发的连接风暴打垮。这个参数非常有用,我曾经见过因为应用连接池配置失误导致瞬间建立几千个连接,直接把数据库打挂的案例。有了Atlas这层限制,至少还有一个缓冲地带。
后端连接池是Atlas自己维护到主库和从库的连接。它会在启动时预建一批连接,执行SQL时从池里取,执行完归还。需要注意event-threads这个参数,它控制Atlas处理网络事件的线程数,一般建议配置成CPU核心数的2倍。线程数太小会导致CPU利用率上不去,太大则在线程切换上浪费过多时间,这个参数需要结合压测结果来调。
2.4 字符集、状态监控与管理接口
Atlas还提供了一个独立的admin管理端口。默认情况下,admin-address配置成0.0.0.0:2345,用MySQL客户端连上去之后,可以通过几条SQL命令管理整个集群:
SELECT * FROM backends;这条命令会列出所有后端节点的状态,包括主库、从库的地址、type类型(rw还是ro)、state状态(up还是down)以及当前的连接数。生产环境做故障切换时,我第一步永远是跑这条命令看后端状态。
下线一台从库的指令是:
SET OFFLINE '192.168.1.11:3306';上线就是:
SET ONLINE '192.168.1.11:3306';这个“平滑上下线”能力很实用。比如从库要做硬件升级,如果直接kill掉数据库进程,正在通过Atlas执行查询的应用会立刻报错。但通过Atlas先把节点置为OFFLINE,Atlas会停止向这个节点发新的请求,等已有请求跑完后,再去做底层维护就安全得多。字符集方面,配置里的charset参数要和后端MySQL保持一致,否则容易出现中文乱码,这个后面实操部分细说。
3. 从零搭建一套Atlas读写分离环境
3.1 环境规划与主从同步前置
动手部署之前,先把环境规划清楚。我这边以一个典型的业务系统为例:
| 角色 | IP地址 | 端口 | 说明 |
|---|---|---|---|
| MySQL主库 | 192.168.1.10 | 3306 | 负责写入 |
| MySQL从库1 | 192.168.1.11 | 3306 | 负责读取,权重1 |
| MySQL从库2 | 192.168.1.12 | 3306 | 负责读取,权重1 |
| Atlas节点 | 192.168.1.20 | 1234/2345 | 1234为客户端入口,2345为管理入口 |
前提条件是主从集群已经搭建好,从库的数据和主库保持同步。Atlas本身不负责同步数据,它只管把SQL路由到对的节点上。如果主从同步是坏的,Atlas再怎么路由也没有意义。
主从同步的搭建这里不展开太多,只说一句:务必确认同步线程是正常的。启动Atlas之前,在从库上执行SHOW SLAVE STATUS\G,看到Slave_IO_Running: Yes和Slave_SQL_Running: Yes两个都是Yes再往下走。
3.2 安装与配置文件解析
Atlas的安装非常简单,从官方或镜像站下载对应操作系统的安装包,解压到你想要的目录即可。假设解压到/usr/local/mysql-proxy,核心的配置文件是/usr/local/mysql-proxy/conf/mysql-proxy.conf。
一份典型的读写分离配置长这样:
[mysql-proxy] admin-username = admin admin-password = admin123 proxy-backend-addresses = 192.168.1.10:3306 proxy-read-only-backend-addresses = 192.168.1.11:3306@1,192.168.1.12:3306@1 pwencrypt = true daemon = true keepalive = true event-threads = 8 proxy-address = 0.0.0.0:1234 admin-address = 0.0.0.0:2345 charset = utf8 tables = orders.id.0-9 sql-log = /usr/local/mysql-proxy/log/sql.log逐个说下关键参数:
proxy-backend-addresses是主库地址。proxy-read-only-backend-addresses是从库地址列表,多个从库用逗号分隔,@符号后面跟的是权重值。权重默认是1,如果你有一台从库配置特别高,可以设成2或更高,Atlas会按权重比例分配读请求。这个权重参数我在压测环境里验证过,2的节点收到的读请求量大约是权重1节点的两倍,计算逻辑是按权重做轮询。
pwencrypt是密码加密开关。配置成true之后,首次启动Atlas会把配置文件里明文的管理员密码和proxy-backend-addresses中涉及的用户密码自动加密写回配置。这是一个很好的安全习惯,避免配置文件泄露后数据库密码直接裸奔。
daemon是守护进程模式,keepalive是崩溃自动拉起。这两个在生产环境必须开。
sql-log会把经过Atlas的所有SQL记录到日志文件,类似于MySQL的通用日志。注意这个是全量日志,生产环境开启后日志增长非常快,建议只在排查问题时临时开启,平时保持关闭。
3.3 密码加密、启动与验证
启动之前,需要先在后端MySQL上建一个专用账号,让Atlas用它去连接主库和从库:
CREATE USER 'proxy'@'192.168.1.20' IDENTIFIED BY 'proxy_pass'; GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'proxy'@'192.168.1.20';这里的权限只需要业务账号需要的增删改查权限即可,不需要给超级管理员权限。配置里如果涉及账号密码,可以通过Atlas自带的加密工具来生成密文。
启动Atlas:
/usr/local/mysql-proxy/bin/mysql-proxy --defaults-file=/usr/local/mysql-proxy/conf/mysql-proxy.conf启动之后,先确认进程在不在:
ps -ef | grep mysql-proxy再确认端口监听:
netstat -tlnp | grep -E '1234|2345'两个端口都监听上了,基本就是起来了。接下来用MySQL客户端连一下Atlas的代理端口:
mysql -h192.168.1.20 -P1234 -uproxy -pproxy_pass能正常连上并执行SELECT 1,说明客户端链路通了。再连管理端口看后端状态:
mysql -h192.168.1.20 -P2345 -uadmin -padmin123执行:
SELECT * FROM backends;如果看到主库的state是up,两把从库的state也都是up,整个读写分离链路就通了。
3.4 客户端接入与应用改造
应用接入非常简单,只需要改数据库连接地址和端口,其他全部不变。比如原来的JDBC连接串是:
jdbc:mysql://192.168.1.10:3306/business_db改成:
jdbc:mysql://192.168.1.20:1234/business_db一个比较容易被忽略的细节是:应用侧最好把连接池的testOnBorrow或testWhileIdle中的校验SQL设为SELECT 1,因为Atlas的路由逻辑是针对标准MySQL协议做的,一些奇怪的连接校验语句有概率被错误路由,虽然概率不大,但碰到了很莫名其妙。
业务代码完全不需要感知背后有几台数据库。写操作自动落在主库,读操作自动分摊到从库,这个过程中应用一个字节都不用改。
4. 部署到生产前必须知道的事
4.1 分库分表的边界与坑
如果你的数据量已经大到单表扛不住,那Atlas的分表功能可以帮你顶一阵子,但有几个边界要提前想清楚。
第一个是分表键的选择。前面说过,分表键必须在WHERE条件里才能精确定位子表。一个常见的反面案例是:订单表按订单ID分表,但业务上最频繁的查询却是按用户ID查订单列表。那这条查询就只能广播到所有子表再合并,体验非常差。正确做法是设计的时候就考虑“用户-订单”这个主查询路径,按用户ID分表,或者用关联表对分表键做映射。
第二个是跨分表的JOIN和子查询。Atlas对跨子表的JOIN支持得并不好,一旦SQL里出现了多张分表的关联操作,它没法像真正的分布式数据库那样做复杂的执行计划。遇到这种情况,通常的解法是冗余字段或者在上游服务里做数据拼装,而不是指望中间件帮你智能处理。
第三个是分表后的自增主键问题。如果一张表分成了10张子表,每张子表的自增ID都是各自独立的,0到9号子表都可能产生ID=100的数据,全局唯一性就没法保证了。Atlas不提供分布式ID方案,生产上我们一般用雪花算法或者独立的ID发号器来生成业务主键,再写入分表字段。
4.2 主从延迟与读一致性
读写分离最大的天敌是主从延迟。MySQL的主从复制是异步的,主库刚写完,从库可能立刻读不到,这个时间窗口短则几十毫秒,长则几秒甚至更久。
Atlas本身没有智能的“延迟感知”能力。它不知道某个从库当前落后主库多少秒,它只知道把这台从库标记为up。所以一旦业务对数据一致性要求比较高,比如用户下单后马上要看到订单详情,就得在架构层面做妥协。
我在实际项目中用过两种解法。
一种是在业务上做区分:对实时性要求高的读操作,在SQL里加注释标记强制走主库。Atlas支持通过注释来指定路由,具体格式大概是在SQL前加上特定格式的注释,中间件识别注释后会把这条SQL固定发到主库。这种做法简单粗暴,但需要业务开发配合,属于“人肉一致性保障”。
另一种是把写后立即读的场景在代码里加一个短暂的延迟重试机制,比如写完主库后先查一次主库,查不到再走从库。这种方案实现起来稍微麻烦一点,但对业务无侵入,比改SQL注释更干净。
4.3 高可用与故障切换
单台Atlas本身也有单点风险。虽然Atlas进程很轻,占用的系统资源不多,但一旦进程挂了,所有依赖它的数据库访问都会断掉。所以在生产环境,Atlas前面必须再接一层高可用。
最典型的做法是两台Atlas节点配合Keepalived,对外提供一个虚拟IP。正常情况下虚拟IP绑定在节点A上,A挂了之后Keepalived把虚拟IP漂移到节点B。前端应用只连虚拟IP,完全不感知后端节点切换。
需要注意,Atlas本身不保存什么状态,切换基本是无风险的。它的配置文件是静态的,后端节点的上下线状态可以通过管理端口动态修改。这意味着节点B只要部署了相同的配置,接管后就能直接服务,不用做数据同步。
如果还需要进一步降低风险,可以考虑在Atlas前面再加一层DNS轮询或者负载均衡器,把流量分摊到多个Atlas节点,同时配合节点健康检查。不过大部分业务场景下,两台Atlas加Keepalived已经完全够用。
4.4 性能调优与监控
Atlas的性能调优,核心围着三个参数打转:event-threads、max-conn和后端连接池相关的参数。
event-threads前面提过,建议从CPU核数的2倍起步。实践下来8核机器配置16是比较稳的起点,然后压测观察响应时间和CPU利用率。如果CPU已经跑满但吞吐上不去,可以把线程数再加4试一轮;如果CPU还有余量但延迟已经很高,则要考虑是不是后端数据库瓶颈,而不是Atlas的问题。
max-conn这个参数要结合应用侧的连接池规模来算。假设有20个应用实例,每实例连接池最大配置50,20×50=1000,那Atlas的max-conn至少要设置成1200到1500,留点余量。如果设小了,高峰期会直接拒绝新连接,应用侧表现为获取连接超时,这种故障排查起来比后端数据库慢很多,因为问题出在中间层。
监控方面,Atlas自带的管理端口就是最简单的监控入口。定期执行SELECT * FROM backends,观察每个后端节点的连接数,如果某个从库的连接数持续明显高于其他从库,就要检查是不是权重配置不合理或者某台从库执行了特别慢的SQL被“卡住”导致连接不释放。还可以配合SELECT * FROM help看相关管理命令,大部分运维操作都能通过管理口完成,不需要重启Atlas进程。
5. 常见问题与排查技巧实录
| 问题现象 | 可能原因 | 排查思路与解法 |
|---|---|---|
| 应用读写正常,但某些查询结果明显过期 | 主从延迟导致读到了旧数据 | 在从库执行SHOW SLAVE STATUS看Seconds_Behind_Master;必要时对该类查询强制走主库 |
| 刚插入的数据查询不到 | SELECT被路由到了从库,而从库复制还没完成 | 确认该SQL是否在事务内;事务内路由已绑定主库,事务外需要业务配合或使用注释强制路由 |
| 连接Atlas超时 | max-conn设置过小 | 统计应用实例数乘连接池大小,重新计算并调整max-conn |
| 某一子表数据特别多,其他子表很少 | 分表键值分布不均 | 检查分表键的取值空间,如果存在热点键,考虑换分表键或加一层映射 |
| 中文乱码 | Atlas和MySQL的字符集不一致 | 确认Atlas配置里charset和后端MySQL实例的character_set_server一致,同时JDBC连接串加上characterEncoding=utf8 |
| 从库被误下线后不会自动恢复 | 下线操作是手动触发的,不会自动回滚 | 通过管理端口执行SET ONLINE重新上线,同时确认从库本身状态正常 |
我在实际运维里踩过最深的一个坑是:有一次对从库做配置变更,直接在数据库层面STOP SLAVE,忘了先通过Atlas把节点置为OFFLINE。结果就是所有读到这台从库的业务瞬间报错,排查了很久才定位到是Atlas还在往这台“假up”节点发请求。自那以后,我定了一条规矩:对后端数据库节点的任何维护操作,第一步必须是先在Atlas管理端口把它置为OFFLINE,等操作完成且验证通过后再置为ONLINE。这应该成为所有Atlas运维人员的肌肉记忆。
还有一个我觉得特别值得分享的经验是SQL日志的“黑盒取证”用法。有一次业务方反馈某时段数据库压力飙升,但业务量并没有明显增长。我们一开始怀疑是慢查询,翻了一圈MySQL慢日志也没发现明显异常。后来想到Atlas有sql-log能力,临时把这个参数打开,跑了十分钟就抓到了真凶:某个定时任务在循环里对一张大表执行没有分表键的模糊查询,每次都要广播到所有子表,等于一次查询放大了10倍压力。这就是中间件在你和数据库之间加了一层“可观测性”的价值——没有这层,你连问题在哪都找不到。
6. 一些关于选型的实在建议
最后聊点我个人对Atlas在当前技术环境下的看法。
Atlas毕竟是一个十多年前诞生的项目,后期维护节奏放缓,它在一些新特性上确实跟不上。比如对MySQL 8.0某些新认证插件和协议特性的支持不够完整,对当前流行的容器化部署也不太友好。如果你的团队是全新的MySQL 8.0集群,又没有历史包袱,我可能会建议优先考虑官方MySQL Router或者ShardingSphere这类仍在活跃迭代的方案。
但如果你遇到的场景和我当年一样:一套MySQL 5.7主从集群,需要快速解决读写分离和单库分表问题,团队里DBA资源又紧张,那Atlas依然是一个非常值得考虑的轻量级选择。它配置简单、文档多、社区里趟过坑的人也多,出了问题搜一下基本都有答案。
我的态度是:工具没有绝对的新旧好坏,关键看它和你的问题是否匹配。把Atlas用明白,你对数据库访问链路的理解会比那些直接上手云数据库托管服务的人深得多。毕竟中间件替你解决的那一层问题,终究是你系统架构里真实存在的一部分。