ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

分布式数据库代理原理与选型实战:从SQL解析到分库分表落地

分布式数据库代理原理与选型实战:从SQL解析到分库分表落地 分布式数据库代理这个词近几年在数据库圈子里出现的频率越来越高。如果你的团队正打算把单机数据库往分布式架构迁移或者已经完成了分库分表但被路由规则散落、连接管理混乱、读写分离难统一这些问题反复折磨那么分布式数据库代理大概率是你绕不开的一层基础设施。它本质上是个介于应用和后端数据库集群之间的中间服务负责把应用发来的SQL请求解析、改写、路由到正确的存储节点再把多个节点的返回结果合并成一份“像单库一样”的响应交还给应用。一句话概括它让应用只面对一个入口却能使用整个分布式集群的存储能力。这篇文章我会从“为什么要引入代理层”讲起逐步拆解一条SQL请求在代理内部的完整旅程然后对比主流的开源选型包括ShardingSphere-Proxy、Vitess、MyCat这批常见方案最后用完整的配置示例演示如何落地一套分片代理并分享日常运维中积累的排查思路。适合正在做分布式数据库选型、准备上线分库分表或者打算在已有架构中引入数据库代理的同学参考也顺手给那些想在中间件层面做二次开发的朋友提供一点方向感。1. 为什么需要分布式数据库代理分库分表之后新的瓶颈出现了1.1 分库分表本来是为了解决问题结果带来了新麻烦单机数据库的边界是很现实的CPU、内存、磁盘IO、连接数任何一个到了瓶颈整个业务都会跟着遭殃。早年大家解决这个问题的思路也比较简单粗暴——分库分表。按照业务维度把数据拆到多台机器上每台机器只承担一部分读写压力容量和并发都能线性扩展。这个思路本身没问题但落地过程中绝大多数团队都会踩进同一个坑分片逻辑写在了业务代码里。我见过不少业务系统数据源配置一大片一个Service里switch-case判断用户ID落在哪个库哪张表查询的时候先手动算分片号再拼接表名。刚开始分两三个库的时候还能忍一旦业务增长分片规则调整、新服务接入、多个团队各自维护一套路由逻辑立刻变成灾难。最常见的问题有这几类路由逻辑和业务代码强耦合分片规则调整必须全量发布业务服务。不同服务之间对分片规则的理解不一致经常出现查错库、join跨分片的全表扫描。每个服务自己维护连接池后端数据库连接数被撑爆。跨分片查询、排序、分页、聚合逻辑在应用层手工编码写起来又慢又容易出错。这时候大家开始反应过来需要把“数据怎么分布、请求怎么路由”这件事从业务代码里整体抽离出来放到一个独立的中间层去治理。这就是分布式数据库代理出现的最直接动因。1.2 代理层管了哪些事从连接管理到结果聚合分布式数据库代理做的事可以归纳成几块连接管理、SQL解析与改写、分片路由、结果合并、读写分离、分布式事务协调以及流控和治理。连接管理方面代理层对外提供标准数据库协议应用只管连它不需要知道后端有多少个数据库实例。代理内部维护连接池把后端的连接复用起来避免每个业务连接都直接占用数据库连接。这一点在连接数敏感的业务里尤其有用。SQL解析与改写是代理层最核心的能力。代理收到一条SQL不是直接转发而是先做词法分析、语法分析生成抽象语法树从中提取出涉及的表名、查询条件、排序字段、聚合函数等信息然后根据分片规则决定这条SQL应该发到哪些分片并改写SQL里的表名、条件、limit等细节。分片路由则是把改写后的SQL分发到对应的后端分片上去执行。路由之后还有结果合并把多个分片返回的结果集做排序、聚合、翻页最后输出一份完整的结果。这部分我会在第二章拆开细讲。读写分离也经常由代理层顺带接管。主库负责写从库负责读代理根据SQL类型自动分流甚至支持配置延迟阈值、强制主库路由之类的策略。1.3 代理层不是银弹它的代价与边界把路由逻辑抽到代理层确实解决了业务代码里的很多脏活但它也有自己的代价。最直观的一点是所有SQL请求都多了一跳网络开销。虽然代理通常和后端数据库部署在同一内网延迟影响有限但高频小请求的场景下处理耗时仍然会比直连数据库高一些。另外代理层自身的稳定性会直接影响整个数据面。一旦代理节点出问题所有应用都会受影响。所以生产环境里代理节点必须做高可用至少两个实例挂在前置负载均衡后面还要配套监控和告警。另一个容易被忽略的边界是代理不是万能的SQL兼容层。复杂的自定义函数、存储过程、某些特殊语法经过解析改写后可能会失效或者行为不一致。我自己在选型时有一个原则如果团队的业务SQL相对规范、以CRUD为主代理层能发挥很大价值如果大量使用复杂存储过程、自定义函数、深分页、重度join那要么先做SQL治理要么对代理的能力边界有清醒认知。2. 核心原理拆解一条SQL请求在代理内部走完的完整流程2.1 协议解析与SQL引擎先“听懂”客户端在说什么代理层对外看起来就是一个数据库所以它首先要能在协议层面和客户端对上话。无论是MySQL协议还是PostgreSQL协议都要完整实现握手、认证、查询、事务控制、预编译语句这些交互过程。完成协议层的交互之后代理拿到的是一个SQL字符串。这时候就要做核心的SQL解析。常见的实现方式是用ANTLR这类解析器按照数据库方言的词法规则和语法规则把SQL拆成Token流再构造成一棵抽象语法树也就是AST。AST里包含了这条SQL的完整结构信息比如查询了哪些表、where条件里有哪些字段、用了什么聚合函数、有没有order by和limit。这里有一个关键点代理层的SQL解析开销是比较高的尤其是复杂的查询语句。为了减少解析重复带来的性能损耗成熟的代理方案都会引入解析缓存、预编译支持以及执行计划缓存。预编译语句的使用可以显著降低SQL解析频次这也是为什么我在生产环境一直推荐应用层尽量用预编译方式访问数据库。2.2 分片路由与SQL改写让查询精确落到正确分片解析完SQL之后接下来就是路由决策。路由决策的核心依据是分片键。分片键通常选择业务中天然分布均匀且查询频率很高的字段比如用户ID、订单ID。常见的分片算法有几种哈希取模比如order_id % 16优点是数据分布均匀实现简单缺点是一旦分片数量需要扩容数据迁移和重新路由的成本较高。范围路由比如ID在1到1000之间的数据落到分片01001到2000落到分片1优点是范围查询友好扩容相对容易缺点是热点数据可能集中在部分分片。时间路由比如按月份分表适合日志、流水这类数据但单月数据量巨大时需要配合其他策略。一致性哈希迁移影响面小但实现复杂度高代理层方案里相对少用。路由只是第一步真正细节多的是SQL改写。一条带分片键的查询比如“SELECT * FROM t_order WHERE order_id 1024”代理解析出order_id 1024会计算出目标分片然后把逻辑表名改写成物理表名再把请求发到对应的后端分片。如果是INSERT代理要确保主键生成策略和分片键赋值提前完成才能在写入前计算分片位置。最危险的情况是查询条件里没有分片键。比如“SELECT * FROM t_order WHERE status 1”代理无法从这条SQL里定位分片只能把所有分片都查一遍再把结果合并。数据量一大这种全路由查询就会变成性能炸弹。所以生产环境的做法通常是两个方向要么在路由配置上强制要求写SQL必须携带分片键要么在SQL规范层面加一道拦截把无分片键的查询路由到一张汇总表或直接拒绝。2.3 结果合并与连接管理把多个分片的答案拼成一份完整响应SQL被分发到多个分片执行之后代理还要做一层结果合并。这一层逻辑比很多人想象的复杂。首先是数据的拼接把多个分片返回的结果集合并成一份返回给客户端。其次要处理排序如果查询里有order by代理不能简单合在一起而是要让每个分片先各自排序再在代理层做归并排序。聚合函数也需要特殊处理比如SUM、COUNT可以直接在每个分片上算然后代理做二次累加但是AVG不能直接拿分片的平均值再求平均必须让每个分片同时返回SUM和COUNT代理再计算整体平均值。分页操作是另一个容易出问题的场景。假设应用想取第1000条到1010条数据SQL里有limit 1000, 10。如果直接让每个分片都返回1000到1010条代理合并后取偏移结果可能是错的。正确的做法是每个分片先返回offsetlimit数量也就是1010条代理层合并后从第1000位开始截取10条。这带来的问题是offset越深每个分片返回的数据量越大网络传输和代理内存开销全涨上去。所以深分页场景在分布式代理体系里是非常禁忌的一般建议改用游标、滚动ID或者时间范围来替代。连接管理层面代理作为中间层同时维护两个连接池一个是和客户端之间的前端连接池一个是和后端数据库之间的后端连接池。后端连接池的容量、超时、生命周期参数直接决定代理的整体吞吐和数据库的负载。连接复用做得好的代理能够用很有限的后端连接数支撑起成倍的前端并发请求这就是代理存在的另一个核心价值。3. 主流方案选型对比ShardingSphere-Proxy、Vitess、MyCat该怎么选3.1 ShardingSphere-Proxy兼容MySQL和PostgreSQL协议的无状态代理Apache ShardingSphere是目前国内乃至全球使用范围都很广的分布式数据库中间件生态ShardingSphere-Proxy是其中的独立代理模式。它以一个独立进程运行对外提供标准的MySQL和PostgreSQL协议应用可以像连接普通数据库一样连接它不需要引入任何客户端依赖对多语言场景特别友好。ShardingSphere-Proxy的核心能力覆盖了分库分表、读写分离、数据加密、脱敏、影子库压测等场景配置体系采用YAML规则清晰文档完善社区活跃。它在架构上的一个优势是支持规则热加载部分配置可以在不重启进程的情况下调整这在运维层面很实用。另外ShardingSphere生态里还有JDBC模式和Sidecar模式。JDBC模式是嵌入应用内部的轻量级方案性能开销更小但受限于Java语言Proxy模式则天然适合异构语言团队。实际项目中两种模式可以结合使用Java服务走JDBC模式获得更低延迟跨语言服务统一走Proxy模式。3.2 Vitess为超大规模分片而生的云原生方案Vitess最早是YouTube为了解决MySQL扩展性问题而开发的后来成为CNCF的孵化项目在云原生环境里非常有名。它本身是一套完整的数据库编排系统核心概念包括Keyspace、Vindex、Tablet其中Vindex就是数据分布的路由索引类似于分片键和路由算法的组合。Vitess对超大规模数据场景的支持非常成熟能够做到动态分片、自动迁移、分布式连接、查询路由等能力。如果你所在的公司已经有比较强的Kubernetes运维能力并且业务增长到了一定体量Vitess是一个值得重点考察的方向。但也要客观说Vitess的架构复杂度和学习曲线明显更高。它不只是把一个代理进程部署上去就行涉及VSchema的规划、Tablet的管理、拓扑结构的设计对DBA和运维团队的要求比较高。对中小规模的业务来说整套方案的维护成本可能超过它能带来的收益。3.3 其他中间件与选型决策建议国内早期做分库分表中间件很多人会想到MyCat。MyCat 1.x在2016年前后用得比较多技术栈偏老2.0做了重写但社区迭代节奏一直不太稳定。如果存量系统已经跑在MyCat上能不迁移可以先不迁移如果是新项目选型我个人不太建议把MyCat作为首选。还有一类代理比如ProxySQL、MaxScale它们更多聚焦在读写分离、负载均衡和故障切换不具备分片路由能力。它们通常作为MySQL架构前端的流量治理层来用和分库分表中间件是互补关系不是替代关系。选型决策上我一般会按这几个维度权衡团队主流开发语言是Java还是多语言混合数据规模是否到了需要数百个分片以上的超大规模阶段对SQL透明性的要求有多高运维能力是否足以支撑复杂的分布式编排系统。用一张表总结会更直观方案定位协议支持分片能力学习成本适用规模ShardingSphere-Proxy通用分布式数据库中间件MySQL、PostgreSQL等强配置灵活中等中小到大规模Vitess云原生数据库编排平台MySQL强超大规模较高超大规模MyCat传统分库分表中间件MySQL一般较低中小规模存量系统ProxySQLMySQL流量治理MySQL无分片低与分片中间件配合使用4. 实操用ShardingSphere-Proxy在15分钟内部署一套分片代理4.1 环境准备与基础部署部署ShardingSphere-Proxy的第一步是准备环境。它基于Java开发JDK 8以上版本即可软件包可以直接从Apache官网下载二进制发行版解压之后的目录结构很清晰bin目录存放启动脚本conf目录存放配置文件ext-lib目录用于扩展依赖。我常用的单机测试规划是准备两个MySQL实例分别在两台机器或者同一台机器的不同端口各建一个空库模拟两个物理分片。代理进程占用3307端口应用连接代理时就像连接普通MySQL一样。基础部署的几个关键操作下载shardingsphere-proxy二进制包解压到指定目录。检查conf/server.yaml中的认证信息设置合理的用户名和密码。确认conf/config-sharding.yaml中的数据源连接串、用户名密码与实际MySQL实例一致。使用bin/start.sh启动然后查看日志确认进程正常监听3307端口。启动完成之后先用MySQL客户端连一下代理端口能正常进入MySQL命令行说明协议层已经通了。4.2 核心配置拆解逻辑库、数据源与分片算法ShardingSphere-Proxy的配置核心是YAML文件下面这套配置覆盖了逻辑库、数据源、分片规则三块内容也是我平时搭建测试环境最常用的模板schemaName: sharding_db dataSources: ds_0: url: jdbc:mysql://192.168.1.10:3306/ds_0 username: root password: 123456 connectionTimeoutMilliseconds: 30000 idleTimeoutMilliseconds: 60000 maxLifetimeMilliseconds: 1800000 maxPoolSize: 50 minPoolSize: 1 ds_1: url: jdbc:mysql://192.168.1.11:3306/ds_1 username: root password: 123456 connectionTimeoutMilliseconds: 30000 idleTimeoutMilliseconds: 60000 maxLifetimeMilliseconds: 1800000 maxPoolSize: 50 minPoolSize: 1 rules: - !SHARDING tables: t_order: actualDataNodes: ds_${0..1}.t_order tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: order_mod keyGenerateStrategy: column: order_id keyGeneratorName: snowflake bindingTables: - t_order, t_order_item broadcastingTables: - t_config shardingAlgorithms: order_mod: type: MOD props: sharding-count: 2 keyGenerators: snowflake: type: SNOWFLAKE这里有几个配置点需要重点理解。actualDataNodes定义了逻辑表t_order对应的物理表和分片位置ds_${0..1}.t_order的意思是ds_0和ds_1两个数据源下都有一张同名的t_order表。tableStrategy里的shardingColumn指定分片键为order_idshardingAlgorithmName引用下面的order_mod算法这个算法是对分片数取模sharding-count设置为2。keyGenerateStrategy负责主键生成使用雪花算法生成分布式唯一ID。这一步非常重要因为如果主键没有在代理层提前生成插入SQL就无法计算分片位置。binddingTables配置了关联表之间的绑定关系t_order和t_order_item绑定之后两个表在join查询时可以按照同一分片键路由到同一个分片避免跨分片join。broadcastingTables则把t_config配置为广播表每个分片都保留一份全量数据适合字典表、配置表的场景。4.3 连接代理、建表与路由验证配置好之后重启代理进程用MySQL客户端连接mysql -h127.0.0.1 -P3307 -uroot -p进入代理的MySQL命令行后可以先用SQL语句创建逻辑表对应的物理表。ShardingSphere的规则只负责路由和改写不会自动在分片上帮你建表所以需要预先在ds_0和ds_1每个库中分别执行建表语句物理表名和逻辑表名保持一致。表建好之后开始验证路由。连续插入几条订单数据观察数据是否均匀落在两个分片。如果一切正常执行带分片键的查询SELECT * FROM t_order WHERE order_id 1024;这条SQL会被路由到唯一确定的分片性能表现和单机查询没有本质差别。然后执行不带分片键的查询SELECT * FROM t_order WHERE status 1;这条SQL会触发全路由向所有分片发起查询再把结果合并。用PREVIEW语句可以查看SQL的改写和路由结果PREVIEW SELECT * FROM t_order WHERE order_id 1024;输出会明确告诉你这条SQL实际会被发往哪个数据源哪个分片这对排查路由问题非常方便。生产环境里我也建议在调试SQL时多看PREVIEW结果避免想当然。需要说明的是PREVIEW本身也会经历完整的解析和路由过程频率过高会消耗代理性能按需使用即可。4.4 上线前容量评估与高可用清单测试环境跑通之后真正上线前还有一堆事情要做。第一件事是容量规划。分片数量不能随便定要根据单分片能承受的数据量和请求量反推。比如预估半年后总数据量是800GB单个MySQL实例承载200GB比较稳妥那就至少需要4个分片再结合QPS估算取其中较大的值。分片键的选择要格外慎重一旦业务上线数据落盘再换分片键会涉及全量数据迁移成本极高。第二件事是代理节点的高可用。ShardingSphere-Proxy本身是无状态的多个代理实例之间不共享会话数据所以高可用方案比较简单直接部署两个或更多的代理节点前端挂负载均衡比如LVS、HAProxy或者云上的负载均衡服务。应用访问的是负载均衡的VIP而不是某个具体代理节点这样单节点故障时流量可以自动切走。第三件事是配套监控。代理层需要监控的指标包括QPS、连接数、后端连接池使用率、SQL路由失败次数、进程GC情况、节点CPU和内存使用率。推荐保留一份慢查询日志把处理耗时超过阈值的SQL单独捞出来分析。上线前做一轮压力测试也很必要用JMeter或者SysBench模拟预期峰值流量观察P99延迟和后端数据库的连接负载确保参数配置留有余量。5. 常见问题与排查技巧实录5.1 连接数打满与后端连接池耗尽代理上线之后最常遇到的故障就是连接数告警。表象是应用侧报数据库连接超时或获取连接失败排查时打开监控面板大概率看到代理节点的后端连接池使用率已经到顶。出现这个问题的原因通常有几个方向。一个是代理连接池maxPoolSize配置得过大后端数据库的实际连接数上限被打穿一个是业务侧计算了过高的前端连接数但后端连接池没跟上导致请求在代理层排队还有一个是慢SQL占用连接时间过长连接释放速度跟不上新请求的到来速度。排查思路一般是这样的先看后端数据库的max_connections是否被占满如果被占满再回到代理看连接池状态和活跃请求数。如果代理和后端连接数都已经到顶说明确实是容量问题需要扩容后端数据库或者调整连接池参数。如果后端没有满但代理请求大量堆积就要重点分析是否有慢SQL拖累了整体吞吐。慢SQL日志在这个阶段价值极大可以快速定位到具体语句。我还有一个经验是很多连接数问题根本不是并发太高而是应用侧使用了连接而未正确释放导致前端连接长期挂起。这类问题在代理层表现为连接数缓慢爬升且不回落需要和业务开发一起梳理连接池配置和代码中的连接获取释放逻辑。5.2 路由结果异常跨分片笛卡尔积与全库扫描路由错误的典型表现是性能突然劣化一条原本毫秒级的SQL变成了秒级甚至分钟级。最让人头疼的是跨分片join产生了笛卡尔积也就是多个分片之间做了全连接。假设t_order和t_order_item没有配置绑定关系而且join条件没带分片键代理无法判断数据在哪个分片于是让所有分片都参与运算结果集膨胀得非常快。排查这个问题的第一步是先把SQL捞出来用PREVIEW看执行计划确认它被路由到了哪些分片。正常情况下绑定表的join应该只路由到对应分片如果看到所有分片都被扫了说明绑定表配置缺失或者分片键没有参与join条件。根治手段是配置绑定表。凡是经常需要关联查询的表只要它们共享同一个分片键就必须加入bindingTables列表这样代理才能把关联查询下推到同一分片执行。另一个手段是使用广播表适合那些数据量不大、每个分片都需要完整副本的字典表。对于完全无法避免的跨分片join我的建议是不要在SQL层解决而是从业务建模层面想办法比如预先冗余字段、拆分查询、用宽表替代这样才能真正根治。5.3 分布式事务与数据一致性跨越分片的基本功当一条业务操作涉及多个分片的数据修改时问题就来了。最经典的手段是XA两阶段提交但XA在三方系统协调、网络分区、协调者故障等场景下有天然的脆弱性。代理层实现了XA并不代表你可以随意依赖它——我在生产环境里见过不少因为跨分片XA事务执行时间过长导致锁等待堆积、最终拖垮整条业务链路的案例。实际业务对强一致性的需求其实没有想象中那么高更重要的是把事务边界尽可能缩小。一个订单的创建过程如果订单主记录和订单明细数量不大完全可以把它们设计到同一分片利用绑定表保证写入落在同一个分片里这样事务就退化成单库事务稳定性和性能都会好很多。必须跨分片更新的时候优先考虑最终一致性的柔性事务方案比如事务消息、本地消息表加定时补偿、SAGA模式这些做法不依赖数据库分布式事务协调器更容易在工程上落地。数据一致性还有一层容易被忽视的地方代理层在主键生成时如果使用雪花算法务必确认全局时钟没有明显回拨。时钟回拨会导致主键重复一旦发生分片写入会直接报主键冲突这类问题定位起来很费劲建议在运维层面做好NTP的监控。5.4 延迟与性能边界代理带来的那一跳网络开销接入代理之后任何SQL都绕不开额外一跳网络传输。如果代理和后端数据库之间的网络链路不稳定或者跨了机房延迟会明显放大。生产环境我坚持让代理和后端数据库部署在同一可用区依靠负载均衡把应用流量导到代理节点这样网络开销对内网环境来说可以忽略不计。代理处理本身的CPU开销也是性能边界的一部分。解析、改写、合并都要消耗CPU和内存。当QPS很高的时候代理节点的CPU容易成为瓶颈。如果看到代理CPU持续高位第一反应不是加机器而是看看是不是SQL解析缓存命中率偏低、单个分片数过多导致合并计算量大以及有没有大量不带分片键的全路由查询在空耗资源。把这些因素排除之后再评估是否需要增加代理节点做水平扩展。另外需要关注的是大结果集的合并压力。一个查询出来几万行数据每个分片都要把结果集传给代理代理还要归并排序内存占用会很夸张。应对手段是限制单次查询的返回行数、强制分页、把分析型查询分离到单独的从库或者OLAP系统。分布式数据库代理擅长的是高并发短查询凡是重查询都应该在架构设计层面给它单独铺路而不是让代理硬扛。我在实际运维中还发现一个细节代理版本升级要谨慎。中间件迭代经常带来行为变化比如某条SQL原本能路由的新版本解析规则调整后路由结果可能不同。升级前一定在测试环境全面回归一遍SQL清单至少覆盖CRUD、分页、排序、聚合、join和事务这些日常场景确认没有问题再上生产。
返回列表