基于360 Atlas的MySQL访问中间件实践:从读写分离到分库分表 接手这个库的时候代码仓库里只有一行字atlas。我一开始以为是哪个地图服务的SDK翻完之后才明白这是我们自己起的项目代号——一套MySQL访问中间件。业务从几百万用户涨到几千万主库连接数天天打满慢查询把主从延迟拉到几千秒DBA凌晨三点爬起来杀会话那种场面经历过的人都懂。atlas这个词在技术圈里有太多含义MongoDB的云数据库叫Atlas华为的AI计算平台叫Atlas波士顿动力的机器人也叫Atlas很多人还把它当地图集的代名词。但这篇要写的是一个和开源组件同名的内部项目从选型360 Atlas到在其之上补分库分表能力再到线上踩坑和运维沉淀的完整过程。如果你是后端工程师、DBA或者团队正被单库性能逼到必须拆分的路口这篇文章应该能帮你少走不少弯路。1. 接手这个代号atlas的项目时我先列了一张问题清单1.1 主库被打满的那几周先交代背景。我们是一个典型的读多写少业务用户端每天的读请求高峰期在五万QPS左右真正的写请求只有几千QPS但就是这几千QPS加上大量重复的读SQL把单台MySQL主库钉死在CPU 80%以上。DBA把连接数上限从默认的1000调到了3000结果反而更糟——大量sleep连接占住会话慢查询排着队进场主从复制延迟一度追到半小时。那几周我做过最频繁的操作就是上服务器看SHOW PROCESSLIST然后把某个失控的查询杀掉。一开始的应急方案很简单挂从库让读流量走从库。但问题在于每个服务都是自己在连接串里写死主从地址有的服务读主库有的服务虽然读从库但负载均衡策略完全随缘。上线一个新服务要人肉配一遍连接串出故障要逐个服务排查根本没有统一入口。最痛的一次是某个服务的连接池配置错了把本来应该打到从库的流量全部冲到了主库直接导致主库连接数爆掉页面大面积超时。就是从那个时候开始我们决定做一个统一的数据库访问网关。项目代号我起了atlas理由很直白所有SQL请求经过这个网关由它决定流量到底该走哪台MySQL它就是我们数据库层面的“地图集”。1.2 需求清单与方案选型对比项目立项后我先列需求清单每一条都对应着当时遇到的实际痛点业务接入成本要低最好不逼着所有团队改代码读写分离主库只处理写请求读请求自动分流到从库从库之间能负载均衡不能所有读流量挤在一台从库上连接池复用减少频繁建连对MySQL的冲击后端MySQL故障时能自动摘除恢复后能自动加回后续要具备数据拆分能力不能只解决眼前问题围绕这份清单我对比了三类主流方案客户端分片SDK以ShardingSphere-JDBC为代表、独立数据库代理Mycat、360 Atlas、ProxySQL都属于这一类、云数据库自带的代理产品。三条路线的差异我整理了一张表方案类型工作位置优点缺点适合场景客户端SDK业务应用内功能全、性能好、分库分表能力强要改业务代码、多语言成本高新项目技术栈统一独立数据库代理应用与MySQL之间对应用透明、DBA可控、集中管理多一跳有损耗、需要高可用部署已有大量存量业务不方便大改云数据库代理云端免运维、功能集成绑定云厂商、定制能力受限基础设施全在云上我们最大的约束是存量服务太多而且各团队技术栈不统一所以秒选了Proxy形态。当时考虑过Mycat但Mycat的社区活跃度和稳定程度都让人心里没底考虑过ProxySQL它的读写分离和流量管理做得确实好但我们对MySQL代理的深度定制需求比较多最后选了360 Atlas。选择360 Atlas的原因很实际它基于MySQL-Proxy 0.8.2改造核心逻辑是C语言写的性能好部署轻量读写分离功能成熟而且很多团队在线上用了很多年踩坑经验相对丰富。虽然它的分库分表能力有限但项目第一阶段的目标是把读写分离和连接池先稳定跑起来V1版本不需要一步到位。1.3 为什么选Proxy形态又为什么叫atlas这里多说一句为什么坚持Proxy形态而不是让业务集成SDK。很多技术团队容易犯一个理想主义错误看到ShardingSphere功能强就希望所有业务统一接入。但在现实里老系统的改造代价极其高昂。我们有十几个业务服务语言有Java也有Go如果走SDK路线每个服务的启动流程、配置中心、监控埋点都要改一遍没有两个月的跨团队排期根本下不来而且中间随时可能因为某个服务的一个依赖冲突翻车。Proxy方案则友好得多。对应用层来说它就是一个MySQL实例应用只需要把JDBC连接串里的地址换成Atlas的地址即可。对MySQL来说它又是一个客户端拿着应用账号去后端数据库执行SQL。所有路由逻辑都收口在一层独立进程里DBA和基础架构组可以统一控制这比把路由规则散落在几十个服务里强太多了。atlas这个名字也是从定位出发的。我们团队里有个同事打趣说既然它是流量的地图不如就叫atlas英文本意就是地图集。大家一致觉得贴切就这么定了。后来发现360本身也有个同名开源项目反而成了一件好事面试新人时直接说“我们用的是360 Atlas”对方立刻就能明白这条链路大概是干什么的。2. 360 Atlas能扛住什么、扛不住什么能力边界摸底2.1 它骨子里是什么mysql-proxy的血统在写配置之前有必要先把360 Atlas的技术底子讲清楚。它脱胎于MySQL官方早年推出的mysql-proxy官方版本后来不太维护了360团队在这个基础上做了大量改造把很多生产环境的经验灌了进去。核心是一个事件驱动的网络服务监听在一个端口上模拟MySQL服务端跟客户端完成握手协议之后再把客户端发过来的SQL转发给后端MySQL。整个过程从客户端视角看是透明的客户端以为自己在直接跟一台MySQL对话而实际上SQL经过Atlas时Atlas会先做一层解析决定往哪台后端转发。这个“决定”非常关键它不只是一个TCP层转发器而是能看懂MySQL协议的中间人。所以它能干很多普通LVS或Nginx TCP代理干不了的事比如根据SQL类型区分读写、延迟转发、会话状态跟踪。它的C语言血统带来的好处是性能下限高。我们实测单台2核4G的虚拟机部署Atlas极限处理上万QPS没有问题跟业务后端的MySQL连接保持在几百个以内完全不会成为瓶颈。坏处则是扩展功能门槛高想给它写个插件要么改C源码要么用Lua脚本能玩转的人确实不多。2.2 靠谱的能力读写分离、连接池、故障自动摘除360 Atlas最成熟的功能就是读写分离。配置里有两组后端地址proxy-backend-addresses指向主库proxy-read-only-backend-addresses指向一个或多个从库。主库接收所有写请求从库接收读请求。多个从库之间会做负载均衡默认是轮询方式可以在配置里调整权重。连接池也是它的亮点。没有代理时每个应用跟MySQL之间都是一条独立连接MySQL在连接数超过1000以后性能会明显下降因为每个连接都需要分配线程栈和内存。Atlas这层把后端连接复用起来应用侧成千上万的连接只需要在后端维持少量长连接即可MySQL的压力瞬间就降下来了。这个特性在连接数爆炸场景下救过我们好几次。故障自动摘除和恢复也很有用。Atlas会定时探测后端MySQL的健康状态某个从库挂了它会自动把流量从那个从库上摘掉不会让业务感知到等从库恢复它也能自动加回负载池。这个机制比应用层自研的心跳探活要省心得多而且探测SQL可以自定义避免某些场景下探活SQL本身把数据库打垮。2.3 边界只支持分表不支持跨库分片与分布式事务能力边界这件事我们一开始是低估的。我一直以为360 Atlas和Mycat、ShardingSphere一样能做完整的数据库分片方案实际用下来才发现它的分库分表能力很有限。360 Atlas的分表能力是单库分表也就是把一张逻辑表按某个字段的hash值散列到同一个MySQL实例下的多张物理表里比如user_0到user_31这种。它解决的是单表数据量过大、索引性能下降的问题不解决单库写瓶颈问题。跨库分片也就是把数据分布到多台MySQL服务器上Atlas本身是不支持的。更大的边界在于分布式事务和跨分片查询。数据库中间件普遍做不好的事Atlas也一样做不好跨库join别想分布式事务别想子查询和存储过程的支持也有限制。我甚至遇到过某些带LIMIT和自定义变量的复杂SQLAtlas理解不了就直接透传结果主从不一致时读写分流失效。所以我的建议是把360 Atlas定位成“入口网关连接管理读写分离器”而不是“全能数据中间件”。它做入口非常称职但不要让业务把复杂的分库分表逻辑寄托在它身上。3. 从部署到接入让Atlas跑起来的最短路径3.1 环境规划与安装准备先交代一下我们当时的拓扑。Atlas代理部署在一台独立的2核4G虚拟机规划IP是10.0.0.10。后端MySQL一主一从主库10.0.0.1:3306从库10.0.0.2:3305。业务通过Atlas的10.0.0.10:1234访问数据库管理端口是10.0.0.10:2345。部署前先把编译依赖装好主要需要gcc、make、libevent、glib2、pcre等CentOS环境下用yum一条命令装齐。然后获取360 Atlas源码并编译安装标准安装路径是/usr/local/mysql-proxy。这一步没有太多坑唯一要注意的是选择一个稳定版本不要追最新中间件追求的是稳不是新。3.2 核心配置逐项拆解安装完成后核心工作是配置/usr/local/mysql-proxy/conf/atlas.conf。这里有一段我们当时线上的精简版配置[mysql-proxy] admin-username atlas_admin admin-password atlas_admin_passwd proxy-backend-addresses 10.0.0.1:3306 proxy-read-only-backend-addresses 10.0.0.2:3305 proxy-address 0.0.0.0:1234 admin-address 0.0.0.0:2345 event-threads 4 daemon true keepalive true pwds app_user:这里填加密后的密码 log-level message sql-log REALTIME log-path /usr/local/mysql-proxy/log逐项解释一下proxy-backend-addresses主库地址所有写请求都走这里。可以配置多个Atlas会做一定的负载均衡但我们不建议写库多配主库多写模式复杂度会很高。proxy-read-only-backend-addresses只读从库地址多个从库用逗号分隔读流量会在这些节点之间分发。proxy-addressAtlas对外提供MySQL服务的地址和端口业务连接串就填这个。admin-address管理端口DBA用MySQL客户端连上来查看后端状态、动态调整配置。admin-username和admin-password管理账号密码。pwds应用连接Atlas时使用的MySQL账号密码。注意这里不是明文需要用360 Atlas自带的加密工具生成密文再填入。event-threads事件线程数一般跟CPU核数一致我们2核就配了4稍后压测再调。daemon后台运行。keepalive进程守护异常退出会自动拉起。sql-logSQL日志开关REALTIME表示实时记录每条SQL调试阶段非常好用但生产环境如果开这个选项磁盘IO压力会很大需要权衡。加密密码这一步很容易被忽略。不能用明文直接写在pwds里否则Atlas启动时会解析失败。正确做法是先执行/usr/local/mysql-proxy/bin/encrypt app_user_password拿到一串密文再填进配置。这一步是使用360 Atlas的常识性操作但我在网上看到过不止一个新人在这一步卡住。3.3 启动、自检与流量验证配置写好后启动命令很简单/usr/local/mysql-proxy/bin/mysql-proxy --defaults-file/usr/local/mysql-proxy/conf/atlas.conf启动后第一件事不是让业务接入而是自己先验证链路。用MySQL客户端连Atlas的代理端口mysql -h10.0.0.10 -P1234 -uapp_user -p先执行SELECT 1确认握手成功然后执行一条INSERT再回查确认读写链路都通。需要注意的是在Atlas背后必须提前在MySQL主库上创建好app_user账号并授权对应库表否则连接会被拒绝。接下来验证读写分离是否真的生效。分别在主库和从库上执行SHOW PROCESSLIST能看到同样一个查询连接过来的客户端地址是Atlas代理的IP。再通过Atlas管理端口执行mysql -h10.0.0.10 -P2345 -uatlas_admin -p然后执行SELECT * FROM backends;会看到主库和从库的地址、类型、状态等关键信息。如果状态都是up说明后端节点健康。多跑几个读请求观察从库的Threads_connected能明显看到读流量被均匀分发到从库。几百行binlog同步到07:15是0行说明追平了。共享和临时权限按要求去掉V4源库校验通过源库日志增量接近于0。到这里Binlog同步链路稳定可以放心做一键切换/发版了。到这里链路就通了。这个步骤看起来简单但对后面排查问题意义重大因为一旦业务接入后出问题你必须先确认Atlas自身是好的才能把问题定位到业务SQL或后端MySQL。4. 线上流量进来之后问题开始暴露五个典型故障复盘4.1 “MySQL server has gone away”背后的连接池失效第一个线上问题来得很突然。业务灰度接入Atlas后大约一个小时后应用开始零星报错MySQL server has gone away。刚开始怀疑是应用连接池问题但检查了两轮没找到原因单独用客户端连Atlas执行查询又是好的。排查链路是这样的先看应用日志发现报错的时间点没有规律每隔十几分钟出现一次。再看Atlas日志发现应用连接到来时后端连接已经断开了。最终定位到是连接的生命周期管理问题MySQL的wait_timeout默认是8小时而Atlas连接池里的空闲连接并没有主动做ping保活某些后端连接空闲超过配置时间被MySQL主动断开但Atlas不知道等业务再次使用时才发现连接已死。解决办法分两步一是把MySQL侧主动断开空闲连接的超时时间调大并在Atlas配置里开启连接探测让Atlas定期向后端连接发送探活命令二是调整应用连接池的空闲回收时间让它低于Atlas和MySQL的空闲超时时间形成一个合理的阶梯。这个问题本质上是三层连接池的生命周期没有对齐排查清楚了就是一个配置问题但排查过程非常考验耐心。4.2 主从延迟引发的写后读不一致第二个问题更隐蔽。业务反馈用户刚提交完一个表单下一秒刷新页面数据时有时无。这不是偶发是高发尤其集中在业务高峰期。React审查过程比较传统至少支持IME构图. 最后还是通过整机测试才通过实际上直接看主从延迟监控就能锁定方向。我们在从库的SHOW SLAVE STATUS里看到Seconds_Behind_Master经常在500ms到2s之间波动。用户写入的数据在主库紧接着读取请求被Atlas分发到了从库而从库还没来得及把这条binlog应用完自然查不到。解决方案是多管齐下第一对强一致的读写场景业务侧在查询语句里加一个注释标记Atlas规则识别到标记就把这个查询强制路由到主库第二优化主从复制参数打开并行复制把从库的slave_parallel_workers从默认值调大第三在业务层把“必须读到刚提交数据”的逻辑做一次降级允许短暂延迟才走从库。这个问题的教训是读写分离不是银弹它默认牺牲了一致性。接入代理前一定要在业务层把一致性语义分清楚哪些请求允许最终一致哪些请求必须实时一致否则Dialogue上线第一天就会被打爆。4.3 分表键设计不当导致的流量倾斜分表键踩坑是我们的分库分表设计逐渐深入之后暴露的。刚开始我们按用户ID做hash分片逻辑上很简单shard_id crc32(user_id) % 分片数。上线一段时间后监控发现某个分片所在MySQL的CPU使用率明显高于其他分片而且差距越拉越大。查了路由统计之后发现某些大型客户注册了子账号体系这些子账号都挂在同一个主账号下但我们按子账号ID分片导致所有关联流量都hash到了同一个分片。加上几个头部商户贡献了非常大比例的访问量流量倾斜不可避免。解决这个问题不能只在分片算法上打补丁而是重新思考了分片维度。最终我们把订单这类强关联数据改成按shop_id分片子账号访问时先解析出对应的主账号再用主账号ID做分片路由把同一个商户的数据尽量落到同一个分片。同时引入了一致性hash的虚拟节点方案避免将来扩容分片时全量数据都要迁移。4.4 无分片键SQL被广播慢查询打满这是我这套中间件上线以来最惊险的一次故障。某个晚上监控报警所有MySQL实例的CPU同时飙升从库和主库全都超过了80%。第一反应是遭遇了突发流量但看了流量曲线并没有异常。真相是某个新上线的报表需求写了一条不带分片键的SQL而且是对一张大表的全量扫描。因为路由层不知道这条SQL该去哪个分片默认把它广播到了所有分片。结果就是一条慢查询变成了N条慢查询同时打满所有实例。早该预料到这种问题的但当时确实没有在路由层加“无分片键查询禁止放行”的强校验。排查链路不算复杂慢查询日志里发现同一类型的SQL在多个分片同时出现随后人工确认是缺分片键导致。解决办法是在路由层增加一个SQL预检模块对不带分片键的查询分两类处理一类是允许广播的小表或低频查询白名单控制另一类是聚合报表类SQL原则上不让走在线MySQL引导到离线数仓或专用的汇总库。4.5 Atlas单点故障代理层HA的必要性还有一次故障纯粹是基础架构问题。某个周末Atlas所在机器因为底层宿主机维护触发了重启所有连接瞬间全断整个业务线跟着停了四十分钟。当时我们的Atlas只部署了一个实例应用连接串全部指向它既没有VIP也没有备用节点一台机器挂了等于全线瘫痪。复盘之后做了两个改变。第一部署双Atlas实例两台机器一主一备通过keepalived绑定同一个VIP业务连接串里只填VIP不填任何真实实例地址。第二编写健康检查脚本定期通过Atlas管理端口探测后端状态不仅检测进程本身还要检测它能否正常访问后端MySQL。这样即使单台Atlas进程卡死VIP也能自动漂移到备机业务无感知。这五个故障让我明白一件事数据库中间件的稳定运行不是靠某一个组件扛住的而是靠整条链路上的冗余设计、流量治理和监控覆盖。任何一个环节缺失都会在某个深夜给你上一课。5. 能力不够补丁来凑我们在Atlas之上补的分库分表设计5.1 从Atlas到atlas一套分层架构的演进360 Atlas不支持跨库分片但业务必须拆分怎么办我们把方案做成了分层架构整个项目代号依然是atlas但内部已经不只是360 Atlas这一个组件了。整个架构分三层最上层是接入层也就是360 Atlas本身负责对外提供MySQL协议、读写分离、连接池、故障摘除。中间是路由层我们自研了一个分片路由SDK集成在各个业务服务里负责根据分片键计算出数据落在哪个分片按分片结果去连接对应的Atlas入口或者直接连接对应的MySQL分组。最下层是数据层由多组MySQL主从集群组成每一组承载一个或几个分片。为什么要保留360 Atlas而不全部用自研SDK因为连接池和读写分离已经跑得很稳了没必要重复造轮子。Atlas在不用完整分库分表的前提下作为每个分片组的入口网关依然非常称职。我们等于把它当成了“分片组内的读写代理”而分片路由交给了更灵活的上层SDK。5.2 分片路由怎么设计分片路由是这部分的核心。我们采用了一致性hash算法而不是简单的取模原因很简单取模分片在扩容时需要对存量数据做全量rehash而一致性hash通过虚拟节点能把迁移范围控制在很小的数据段内。简单示例假设我们要分成32个虚拟分片shard_no crc32(shard_key) % 1024 data_node virtual_to_node[shard_no]每个物理MySQL分组承载若干个虚拟分片路由规则维护在配置中心业务SDK启动时加载一份配置更新后通过推送接口刷新。分片键的选择直接决定路由的均匀性。我们的经验是优先选择查询频率最高、离散度最好的业务键不要迷信主键主键虽然唯一但很可能不具备业务上的关联性导致同一个客户的数据散落在多个分片跨分片查询没法避免。5.3 分布式ID、聚合查询与事务的取舍分库分表之后原来单库上的自增主键不再适用因为多个分片的自增ID一定会冲突。我们最终选了雪花算法变种来生成ID64位时间戳加机器号加序列号单机每毫秒可以生成上千个不重复ID。这个方案简单可靠不需要中心化的发号器在分布式环境下也不会产生性能瓶颈。聚合查询是分库分表方案里牺牲最多的地方。原来一条JOIN能处理的事现在必须先确定所有数据都在同一个分片才能执行跨分片的关联查询我们一律在应用层聚合或者用宽表、汇总表、Elasticsearch来承接。比如订单列表需要关联商品信息我们在订单表里冗余了商品名称和快照字段避免高频列表查询必须join商品表。事务取舍也一样。我们只保证单分片事务跨分片业务不启用分布式事务而是采用本地消息表加异步对账的最终一致性方案。早期尝试过两阶段提交性能和实现复杂度都让人难受而且一旦中间件或网络抖动全局事务就会卡住。业务上能接受短暂不一致的用最终一致性方案要清爽得多。这套取舍要从业务出发不要为了技术上的“正统”把所有问题都复杂化。分库分片本身就是一种架构取舍不可能什么都保留。6. 运维阶段沉淀下来的Atlas实用技巧6.1 参数调优event-threads与连接池Atlas跑稳之后调优主要集中在几个参数上。event-threads不建议一味增大我们2核机器初始配4压测后改成2反而更稳原因是线程太多导致锁竞争和上下文切换开销增加。最靠谱的做法是压力测试时观察CPU和延迟曲线找到拐点再定。max-conn参数决定了Atlas最多能接受多少客户端连接。超出的连接会被直接拒绝这可以保护后端MySQL不被洪水一样的连接打垮。但设置时要充分评估业务峰值否则大促期间业务会提前被代理层拒之门外。我们当时把max-conn调到了业务预估峰值的1.5倍留足缓冲。sql-log从REALTIME改成了按需开启。调试阶段实时日志非常有用但生产环境开了之后一个几万QPS的入口会产生大量磁盘写IO而且日志文件增长惊人。现在我们只在排查问题的时候临时打开问题定位完立刻关掉。6.2 日志、监控与告警监控体系是这套中间件能安稳运行的保障。我们主要关注四个指标Atlas活跃连接数、后端MySQL健康状态、主从延迟秒数、SQL错误率。前两个通过Atlas管理端口的backends表轮询拿主从延迟从MySQL的SHOW SLAVE STATUS采集SQL错误率从Atlas日志里按时间窗口统计。Prometheus配合Grafana是标准组合。我们用自研exporter定期请求Atlas的管理端口把backends表数据转成指标MySQL层面用现成的mysqld_exporter。告警阈值设了两档活跃连接数超过峰值的70%触发预警超过85%触发紧急主从延迟超过30秒触发告警SQL错误率在5分钟内超过1%就立刻通知。监控的价值不在于建几个图表而在于故障发生时能不能快速回答三个问题哪里在恶化影响范围多大最近一次变更是什么Atlas的SELECT * FROM backends和日志就是回答这些问题的第一手证据。6.3 变更、扩容与降级预案中间件上线后变更流程比普通业务服务要慎重得多。每次添加新从库我们是先通过Atlas管理端口的动态指令把它加为只读后端观察一段时间负载均衡是否正常再更新静态配置文件。这样做的目的是让变更可回滚尽量避免重启Atlas进程造成连接中断。扩容分片时我们采用先扩虚拟节点再迁移数据的思路。一致性hash的虚拟节点方案让迁移成本可控只需要把部分虚拟分片从一个数据节点移动到新节点复制binlog追平后做一次短暂切换。整个过程对应用是透明的因为SDK里的路由表在配置中心更新后自动生效。降级预案同样重要。我们保留了一条“绕过Atlas直连MySQL”的紧急通道一旦Atlas层出现短时间内无法恢复的问题运维可以把应用连接串从VIP切到后端MySQL实例虽然失去读写分离和负载均衡但至少能保住服务可用。这个通道平时不启用每个季度演练一次。atlas上线到现在大半年我最深的体会是数据库中间件永远只是中间件它解决的是流量治理和连接管理的问题但救不了设计混乱的表结构和没有规范意识的业务SQL。真正让它稳定运行的反而是上线前严谨的分片键设计、上线后成体系的监控覆盖以及每一次变更前都准备好的回滚方案。如果让我给正在折腾同类组件的团队一个建议那就是先把主从延迟监控做起来再考虑上不上代理、拆不拆分——数据库这条链路上的故障几乎都能从延迟指标里嗅到前兆。