
如果没有那次大促前的容量评估我大概不会想到一个以神话巨人命名的老中间件会在我们的生产环境里一跑就是三年。当时数据库层的数字已经很不好看了读流量是写流量的十倍出头主库高峰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做一轮真实压测。维度应用层双数据源ShardingSphereAtlasMyCATProxySQL改动量高中高低中中多语言支持每个语言都写一遍仅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默认autocommit1很多业务代码里其实没有显式BEGIN而是通过多条语句自然形成“隐式事务”。但只要应用代码里有BEGIN或START TRANSACTIONAtlas就会在会话级别做标记这个连接上的所有后续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里填的是从库列表多个从库用逗号分隔。密码不能放明文要用工具生成加密串。管理端口非常实用连上之后可以执行管理SQLmysql -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 doesnt 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主机的IPState大多是Sleep说明连接不是真的在跑SQL而是被建立后闲在那里。最后查到根因一个刚接入不久的新Java服务连接池的maximumPoolSize配了50实例数有40个。这个配置对直连MySQL可能勉强能扛但经过Atlas后前端的2000个连接被映射为后端的数千个连接从库直接被连接风暴打垮。代理层的连接数不是一比一透传它会放大应用侧的不合理配置。解决思路也很直接把这个服务的连接池调小到maximumPoolSize10缩短连接空闲回收时间同时在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虽然宣称支持分库分表但我们用下来只能说“能用但远远谈不上好用”。配置方式比较拧巴对分片算法的支持有限跨分片查询基本指望不上。后来团队真上分片业务时是在应用层引入了ShardingSphereAtlas继续保持读写分离的老本行。如果你现在做技术选型我的建议是读写分离场景Atlas可以胜任一旦涉及真正的分库分表不要硬让Atlas上直接考虑更专业的方案。把中间件用在它擅长的边界内是运维经验里很重要的一课。5.2 单点高可用、监控与熔断都要自建Atlas本身是一个进程天然是单点。进程挂了业务连数据库的入口就断了这个问题必须在架构层解决。我们当时在前面加了LVS做VIP用Keepalived做健康检查确保Atlas进程失效后流量漂移到备用节点。如果你所在的云环境不允许用LVS可以通过DNS或者服务发现做连接串切换但会有一定的生效延迟。监控方面Atlas没有自带的可视化面板需要自己采集管理端口的状态和进程日志。至少要覆盖四种指标进程是否存活、端口是否可达、后端backends表状态是否正常、连接数水位是否逼近上限。我们当时还写了一个很小的采集脚本定期把Atlas状态推给Prometheus告警规则挂到钉钉群这样值班的人不需要主动登录机器就能感知异常。熔断和限流也不要寄希望于Atlas。它没有请求级熔断、没有按SQL执行时长的中止机制突发流量来了基本靠后端MySQL硬扛。如果你对熔断、限流有强需求Atlas不在考虑范围内。5.3 后来我们怎么平滑演进大概在第三年新项目逐步换成了云数据库代理和ProxySQLAtlas开始退居二线只在老业务上继续服役。迁移过程不算痛苦因为我们从一开始就给所有业务保留了“连接串只连VIP”的架构应用侧不感知后端代理的替换。如果你也在考虑从Atlas迁走可以参考这个路径先在Atlas前面加一层LVS/VIP业务连接串完全不动再把新的代理实例挂到同一个VIP后面用流量权重控制灰度比例最后在低峰期把权重切到新代理Atlas下线业务全程无感。真正迁移起来你会发现最麻烦的不是中间件替换而是这么多年积累的路由规则和变更Checklist要怎么平移过去。这也是我为什么一直觉得代码和配置是可以淘汰的但团队对流量模型的认知不会贬值。Atlas教会我们的不是“一个老中间件还能跑”而是当你决定把流量交给某个组件之前一定要先想清楚它在整条链路上的职责和极限并把变更预案和监控水位补齐。如果你正在评估一个类似的轻量中间件我给的建议只有一句先在测试环境把普通读、写、事务、特殊SQL、主从切换这五类场景完整跑一遍把路由预期写成表格再谈上线。中间件的价值永远是从你想清楚它的边界那一刻才开始显现的。