☰
自研MySQL代理atlas:从6亿订单分库分表到连接风暴治理
2026/10/19 9:08:05 网站建设 项目流程

前阵子我们订单库的单表数据量冲到了6亿,写入开始明显抖动,DBA半夜打电话让我起来看慢查询。这种"单扛不住、又不敢乱拆"的状态,相信很多做业务的同学都体会过。我们当时面临的选择很简单:要么继续在业务代码里堆分库分表的逻辑,要么在数据库前面加一层统一收口的代理。最后我们选了后者,项目代号就叫atlas,取的是"擎天神"的意思,希望它能把数据库这层重量扛起来。这篇文章就把atlas从立项、设计到上线这一路的关键决策和踩坑记录都摊开讲一讲。

如果你也在纠结三个问题——业务增长到瓶颈怎么拆库、客户端分片方案为什么越做越难受、自研数据库代理到底值不值——那这篇应该能给你一些参考。这里不聊高大上的架构理念,只说我们实际怎么设计、怎么实现、怎么被坑、怎么填坑。

1. 先交代清楚:为什么我们要在MySQL前面加一层atlas代理

1.1 客户端分片方案的维护成本正在失控

早期我们用的是客户端分片方案,每个业务服务里都引入分片组件,分片规则写在各自的配置文件里。一开始业务少,规则也简单,问题不大。但接入的业务线越来越多之后,重复建设和运维成本开始失控,最典型的有三个问题。

第一个是分片规则散落在各个服务里。订单服务、支付服务、积分服务各自维护一套分片规则,规则内容几乎一样但又不完全一样,改一个分片键或者调整哈希策略的时候,需要所有接入方同步改代码、重新发版。每次这种联动变更,总有一两个服务漏掉,然后线上至少出一个数据错乱的事故。

第二个是组件版本无法统一。各业务线为了稳定性不敢随便升级组件版本,导致线上同时存在好几个不同版本。有安全漏洞要修复的时候,就要挨个业务线催着升级,效率极低。

第三个是缺少入口级的治理能力。限流做不到,SQL黑名单做不到,统一的慢查询采集也做不到。出了问题只能在业务日志里一层层翻,排障效率非常低。

正是这三个问题让我们下定决心,把分片逻辑从业务代码里剥离出来,下沉到一个独立的代理层。

1.2 代理模式为什么能解决"透明接入"这个关键问题

客户端分片方案本质上把数据库连接和分片逻辑耦合在业务进程里,而代理模式做的事情很简单:业务方只连接一个逻辑库,完全不感知后端有几个MySQL实例、分了几个表。业务连接串只需要指向代理地址,其他操作和连接普通MySQL没有任何区别。

这个"透明"的价值怎么强调都不过分。新业务接入分片时,不再需要引入一堆依赖、写一堆分片规则、做联调测试,只需要申请一个逻辑库账号,连接atlas就能开始写代码。存量业务迁移时,只需要把连接地址从原来的MySQL改成atlas,数据库层改动对业务完全屏蔽。

另外,所有治理能力都可以在这个入口统一做:SQL解析、权限校验、限流、慢日志、审计,都不需要改动业务代码。我们是先被客户端方案的维护成本折磨够了,才真正体会到代理层这个"统一收口"的价值。

1.3 为什么没有直接使用开源中间件

聊到这里,很多人会问:市面上现成的数据库中间件不少,为什么要自研一个atlas?

我们的判断是这样的:开源中间件确实功能全,但带来的兼容和定位问题在我们这种体量下反而变成了负担。我们内部有自己的一套配置中心、告警系统和发布平台,希望分片规则的配置能直接对接这些系统,而开源中间件往往有自己的一套管理端和配置体系,要改造成本并不低。

其次,我们自己需要的分片策略比较特殊,既要支持范围路由,又要支持哈希取模,希望规则可以热加载而不重启代理实例。在评估了现成方案的拓展机制后,发现完全兼容这些需求会比较费劲。

还有一个现实原因,我们团队当时规模不大,更希望做一个边界清晰、逻辑可控的轻量级方案,而不是引入一个功能大而全、出了问题不好定位的巨型系统。atlas只专注做分库分表和基本的治理能力,其余能力一概不要。事实证明,边界收得越窄,后续做稳定性投入产出比就越高。

2. atlas的模块划分与一条SQL语句的完整旅程

2.1 对外形态与核心模块职责

atlas对外就是一个标准MySQL协议的服务端,端口监听后,业务方通过JDBC或者MySQL客户端就能连上来。整体分为接入层、会话管理、SQL解析层、路由层、执行层和结果集归并层,外加一个元数据与配置管理模块。

各个模块的职责大概是这样的:

模块核心职责
接入层端口监听、MySQL协议握手、认证校验、连接生命周期管理
会话管理绑定后端连接、保存事务状态、维护last_insert_id等会话上下文
SQL解析层词法解析、语法树构建、逻辑表名识别、SQL改写
路由层提取分片键、匹配分片规则、输出精确路由或广播路由结果
执行层并发执行多个分片上的SQL,控制超时和错误处理
结果集归并层多分片结果集排序、聚合、分页、去重后返回给客户端
配置管理从配置中心加载分片规则、全局表清单、账号权限等元数据

2.2 SQL解析与路由:代理层最核心的头脑

当业务连接atlas后发出一条SQL,atlas首先做的是协议层解包,识别出这是一条COM_QUERY消息,然后交给SQL解析层。解析层把SQL语句拆成语法树,识别出涉及哪些逻辑表、哪些字段、where条件里是否有分片键。

以订单表为例,如果业务执行的是:

SELECT * FROM order_tbl WHERE user_id = 12345;

atlas解析后会提取出分片键user_id,然后根据预先配置的分片规则计算出12345应该落在哪个分片,这就是精确路由。如果SQL里没有分片键条件,比如:

SELECT * FROM order_tbl WHERE create_time > '2024-01-01';

atlas不知道去哪找数据,只能走广播路由,把SQL发送到所有分片执行,最后归并结果。广播路由的成本非常高,我们对没有分片键的查询做了一层保护:只有明确在白名单里的查询才允许广播,其余直接在入口拒绝。

2.3 SQL改写:逻辑表到物理表的转变

路由计算出目标分片后,SQL改写层会把逻辑表名order_tbl替换成物理表名,比如order_tbl_0、order_tbl_1,并且自动补充库名前缀。这里有一个容易忽略的细节:如果后端是多套MySQL实例,每个实例上的数据库名可能不一样,所以改写的时候需要自动加上正确的库名前缀,避免跨实例连接时报"database not selected"。

改写完之后,SQL才真正交给执行层。执行层根据路由结果,可能需要同时连接多个后端分片,把改写后的SQL并发发出去。这里的并发度不是越大越好,我们默认控制在4个并发以内,避免大查询把所有分片连接都占满。

2.4 结果集归并与分片算法的选择

多分片查询完成后,最麻烦的是结果集归并。如果是单分片命中,直接把后端结果集透传就行,性能损耗很小。如果是多分片,比如limit分页查询,需要先从所有分片各取足够的行,再在atlas内存里做排序、分页、去重,最后组包返回。

分片算法我们用的是范围加取模的二级拆分思路。第一级先按user_id范围分成若干个分片组,第二级在组内再做哈希取模,这样既能控制单表数据量,又能在未来扩容时只迁移部分分组,不需要全量重建数据。一致性哈希我们也评估过,但工程复杂度明显增加,短期内收益不明确,暂时没有引入。

3. 落地过程中踩得最狠的五个坑,以及完整排查链路

3.1 自增主键返回:JDBC驱动和代理层的多年恩怨

第一个坑来得很快,有业务方反馈,插入订单之后拿不到自增主键ID,直接返回了0。

走代理之后,插入语句被改写,可能命中一个分片也可能命中多个分片。如果只命中一个分片,问题还不大,把后端返回的last_insert_id透传即可。但如果是多个分片都执行了插入,每个分片各自生成自增ID,就必然撞主键。

排查链路走了一遍:先看协议层,确认代理确实收到了后端返回的OK包;再看会话管理,确认last_insert_id有没有被正确保存。最后发现,JDBC驱动在连接属性里如果设置了useGeneratedKeys=true,会额外发送一条查询,代理层如果没有把这个参数和会话状态关联起来,就会丢掉。

最后的处理方案是我们完全放弃了自增主键,业务统一改成snowflake算法生成的分布式ID。代理层保留了自增主键透传能力,只给少量非核心配置表用。分库分表环境下自增主键终究是伪需求,越早切换到全局唯一ID生成方案,越不会在这里浪费时间。

3.2 MySQL 8.0默认认证插件不兼容:握手阶段直接挂掉

测试环境把MySQL升级到8.0之后,业务反馈页面全挂,报错信息直接指向认证失败。

MySQL 8.0默认的认证插件是caching_sha2_password,而早期版本的客户端驱动不认这个插件,握手阶段直接拒绝。问题发生在atlas接入层和后端连接层两边:业务侧连atlas时,atlas需要支持客户端能用的认证方式;atlas连后端MySQL时,又要让后端8.0接受atlas的认证方式。

排查过程不是特别复杂,打开atlas调试日志看到handshake报文里认证插件名是caching_sha2_password,就知道问题出在哪了。修复方案是两边的插件适配都做上:接入层优先支持mysql_native_password,同时兼容caching_sha2_password;后端连接层支持到8.0的caching_sha2_password完整握手流程。

如果后端服务暂时不方便升级驱动,也可以在MySQL里创建一个使用mysql_native_password的账号,避免整个集群都受影响。但从长远看,尽早让驱动和代理层完整支持新认证协议才是正解。

3.3 全局表JOIN:SQL改写的一个隐蔽陷阱

分库分表之后,字典类表我们不希望每个业务各自copy一份,而是做成全局表,在每个分片上都放一份同样的数据。订单表join商品类目表就是这样一种典型场景,订单表按user_id分片,类目表是全分片复制。

坑在于,atlas的SQL解析器第一次遇到这种join时,没有识别出类目表是全局表,只按订单表的分片键路由,结果执行时其他分片上没有对应的类目物理表,直接报"table not exists"。

排查链路倒是清晰:错误信息直指物理表缺失,随后检查元数据配置,发现全局表清单只在某个配置项里维护,而新加的类目表忘了登记进去。修复并不难,但更重要的是从根本上解决——atlas的元数据模块在启动时要自动拉取所有逻辑库的全局表清单,而不是靠人工去配置中心维护。

另外还有一个隐含的坑:join里如果同时命中多分片,结果集归并时排序字段如果涉及全局表的列,需要额外做映射归一,否则group by得到的结果集会不正确。这类问题在单库时代永远不会暴露,只有做过多分片归并才会理解。

3.4 limit offset穿透:深分页把所有分片都拖下水

分页查询翻到很深的页数时,问题就来了。比如每页10条,用户翻到1000页,对应offset是9990。程序逻辑上,atlas需要从每个分片取9990+10条数据,在内存里归并排序后再丢掉前面的9990条。这个低效操作在高频请求下会拖垮整个代理层的CPU和内存。

更麻烦的是,这种查询产生的后端压力是成倍的:表面上用户只看一页,后端每个分片都在为这一页做大量无用功。

我们的处理方案是双管齐下。第一,归并层加保护,单个请求offset加limit超过10000时直接拒绝。第二,引导业务改成游标分页,也就是where id大于上页最大ID之后limit 10,这种分页方式天然规避了深翻页问题。

这个坑本质上是所有分库分表中间件都逃不掉的,客户端分片方案里也存在类似问题,只是代理层做了统一收口后,问题被放大到了所有后端分片。

3.5 连接风暴:一次大促把后端MySQL连接打满的复现与修复

最严重的一次线上事故发生在一次大促预热期间,业务侧连接池参数被调大后,atlas开始源源不断地向后端MySQL建立连接,后端连接数暴涨,最终直接耗尽。整个排查链路是这样的。

第一步发现告警:MySQL实例连接数告警,基础监控里看到连接数曲线直接突破阈值。第二步看看连接来源,发现全部来自atlas代理节点,但没有明显的业务QPS增长,说明问题出在连接管理自身。第三步查atlas日志,发现后端连接池正在大量回收空闲连接,而每次回收后,紧接着的新请求都要新建连接,建连速度跟不上请求速度时,连接池会继续创建更多连接,形成恶性循环。

核心原因有两个:一是后端连接池空闲回收时间设置小于MySQL的wait_timeout,导致连接被前端主动回收,但请求还在继续进来;二是没有控制并发建连速率,连接释放和新创建完全失控。

修复方案我们加了三个机制。第一是连接预热,代理启动时预先创建minIdle个后端连接。第二是新建连接限速,每秒最多新建不超过50条连接,避免突发建连压垮MySQL。第三是快速失败保护,如果后端健康检查失败率超过阈值,直接熔断,拒绝新建连接,保护后端数据库。

这里整理一下连接池相关的关键参数,方便参照:

参数建议值说明
minIdle10每个后端节点的最低空闲连接数
maxActive50每个后端节点的最大连接数上限
maxWait3000ms获取连接的超时时间,超过直接失败
idleTimeout300s空闲回收时间,必须小于后端wait_timeout
connectionTimeout5000ms建立物理连接的超时时间
validationQuerySELECT 1获取连接时的健康检查语句

4. 从压测到全量切换:我们怎么证明atlas可以接生产流量

4.1 压测方式与性能损耗数据

atlas上线之前,我们做了三轮压测,用的工具组合是sysbench加JMeter,同时搭了一个简单的线上流量回放工具来模拟真实业务请求。

第一轮压测目标是测功能正确性,只看SQL改写和路由结果是否符合预期。第二轮压测目标才是性能,对比直连MySQL和走atlas两种方式的QPS和RT差异。第三轮是稳定性压测,持续跑12小时,观察连接数、内存、GC和慢查询曲线。

实际测试结果:简单点查场景下,atlas的RT损耗大约在2%到3%之间,基本可以忽略;复杂聚合查询场景下,损耗会到8%到10%,主要原因在于多分片结果集需要在代理层内存里做归并。这个损耗在我们可接受范围内,毕竟换来了分片能力和统一治理能力。sysbench压测命令大致长这样:

sysbench --mysql-host=127.0.0.1 --mysql-port=3307 --mysql-db=logic_db \ --tables=8 --table-size=1000000 --threads=64 \ oltp_read_write run

4.2 灰度切换的三个阶段

全量切换之前,我们设计了三个阶段的灰度策略,每一步都有回退方案。

第一阶段是影子流量,把线上真实的请求通过TCP层复制一份打到atlas,但是不把结果回写业务,纯粹验证atlas在真实流量下的行为和稳定性。这个阶段发现了很多模拟压测发现不了的问题,比如部分客户端版本发送的握手报文跟标准协议有一些细微差异,只在真实流量下才能暴露。

第二阶段是只读业务切换,选择几个低峰期读多写少的业务线,把连接地址切换到atlas,观察一周,重点看慢查询、错误率、连接数稳定性。这个阶段没有出现大问题,主要是一些业务SQL用了不常见的语法,导致atlas解析失败,需要把对应SQL加进兼容白名单。

第三阶段才是写流量逐步切换,先选一个业务线,在凌晨低峰期完成切换,观察两个大促周期后,再推广到其他业务线。切换过程不涉及双写,因为atlas对业务是透明的,业务只需要改host和port,逻辑库名和账号都不变,切换失败时回退也很快。

4.3 上线后那个诡异的凌晨3点断连问题

全量切换完成后的第二周,监控发现每天凌晨3点左右,总是有少量连接报"Communications link failure"。数量不多,但每天都很规律。

排查过程是这样的:先在atlas日志里看到大量空闲连接被回收的标记,再对比MySQL端wait_timeout参数,发现问题出在DBA把wait_timeout从默认28800秒调成了3600秒,而atlas连接池的空闲回收时间初始设置成了4000秒。一个连接空闲超过3600秒时,会被MySQL端主动断开,但atlas连接池不知道,等到有请求来的时候,拿出去用才发现连接已经断了。

修复方案很简单:把atlas连接池的idleTimeout下调到300秒,并且开启获取连接前的validationQuery。但这个坑的教训很深刻——代理层和后端数据库的会话参数必须做联动核对,任何一个参数不匹配都会在最意想不到的时间点冒出来。

5. 复盘:atlas里值得保留的设计与下次会重写的部分

5.1 过去一年里,被验证有效的三个设计

第一个是分片规则与配置中心解耦。分片规则的任何修改,在配置中心变更后atlas可以在秒级完成热加载,不需要重启代理节点。这个设计让扩容和分片策略调整的运维成本大幅降低。

第二个是SQL黑名单和慢日志在入口统一治理。以前业务代码里出慢查询,要定位到具体服务,再翻日志。现在atlas直接采集所有经过代理的慢SQL,按照逻辑库、账号、业务线分类,排障效率明显提升。

第三个是后端连接多路复用。atlas作为代理层,业务连接数和后端连接数是解耦的,多个客户端连接可以共享后端连接,在整体连接管理上对MySQL后端非常友好。以前直连模式下,每个业务实例都会占用一批后端连接,现在总量受到严格控制。

5.2 如果再让我做一次,我会重新设计的三个部分

第一,规则引擎不应该手写解析。当前规则加载模块在处理复杂分片表达式时偶尔会出现边界解析错误,虽然每次都修复了,但维护成本不低。如果重做,我会直接用ANTLR生成AST,把SQL解析和规则匹配拆成独立模块,再叠加白名单机制。

第二,预处理语句支持度不够。为了省事,早期把COM_STMT_PREPARE直接转成了COM_QUERY,导致预编译占位符的SQL没有走到MySQL预处理语句缓存,高并发场景性能下降。这个改起来工作量很大,但如果一开始就按预处理协议支持来设计,后面省的事会多得多。

第三,缺少完整的SQL审计日志。合规和排障都需要知道某个时刻谁执行了什么SQL,当前实现只保留慢查询日志,完整审计数据没有落库。这是一个很值得补齐的能力。

5.3 atlas还能往哪走:从分片代理到数据库网关

atlas现在的定位还只是一个分库分表代理,但它的架构位置决定了它可以继续发展成一个完整的数据库网关。后面可以做的方向包括统一连接管理、账号权限收口、SQL治理、索引建议、Binlog解析,甚至多活场景下的流量调度。

我个人体会最深的一点是,自研代理层这件事,最有价值的产出其实不是atlas本身,而是整个团队对MySQL协议、连接管理、执行计划这些基础能力的理解深度。这些能力在后续排查任何数据库问题时都会反复用到,属于非常划算的技术投资。

最后分享一个小建议:如果团队规模不大,中间件运维人力有限,优先用成熟方案;如果已经到了一定规模,自研一个边界清晰的轻量代理是值得的,但一定要从第一天就把可观测性和优雅退出的方案设计进去,而不是等项目上线了再补。命名不重要,叫atlas还是叫别的都行,重要的是它真正解决业务的问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询