如果没有那次大促前的容量评估,我大概不会想到,一个以神话巨人命名的老中间件,会在我们的生产环境里一跑就是三年。当时数据库层的数字已经很不好看了:读流量是写流量的十倍出头,主库高峰CPU能冲到70%,底下几台从库的CPU却长期趴在20%以下。业务方每次问“能不能扛住活动峰值”,DBA这边都没法拍胸脯保证。原因很简单,流量模型是典型的读多写少,索引优化到一定程度后就再榨不出多少空间,必须把读流量真正搬到从库去。
于是选型开始了。我们要的条件不高,但也挺具体:应用不用改代码、支持多个语言栈、DBA自己能运维、部署别太重。筛了一圈下来,目标落到了一个相对冷门的组件上——360开源的Atlas,一个基于MySQL Proxy 0.8.2定制的MySQL读写分离中间件。
这篇不是官方文档翻译,也不是给某个方案站台。我打算从选型逻辑、路由原理、上线前后要处理的具体问题,以及几次真实故障的排查过程写起,把Atlas在实战里的优点、坑和边界一次说清楚,给正在评估同类中间件的同学一个参考。
1. 为什么最后选了Atlas而不是更年轻的读写分离方案
1.1 当时的现状和约束
团队当时的情况,放在很多互联网公司里都有代表性。应用栈以Java为主,但也有PHP和Python写的服务,加起来二十多个业务模块。数据库是MySQL 5.6和5.7混部,已经搭好了一主三从的复制拓扑,问题是从库并没有被充分利用。部分应用虽然做了读写分离,但规则写在各自的代码里,有的走MyBatis拦截器,有的是在Service层手动指定数据源,还有的根本没做分离,所有流量都打在主库上。
这种局面下,最忌讳的就是提出一个“看起来很完美但推不动”的方案。我们评估了四条路线,每一条都摸过底。
1.2 四个候选方案的对比
第一是应用层双数据源。这个思路最直接,每个服务自己配两个DataSource,路由逻辑写在框架里。但问题是二十多个应用、四种语言,每个团队的代码习惯还不一样,改造周期拉长,风险完全失控。我们估算过,想把所有应用的读写分离统一改造完,至少两个季度,这还不包括测试和回归。
第二是ShardingSphere,当时更流行的叫法还是sharding-jdbc。功能确实强,社区也活跃,但它是客户端类方案,Java项目接入要改一堆依赖和配置,PHP和Python项目完全用不上。更要命的是,团队里没有专职的Java架构师去统一主导这件事,一旦某个应用的SQL解析出现问题,排查链路会横跨业务代码和框架内部,值班的同学压力很大。
第三是Proxy类中间件,也就是我们最终走的路线。Proxy型方案的共同优点是协议层透传,应用只改一个连接串,Java、PHP、Python都能用。关键在于选哪个。
我们当时对比了MyCAT、Atlas、ProxySQL、DBProxy。MyCAT功能全面,分库分表、全局序列都有,但对一个只为了解决读写分离的场景来说太重了,团队得养一个中型中间件平台。ProxySQL是后起之秀,查询路由、连接池、可观测性都很强,不过那时线上MySQL还是5.6和5.7,和生产环境相关的踩坑资料不像现在这么多,团队评估起来有点没底。DBProxy是美团开源的,同样优秀,但我们在内网社区里搜到的Atlas实践案例更多,最后就决定先用Atlas做一轮真实压测。
| 维度 | 应用层双数据源 | ShardingSphere | Atlas | MyCAT | ProxySQL |
|---|---|---|---|---|---|
| 改动量 | 高 | 中高 | 低 | 中 | 中 |
| 多语言支持 | 每个语言都写一遍 | 仅Java生态友好 | 协议层透明 | 协议层透明 | 协议层透明 |
| 读写分离 | 自己写 | 支持 | 支持 | 支持 | 支持 |
| 分库分表 | 自己写 | 强 | 弱 | 强 | 一般 |
| 运维复杂度 | 中 | 中 | 低 | 中高 | 中高 |
| 社区活跃度 | 依赖自研框架 | 高 | 低 | 中 | 高 |
1.3 Atlas在我们场景里的独特优势
Atlas是360开源的,基于MySQL Proxy二次开发,在官方版基础上修了不少问题,增强了读写分离和从库负载均衡,还支持一定程度的分库分表。虽然社区后期维护节奏明显放缓,但它的代码量不大,架构也简单,出了问题可以根据日志很快定位。
说白了,那个时间点选它,看中的就是“最小可用”。接入成本低到业务只改一个数据库连接串,路由逻辑清晰,管理命令简单,DBA学一遍就能上手。它没有Pro机型那么亮眼,但够用、够简单,能让业务无感切换,也让值班的人看得明白。
后来回头看,这个选型决策的价值在于,我们在正确的时间选了一个复杂度刚好匹配问题的方案。没有过度设计,也没有图新图强,把自己拖进另一个运维泥潭。
2. Atlas路由原理与配置模型:从一条SQL的流向说起
2.1 一条读SQL是怎么被转发的
我建议任何准备用代理的人都先搞懂,SQL到了代理之后是怎么被分类、派发、返回的。不然故障来了真的会抓瞎。
客户端和应用之间,Atlas伪装成了一个MySQL Server。应用连接Atlas的监听端口,和连真实MySQL没有区别。SQL发给Atlas后,Atlas会先用解析器判断第一个关键字,然后走下面的路由决策:
- 以SELECT开头,进入读路由分支。
- INSERT、UPDATE、DELETE、DDL、CALL等非SELECT语句,默认全部发给可写后端,也就是主库。
- 读路由分支里,如果会话已经处于事务中,则强制走主库。
- SELECT语句带
/*master*/注释,强制走主库。 - SELECT语句带
/*slave*/注释,强制走从库。 - SELECT ... FOR UPDATE这类带锁的读,必须走主库。
- SELECT LAST_INSERT_ID()这类依赖写后结果的语句,也要走向主库。
- 其余普通SELECT,按权重从只读后端列表里挑一台从库转发。
这看起来不复杂,但实际排障时,很多“玄学问题”都能在这条逻辑链上找到答案。比如一条SQL为什么跑到从库上去了,先看它是不是普通SELECT,再看应用有没有显式开启事务,最后看是不是命中会话状态类函数,三步基本能定位。
2.2 事务、特殊SQL和强制路由
事务是读写分离里最容易翻车的地方。MySQL默认autocommit=1,很多业务代码里其实没有显式BEGIN,而是通过多条语句自然形成“隐式事务”。但只要应用代码里有BEGIN或START TRANSACTION,Atlas就会在会话级别做标记,这个连接上的所有后续SQL都走主库,直到COMMIT或ROLLBACK出现。这个设计保证了事务内的读不会因为路由到从库而读到不一致的数据。
特殊SQL要特别注意两类。一类是SELECT ... FOR UPDATE,带锁读如果去了从库,锁就没有意义了,数据一致性会出大问题。另一类是SELECT LAST_INSERT_ID(),很多业务用它取写操作生成的自增主键,如果被路由到从库,轻则拿不到值,重则拿到一个错值。
我们实测的版本对这类语句是有识别的,但为了保险,业务代码里遇到这种敏感SQL,我仍然建议显式加上/*master*/注释做兜底。注释强制路由是Atlas一个非常实用的能力,等于在SQL级给开发同学留了一个逃生舱。
2.3 配置模型与管理命令
Atlas的配置不算复杂,核心就是主库列表、只读列表、账号和端口。一个典型的配置大概长这样:
[mysql-proxy] manager-address = 127.0.0.1:2345 proxy-address = 0.0.0.0:1234 proxy-backend-addresses = 192.168.1.10:3306 proxy-read-only-backend-addresses = 192.168.1.11:3306,192.168.1.12:3306 proxy-password = 你的加密后密码串proxy-address是业务连接的端口,manager-address是管理端口。proxy-backend-addresses里填的是可写后端,也就是主库;proxy-read-only-backend-addresses里填的是从库列表,多个从库用逗号分隔。密码不能放明文,要用工具生成加密串。
管理端口非常实用,连上之后可以执行管理SQL:
mysql -h127.0.0.1 -P2345 -uadmin -padmin-- 查看所有后端的状态 SELECT * FROM backends; -- 在线摘除某个后端 SET OFFLINE '192.168.1.11:3306'; -- 重新上线 SET ONLINE '192.168.1.11:3306';为什么强调用管理命令而不是直接改配置?因为生产环境改配置通常要重启进程,会断连接。管理命令可以在线变更后端列表,排障时能少很多沟通成本。我们后来所有的主从切换操作都离不开这套命令。
配置层面有几个坑是新手容易踩的。主库和从库列表千万不要重叠,同一个实例同时出现在两个列表里,路由就乱了。从库列表里的实例复制关系要保持一致,如果某台从库落后主库太多,读流量打到它头上,业务就会读到旧数据。配置文件改完先做语法校验,不要直接推到生产。
3. 上线前必须补齐的账号、灰度与边界验证
3.1 账号体系和安全基线
Atlas有自己的一套账号映射,并不是简单透传MySQL账号。业务连接Atlas时,Atlas会拿着连接串里的用户名密码去匹配自身配置的账号,匹配成功后再建立到后端MySQL的连接。
这意味着每个接入的业务方都需要在Atlas里创建一个对应的账号,密码用加密工具生成后写进配置。我建议为每个业务单独建账号,不要图省事全部共用一个。否则一旦某个应用连接串泄露,所有业务都会被影响,到时候想定位是谁在查询都难。
安全基线也要提前定好。业务连接Atlas后,背后MySQL账号的权限仍然要按最小化原则授予,从库账号只给SELECT权限和必要的会话权限。运维账号和业务账号必须分开,禁止业务侧用运维账号连Atlas。我们当时还做了一件事:全部切换完成后,在MySQL层封掉了绕过Atlas直连数据库的入口。曾经有业务方排查问题,图方便直连主库跑了一个重查询,差点把主库打挂。这不是Atlas能管的事情,是流量治理问题,必须从权限上堵死。
3.2 平滑上线与灰度切换
Atlas最大的吸引力就是应用侧改动小,但“改动小”不等于“直接切”。我们的上线流程是分四步走的:
- 在测试环境搭一套完整的Atlas加一主两从,用生产流量回放或者sysbench压测,确认路由正确、性能损耗可接受。
- 所有业务上线前都要填一张申请表,说明连接池配置、预估QPS、峰值连接数,DBA审核通过后才允许接入。
- 选择一个低峰期、非核心的应用,先改连接串切到Atlas,观察一段时间。主要看主从延迟、从库新增连接数、应用报错日志。
- 如果观察期没有异常,再逐批切换其他应用。一旦发现任何异常,连接串原地改回去就能回退。
这套流程看起来保守,但非常可靠。中间件上线的最大风险不是中间件本身,而是接入姿势不正确引发的次生问题。连接池配置不合理、特殊SQL没测到、账号权限没收紧,这些全都能在一次小流量切换中暴露出来。
3.3 规则矩阵导出:四种必测场景
这里分享一份我们验收Atlas时用的规则矩阵,后来面试中间件岗位我也经常拿这套东西考察候选人。
| 测试项 | 操作方式 | 预期结果 |
|---|---|---|
| 读流量分流 | 连接Atlas执行多次普通SELECT | 从库processlist能观察到这些SELECT |
| 写流量落在主库 | 连接Atlas执行INSERT或UPDATE | 主库出现写入,从库没有 |
| 事务内读 | 在事务中执行SELECT | 该SELECT对应的连接在主库 |
| 强制主库路由 | 执行/*master*/ SELECT ... | SQL出现在主库 |
| 强制从库路由 | 执行/*slave*/ SELECT ... | SQL出现在从库 |
| 后端状态管理 | 执行SELECT * FROM backends | 所有后端状态正常 |
表格之外还必须测三类容易翻车的SQL。第一种是SELECT ... FOR UPDATE,虽然路由识别通常正常,但要确认带锁读一定不会到从库。第二种是SELECT LAST_INSERT_ID(),验证写操作后能取到正确的自增ID。第三种是会话状态类SQL,比如SET NAMES、SET autocommit,这些命令在连接池复用时容易状态串话。
临时表和视图也要单独测。临时表是会话级的,如果应用在一个连接上建了临时表,下一次查询被分到另一台从库,就会直接报“table doesn't exist”。视图还好,所有实例上都有同一份定义,但临时表这个问题必须靠业务规范来规避,代理层救不了。
测完这些,基本上可以确定Atlas在当前业务场景下是稳的。剩下的就是上线后持续观察监控水位。
4. 半年内的三次故障:从连接风暴到主从切换错位
4.1 从库连接数一夜飙升:代理放大了连接问题
第一次大故障发生在一个工作日下午。告警群突然开始刷消息,某台从库的Threads_connected从三百多一路涨到一千五,Atlas所在主机的文件描述符也被占满了。紧接着,部分应用开始报“get connection timeout”。
我当时的排查链路是这样走的:
先在告警机器上执行show global status like 'Threads_connected',确认确实是连接数问题,而不是QPS暴涨。然后登录Atlas管理端口,执行SELECT * FROM backends,发现所有后端状态都正常,但每个后端关联的连接数都偏高。再到从库本机看show processlist,连接来源全是Atlas主机的IP,State大多是Sleep,说明连接不是真的在跑SQL,而是被建立后闲在那里。
最后查到根因:一个刚接入不久的新Java服务,连接池的maximumPoolSize配了50,实例数有40个。这个配置对直连MySQL可能勉强能扛,但经过Atlas后,前端的2000个连接被映射为后端的数千个连接,从库直接被连接风暴打垮。代理层的连接数不是一比一透传,它会放大应用侧的不合理配置。
解决思路也很直接,把这个服务的连接池调小到maximumPoolSize=10,缩短连接空闲回收时间,同时在MySQL侧把wait_timeout调低,让无用连接尽快释放。Atlas所在主机的ulimit -n也一并调大。事后复盘,我们定了一条铁律:任何新应用接入Atlas前,必须申报连接池配置,禁止使用默认最大池。
4.2 主从切换后读到了旧数据:Atlas不感知复制拓扑
第二次故障来得更隐蔽,是在一次主从切换演练中暴露的。演练进行到一半,业务反馈“少量订单读不到最新状态”。主从复制本身没有异常,新主库写流量也正常,但部分读请求还是打在了旧主库上,导致业务侧读到切换窗口前的旧数据。
排查的第一步是看复制状态,确认主从切换成功。第二步是看Atlas的backends表,问题立刻浮出水面——只读后端列表里还是切换前的从库IP组合,可写后端也没有把新主库加进去。也就是说,MySQL层的复制拓扑已经变了,但Atlas层的配置完全没有同步更新。
这个故障让我彻底想明白了一个道理:Atlas不感知复制拓扑。它连“谁是主库”都不认为,只知道配置里写着的可写后端和只读后端是哪些。主从切换这种运维动作,必须同步改Atlas的后端注册,否则代理就会继续按旧拓扑转发流量。
解决方式不复杂,通过管理端口把旧主库从后端列表里摘掉,再把新主库和新从库列表重新注册。但真正值钱的教训是流程上的:我们把整个切换操作做成了脚本,数据库拓扑变更和Atlas后端列表变更放在同一步骤里执行,并在切换后的Checklist中增加一项“30秒内检查Atlas backends表”。从那以后,主从切换演练再也没出过同类事故。
4.3 一条慢SQL拖垮整个从库:路由救不了SQL质量
第三个故障发生在凌晨一点。从库CPU直接打满,慢查询成片堆积,早上的报表接口开始超时,但主库负载很正常,所以能很快排除主库问题。
我登录从库执行show full processlist,看到一条报表SQL已经跑了快两分钟。这条SQL是从Atlas转发过来的,确实是SELECT,路由到从库也完全没有问题。打开Atlas日志确认,它走的就是正常的从库路由分支。
业务方排查后确认,运营后台的统计任务里,两个千万级大表被CROSS JOIN展开,中间结果集大得离谱,跑一次就把从库CPU打满。代理层没有按执行时间熔断的能力,只能眼睁睁看着它把从库拖死。
这个故障的解决分三层。第一层,业务侧停掉任务,把SQL改成按天分批统计;第二层,在从库开启max_execution_time,超过阈值直接终止SQL;第三层,给运营后台的账号单独划分资源限制,不放到公共从库,同时补齐慢SQL监控,超10秒就告警。
复盘结论很扎心也很清醒:读写分离只能缓解“量大”的问题,不能解决“单条SQL重”的问题。从库也不是无限资源,慢SQL治理必须走在中间件前面。不要指望一个轻量代理帮你做熔断,如果真需要熔断能力,应该选ProxySQL这类带规则引擎的方案。
5. Atlas的边界:哪些事它真的做不了
5.1 分库分表不是它的主场
Atlas虽然宣称支持分库分表,但我们用下来只能说“能用,但远远谈不上好用”。配置方式比较拧巴,对分片算法的支持有限,跨分片查询基本指望不上。后来团队真上分片业务时,是在应用层引入了ShardingSphere,Atlas继续保持读写分离的老本行。
如果你现在做技术选型,我的建议是:读写分离场景,Atlas可以胜任;一旦涉及真正的分库分表,不要硬让Atlas上,直接考虑更专业的方案。把中间件用在它擅长的边界内,是运维经验里很重要的一课。
5.2 单点高可用、监控与熔断都要自建
Atlas本身是一个进程,天然是单点。进程挂了,业务连数据库的入口就断了,这个问题必须在架构层解决。我们当时在前面加了LVS做VIP,用Keepalived做健康检查,确保Atlas进程失效后流量漂移到备用节点。如果你所在的云环境不允许用LVS,可以通过DNS或者服务发现做连接串切换,但会有一定的生效延迟。
监控方面,Atlas没有自带的可视化面板,需要自己采集管理端口的状态和进程日志。至少要覆盖四种指标:进程是否存活、端口是否可达、后端backends表状态是否正常、连接数水位是否逼近上限。我们当时还写了一个很小的采集脚本,定期把Atlas状态推给Prometheus,告警规则挂到钉钉群,这样值班的人不需要主动登录机器就能感知异常。
熔断和限流也不要寄希望于Atlas。它没有请求级熔断、没有按SQL执行时长的中止机制,突发流量来了基本靠后端MySQL硬扛。如果你对熔断、限流有强需求,Atlas不在考虑范围内。
5.3 后来我们怎么平滑演进
大概在第三年,新项目逐步换成了云数据库代理和ProxySQL,Atlas开始退居二线,只在老业务上继续服役。迁移过程不算痛苦,因为我们从一开始就给所有业务保留了“连接串只连VIP”的架构,应用侧不感知后端代理的替换。
如果你也在考虑从Atlas迁走,可以参考这个路径:先在Atlas前面加一层LVS/VIP,业务连接串完全不动;再把新的代理实例挂到同一个VIP后面,用流量权重控制灰度比例;最后在低峰期把权重切到新代理,Atlas下线,业务全程无感。真正迁移起来你会发现,最麻烦的不是中间件替换,而是这么多年积累的路由规则和变更Checklist要怎么平移过去。
这也是我为什么一直觉得,代码和配置是可以淘汰的,但团队对流量模型的认知不会贬值。Atlas教会我们的不是“一个老中间件还能跑”,而是当你决定把流量交给某个组件之前,一定要先想清楚它在整条链路上的职责和极限,并把变更预案和监控水位补齐。
如果你正在评估一个类似的轻量中间件,我给的建议只有一句:先在测试环境把普通读、写、事务、特殊SQL、主从切换这五类场景完整跑一遍,把路由预期写成表格,再谈上线。中间件的价值,永远是从你想清楚它的边界那一刻才开始显现的。