ARTICLE DETAIL

资讯详情

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

分布式数据库代理全解析:连接收敛、读写分离与选型实践

分布式数据库代理全解析:连接收敛、读写分离与选型实践 说实话我第一次认真研究“分布式数据库代理”这个话题是因为一次非常狼狈的容量规划。当时团队把订单库从单机拆成了十几个分片每个分片还挂了主从以为上线后就万事大吉了。结果压测一开始后端数据库实例的连接数直接被打满应用层报错刷屏一堆服务开始超时。我们排查了很久才发现问题根本不在于数据库本身而在于所有应用都直接去连那一堆分片和副本连接数是乘法关系往上翻谁也扛不住。后来引入了一层数据库代理才算是把访问入口收住整个链路才重新变得可控。这篇内容就是围绕“分布式数据库代理”来写的它到底是什么解决了哪些核心问题选型时该怎么权衡以及我在实际搭建和排障过程中的经验。不管你是做后端开发、DBA还是架构设计只要你的数据库开始走向分布式、分片化这篇文章应该能帮你少踩几个坑。1. 为什么需要数据库代理——先从一次真实的容量规划说起1.1 节点变多之后应用已经不知道怎么连数据库了很多人刚接触分布式数据库时关注点都在数据分片、分布式事务、一致性这些方向上但真正落地时第一个被击穿的往往是连接管理。举个很典型的场景。你的订单库从单机1主1从扩成了16个分片每个分片一主两从那后端就是48个MySQL实例。如果应用直接连接这些实例连接数怎么算假设你有40个应用实例每个实例连接池配置20个连接那后端理论上要承受 40 × 20 × 48 的极端连接压力。就算实际达不到这个峰值日常的存量连接也足够让数据库的线程调度变得异常吃力。我当时遇到的情况还要更隐蔽一些连接数没有立刻爆掉而是每隔一段时间就会有一波新建连接风暴。原因很简单应用直连多个后端节点时只要某个节点抖动一下连接池重建就会瞬间产生大量TCP握手和MySQL握手。数据库节点本身CPU不高但连接线程一多整体RT就开始恶化进而引发更多超时和重连形成恶性循环。这就是分布式数据库带来的第一个“幸福的烦恼”节点多了拓扑复杂了应用已经不知道怎么去连数据库了。你不可能要求每个业务开发都记住分片规则、知道哪个库是主库、哪个从库可以读更不可能让他们在代码里自己实现故障摘除和Failover逻辑。1.2 代理层解决的三个核心问题要解决上述问题数据库代理其实只干三件事连接收敛、拓扑屏蔽、策略统一。连接收敛很好理解。应用无论有多少实例都只认一个逻辑地址就是代理的地址。代理负责与后端所有数据库节点保持长连接池应用发过来的连接请求只是在代理上报个到实际后端连接是复用的一批存量连接。这样后端连接数不再是“应用实例数 × 数据库节点数”而是“代理节点数 数据库节点数”数量级一下子就下来了。拓扑屏蔽的意义更大。分片规则、主从切换、节点扩缩容这些细节全部收进代理内部。对应用层来说它看到的永远是一台或者几台逻辑数据库。你后端换了一个分片节点应用连的DSN都不用变代理把它屏蔽掉了。这对研发团队来说简直太重要了因为数据库拓扑变了往往不需要业务发版本。策略统一则是治理层面的价值。SQL限流、审计、脱敏、读写分离路由、慢SQL拦截这些能力如果分散在每个客户端的连接池配置里根本没法统一管理。放到代理层就是一处配置、全局生效。1.3 不要把代理理解成“让数据库更快”有一句大实话得说在前面数据库代理不会让单个SQL执行得更快它甚至还会引入额外的网络跳转和SQL解析开销。那为什么还要用我习惯把代理比作公司前台。前台不会帮你更快写完代码但她能帮你拦下无关访客、把快递分类、告诉你哪个会议室在哪。如果没有前台所有人都直接往工位里闯每个员工都得自己处理这些杂事公司规模一大必然乱套。代理也是这个定位。它是架构意义上的“访问治理入口”而不是性能加速层。只有在两层情况下你感觉“变快了”一种是从库读分担了主库压力整体吞吐上去了另一种是代理把那些连错节点、重复建连的浪费消掉了系统稳定了自然不卡了。本质上它优化的是系统的吞吐和稳定性不是单条SQL的执行效率。另外要区分清楚TIDB这样的原生分布式数据库本身就自带接入层它对外提供的那个“数据库入口”其实已经是代理能力的一部分而ShardingSphere-Proxy这类中间件代理则是把代理能力做成一个独立服务放在应用和底层MySQL/PostgreSQL之间。两者定位相似但前者和存储引擎强绑定后者更通用可以接多种数据源。理解这一点后面选型的时候才不容易混乱。2. 核心功能拆解代理层到底在干什么活2.1 连接收敛如何把“万人连接”降级成“千人连接”连接收敛听起来简单实际做起来有不少细节。首先代理与后端之间必须是“长连接复用”。也就是说无论有多少客户端连接经过代理访问某个分片代理到该分片只维持固定数量的真实连接通常做一个连接池来管理。客户端连接即使频繁创建销毁也不会直接传导到后端。其次是代理与前端的连接。很多代理产品支持连接池模式也支持直连模式。连接池模式的好处是能快速响应应用请求但代价是代理自身需要维护更多socket和内存直连模式则更省资源但每次新建连接都会有一次完整的握手开销。我个人的经验是在高并发场景下代理最好配置一个合理的“前端连接池上限”避免瞬时连接数冲得太高后端没挂、代理先顶不住了。这里还有一个很多人会踩的坑代理的连接数上限需要和后端实例数量、后端thread pool大小一起评估。如果代理允许的并发连接数设得比后端max_connections还大那连接风暴只是从后端转移到了代理并没有真正被消灭。压测阶段就要把整条链路的连接水位测一遍。2.2 SQL路由从SQL解析到目标节点映射路由是分布式数据库代理最核心的能力之一也是大家选择它作为分库分表入口的主要原因。一条SQL到了代理手里大体要经过三个步骤解析SQL提取分片键映射目标节点。举个例子订单表按order_id哈希分成两个分片。应用执行一条查询select * from t_order where order_id 10001;代理拿到这条SQL之后会解析出等值条件order_id 10001然后对这个值做哈希取模根据分片算法算出来它应该落在哪个分片节点上最后只把SQL转发给那一个节点执行。这个过程的预期是让查询精准命中目标分片减少跨节点访问。但问题往往出在“没有分片键”的SQL上。典型的场景是select * from t_order where user_id 12345;如果分片键用的是order_id那代理没法直接算出user_id对应的分片只能把这条SQL广播到所有分片节点再把结果合并。一次查询变成多次后端查询性能自然好不了。这其实不是代理的问题而是分片键设计时留下的债务。另外PreparedStatement预编译语句也是代理实现里一个非常隐蔽的深坑。文本SQL好解析但预编译协议要先处理COM_STMT_PREPARE、再处理COM_STMT_EXECUTE代理必须维护一套“语句句柄到具体SQL”的映射关系同时还要跟踪参数类型。一旦某个客户端没有正确关闭语句句柄代理内存里就会积压一堆PreparedStatement缓存时间长了内存和CPU都会异常。这个在线上环境里特别常见查的时候不妨先看代理的statement cache命中率和对象数量。2.3 读写分离不是简单“读去从库”读写分离的初级理解是写语句走主库读语句走从库。但实际落地时最大的问题是“这条读能不能走从库”而不是“它是SELECT”。事务中的读不能走从库。比如你在一个事务里先插入订单再查询订单状态如果查询走了从库而从库复制延迟没追上你很可能查不到刚插入的数据业务逻辑就直接错了。所以代理的实现里一旦检测到当前会话处于事务状态事务内所有读都必须被强制路由到主库。带锁的读也不能走从库。select ... for update这种语句本质上是写语义必须主库执行否则锁在哪边都说不清楚。还有一点容易忽略的写入后的立即读。即使没有显式事务业务上“写完马上要看到结果”的场景也很常见。比如支付回调后立刻查询订单状态。这种语句如果走了延迟较大的从库就会出现“数据怎么又没了”的诡异现象。这种问题特别难排查因为从库延迟通常只有几百毫秒闪现一下就被覆盖了但被用户秒点碰到一次就是P0事故。所以成熟的代理方案一般会提供两种控制手段一是事务内路由自动保持会话一致性二是通过SQL Hint或者配置强制指定某些读走主库。我在配置线上规则时一直都是“能走从库的才走从库不确定的一个都不放走”。2.4 故障发现与自动切换机制分布式数据库池子里的节点总有挂掉的时候代理作为“唯一入口”必须比应用更早感知到故障并做出处理。故障发现依赖探活。轻量一点的探活方式是TCP探测也就是代理周期性检查后端端口是否可达靠谱一点的探活方式是发送MySQL的ping命令或者执行select 1。TCP探测最大的问题是“端口还开着但数据库已经假死”的情况探测不到所以我更倾向于在MySQL协议层做探活。探活频率要克制比如每1秒一次足矣太频繁反而会给后端制造无谓的压力。处理故障时代理要干两件事摘除故障节点并把新的请求路由到健康节点同时已经建立的连接如果落在故障节点上要及时断开让应用层感知并重连。这里有经验的团队会把代理和数据库高可用组件配合起来比如MHA、Orchestrator这类工具负责把从库提升为新主然后代理感知到拓扑变化后把流量切到新主上。整个过程需要做一些移交预演毕竟在线切库这种事容不得临场发挥。我自己曾遇到过一种情况后端主库被kill之后代理的游戏规则是“探活失败摘除节点”但那个节点上的已有连接没有及时断开导致应用层一直往同一个socket上写数据然后就是各种MySQL server has gone away。后来我们把代理的“失效连接回收时间”调短配合应用连接池的重试机制切主时间才压缩到业务可接受范围。3. 方案选型与架构设计的几个关键权衡3.1 集中式部署与客户端接入到底怎么选数据库代理落到部署形态上主要分成两类集中式代理和客户端嵌入式代理也有叫Sidecar模式的。集中式代理的优势是统一、可控、易治理。所有流量都经过一个明确的服务监控、限流、审计都有了落点后端拓扑变化也能隐藏在一个逻辑接入点后面。缺点是多了一跳网络开销并且引入了中心组件的高可用问题。ProxySQL、ShardingSphere-Proxy就是这种路线的代表。客户端嵌入式代理则直接“长”在应用进程里通过类库的形式提供分片路由和读写分离能力。它的优势是延迟最低没有独立代理的网络跳转劣势是每一个语言都得维护一套SDK控制面分散在各处多个团队使用时规范很难强制统一。比如有的团队在代码里写了自定义路由另一个团队用了另一套配置最后DBA根本没法全局管理。我的建议是团队规模小、系统单一、追求极致性能可以考虑嵌入式一旦涉及多个业务团队、数据库种类也多集中式代理会省心很多。架构的演进通常会从嵌入式开始等规模大了再沉淀成集中式。3.2 常用方案对比与选型建议市面上的数据库代理方案我大致用过几个主观感受放在下面方案形态核心能力适合场景需要注意的点ProxySQL集中式代理连接池、读写分离、查询路由、限流纯MySQL场景快速解决连接和读写分离分片能力弱更多是接入治理ShardingSphere-Proxy集中式代理分片、读写分离、数据脱敏、分布式事务分库分表场景需要强路由能力对复杂SQL兼容性需要充分测试Vitess集群化代理大规模MySQL分片、在线DDL、备份恢复超大规模、需要横向扩容运维复杂度高存储引擎绑定Vitess体系原生分布式数据库接入层内建代理与存储引擎天然协同分片、事务、高可用一体直接选用新架构的团队跟具体数据库绑定迁移成本高选型不要只看功能矩阵更重要的是看“协议兼容能力”和“团队运维能力”。协议兼容能力的意思是代理能无缝兼容你客户端发出的每一条SQL和每一种协议行为。很多方案在文档里写得很满实际跑一条带子查询的多表Join就出了岔子。所以无论选哪个上线前都要用真实业务的SQL流量回放做一轮兼容性测试。3.3 代理层本身的高可用设计引入了一个新中间件它自己挂了怎么办这个问题是必须回答的。核心思路是让代理尽量“无状态化”。代理的状态主要存在于会话上下文、连接池、PreparedStatement缓存里。如果这些状态全部放在单机那么这台机器一挂所有连接都要重新建后端同时涌来大量重连非常危险。因此生产环境至少要部署两个代理节点用VIP或者负载均衡器对外提供一个统一入口后端挂了可以从另一个节点接上。如果用的是负载均衡器注意会话保持的连接如果在Failover之后还要复用是做不到的因为新节点的会话上下文不一定完整。所以应用连接池本身必须有足够的断线重连能力。换句话说引入代理不能只部署代理应用端的连接池参数也要针对“代理切换”场景做一轮适配重试次数、重试间隔、maxTotal最好都调到合理范围。我见过一些团队为了追求高可用引入三台代理加一套Keepalived配置但从来没有做过主备切换演习。等到真挂了一台才发现VIP漂移倒是成功了但因为后端长连接全部失效业务瞬间报了上千条错误。记住高可用不是配置出来的是演练出来的。3.4 性能损耗的现实数据代理的性能损耗我在多个版本里做过对比测试也比较过不同方案之间的差异。直接说结论对于文本协议转发为主的场景损耗通常在5%到10%之间主要体现在小查询的P99延迟上如果代理做了SQL重写、结果集合并、排序聚合这类工作损耗会上升到15%复杂分页场景下甚至更高。压测方法很简单先让应用直连数据库跑一轮压力测试记录QPS和P99然后把连接地址改成代理地址同样的参数再跑一轮对比两组数据。我在一个16核32G的代理节点上做过分库分表压测直连MySQL的QPS大概在两万出头走代理之后掉到一万八左右P99从10ms涨到13ms。这个损耗我认为完全值得因为你换来的是后端连接数大降、拓扑可以随时切换、治理策略可以统一管控。真正需要警惕的不是这5%的损耗而是代理SQL解析逻辑写得糟糕导致CPU直接被拖满。后面在排查篇里详细说。4. 实操搭一套分布式数据库代理并把核心场景跑通4.1 测试环境拓扑规划纸上谈兵再多不如动手跑一遍。下面是我在测试环境里验证分布式数据库代理核心能力的一套标准拓扑简单但覆盖面足够。我准备了两台MySQL节点一个节点模拟主库source一个节点模拟从库replica另外一台机器部署ShardingSphere-Proxy。Docker环境下可以用下面的方式快速起两个MySQL实例docker run -d --name mysql-source \ -p 13306:3306 \ -e MYSQL_ROOT_PASSWORDroot \ mysql:8.0 docker run -d --name mysql-replica \ -p 13307:3306 \ -e MYSQL_ROOT_PASSWORDroot \ mysql:8.0这不是完整的MySQL主从复制配置只是先把两个实例跑起来。实际做主从复制时还需要分别设置server-id、开启binlog、配置复制账号并执行CHANGE MASTER TO。这些步骤比较常规就不贴完整命令了但要注意主从复制的点位信息在切换和故障恢复时会非常关键。代理节点我是直接用官方镜像跑的数据源配置指向上面两个MySQL实例。先不要急着加复杂的规则第一步验证“代理能连通、SQL能透传”再做分片路由。4.2 配置分片路由与读写分离以ShardingSphere-Proxy 5.x为例核心配置在一个YAML文件里。下面的内容是一个简化的分片加读写分离配置只是演示结构实际使用务必以对应版本的官方文档为准dataSources: ds_0: url: jdbc:mysql://127.0.0.1:13306/demo?useSSLfalse username: root password: root connectionTimeoutMilliseconds: 3000 ds_0_slave0: url: jdbc:mysql://127.0.0.1:13307/demo?useSSLfalse username: root password: root connectionTimeoutMilliseconds: 3000 rules: - !READWRITE_SPLITTING dataSources: readwrite_ds: writeDataSourceName: ds_0 readDataSourceNames: - ds_0_slave0 loadBalancerName: round_robin - !SHARDING tables: t_order: actualDataNodes: readwrite_ds.t_order_${0..1} tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: order_hash_mod shardingAlgorithms: order_hash_mod: type: HASH_MOD props: sharding-count: 2配置好之后用MySQL客户端连代理创建两张分表再写几条数据验证create table t_order_0 (id bigint primary key, order_id bigint, user_id bigint); create table t_order_1 (id bigint primary key, order_id bigint, user_id bigint); insert into t_order (id, order_id, user_id) values (1, 101, 1001); insert into t_order (id, order_id, user_id) values (2, 102, 1002); insert into t_order (id, order_id, user_id) values (3, 103, 1003);然后分别查询order_id101和order_id102再到后端两个真实MySQL实例上看数据落到了哪张分表。只要路由规则生效你会发现两条记录分别进了不同的物理表。再到代理上执行一条select * from t_order where user_id 1001它会触发广播查询从代理日志和后端实例执行记录里也能看出区别。这套实验做完你对“分片路由”和“广播查询”的感知会比看十篇文章都深。4.3 故障切换实测路由验证通过后再做故障切换。这个实验强烈建议你亲自做一次因为理论和实际差别太大。操作很简单在主库容器里执行docker stop mysql-source然后观察代理日志。正常情况下代理会在下一个探活周期发现主库不可达标记节点失效并把后续写流量路由到从库。但这里有一个大前提从库必须已经被提升为新主否则代理只是把流量切过去从库本身没有写能力业务写入还是失败。我们当时是把切换能力交给外部高可用组件来做的组件负责提升从库为新的主库代理只负责感知拓扑变化并更新路由。如果没有这类组件你需要人工登上从库执行stop slave; reset slave all;让它脱离复制关系成为独立主库然后再把代理的写数据源指向它。整个过程如果稳健业务感知的抖动可能只有几秒到十几秒。如果超过30秒多半是应用连接池没有配置好断线重连或者代理的失效连接回收太慢。还有一个容易被忽视的细节切换完成后旧主库如果重新拉起它很可能还会以旧的复制关系连过来造成脑裂风险。所以旧主库恢复后必须确认它是作为新主的从库重新加入拓扑还是被彻底隔离不能稀里糊涂放回生产。4.4 性能衰减与稳定性观测链路通了路由也没问题接下来要回答“代理上线后性能到底打了几折”。我用sysbench做过一组简单对比测试命令大致是这个样子sysbench oltp_read_write \ --db-drivermysql \ --mysql-host127.0.0.1 \ --mysql-port13306 \ --mysql-userroot \ --mysql-passwordroot \ --tables10 \ --table-size100000 \ --threads16 \ --time60 \ run直连后端MySQL跑一轮再把端口改成代理的端口跑一轮对比两组数据即可。我实测下来QPS大约下降10%左右P99从毫秒级缓慢爬升了几毫秒。这个数字仅供参考不同环境、不同配置差异很大但它能坚定一个判断代理的性能损耗可控远没有想象中那么可怕。更重要的是压测过程中要观测代理本身。我通常盯四个指标CPU使用率、内存占用、连接数、P99延迟。如果并发线程从16涨到64代理CPU涨得比MySQL还快说明解析逻辑或者连接管理有优化空间。如果内存涨上去降不下来重点查PreparedStatement缓存和连接池是否回收不够及时。5. 常见问题排查与避坑实录5.1 连接数打满所有服务都卡住现象数据库和代理都还没到瓶颈但应用侧开始大量报连接超时登录代理管理界面一看前端连接数飙到了上限。排查方向分两步先看是不是连接收敛失效。有些代理在后端配置了很大的连接池导致数据库节点被创建的连接数其实不少再看是不是后端某个慢查询把连接池里的连接全部占住新请求只能排队等连接。这时候用show processlist看后端通常能看到一堆Sleep时间很长的连接或者正在执行的慢SQL。解决思路把代理的“最大前端连接数”设置成合理的值并且在后端连接池里设置连接空闲超时别让连接无限期占用。更关键的是排查应用侧连接池配置如果每个应用实例maxTotal都开得特别大即使有代理也扛不住瞬间并发。5.2 无分片键的查询变成广播现象某条线上SQL执行时间突然从几毫秒变成几百毫秒后端所有分片节点的CPU同时出现尖峰。这种问题基本都是SQL没有带分片键导致的。代理拿不到路由依据只能全节点广播再合并结果。数据量小的时候无所谓数据量一大广播查询就是一场小型灾难。排查手段是看代理的执行日志里面通常会标注目标节点或者路由类型。如果是广播就要推动业务改造SQL让查询条件包含分片键如果实在改不了再考虑建立全局索引表或者使用别的存储来承接这类查询。这里我有一条经验上线分表之前就把常见查询的SQL清单梳理一遍逐一确认是否携带分片键。不要在分表之后才让业务去查“为什么我的SQL变慢了”。5.3 读从库读到旧数据现象应用刚写完数据立刻读偶尔读不到过一会儿再查就正常了。这种偶发问题最容易让开发同学怀疑人生。原因通常出在读写分离路由策略上。代理默认会尝试把不在事务里的SELECT路由到从库去减轻主库压力但“不在事务里”不等于“允许读延迟”。如果你刚在主库写入紧接着发起的读被路由到延迟中的从库复制延迟一出现脏读就发生了。解决方式对一致性要求高的接口要么把整段逻辑包进事务让代理内部强制所有SQL走主库要么在SQL里加一个强制主库的Hint比如ShardingSphere-Proxy就支持通过Hint注解指定路由规则。还有一招是把对应读从“从库路由范围”里剔除。另外监控从库延迟不能只看秒级。show slave status里的Seconds_Behind_Master在MySQL 8.0里是秒级精度的如果网络抖动或者大事务回放这个值会瞬间跳高。有条件的话记录一段时间内的延迟曲线对比业务报障时间段能快速定位是不是复制延迟引发的。5.4 代理CPU偏高但QPS并没有明显增长现象代理节点CPU持续在80%以上但观察到的QPS并不高后端的压力也不大。这种情况大概率是代理做了大量“额外工作”。常见因子有这么几个SQL重写逻辑过于复杂每次请求都要重新做语法树解析开启了全量SQL审计每条SQL都要写日志PreparedStatement缓存没有生效同一个SQL反复解析。我遇到过最夸张的一次是某团队把代理的“慢SQL日志”阈值设成了0等于任何SQL都记录结果日志刷屏把磁盘IO拖垮CPU和IO双双告警。最后把阈值恢复到1秒问题立刻缓解。排查思路很简单先关掉一切不影响核心功能的高级特性再一个特性一个特性打开压测看CPU曲线的变化拐点。找到元凶之后要么优化SQL要么调整特性参数不要一上来就加机器。5.5 实战避坑表整理问题常见原因快查手段解决建议连接数突增后雪崩应用重连风暴、代理连接池过大查看代理连接数曲线、后端连接数限制前端连接上限、设置空闲回收、压测重连场景SQL整体变慢分片键缺失、广播查询、结果集合并代理执行日志、后端CPU曲线补全分片键、拆SQL、优化路由刚写完读不到从库复制延迟从库延迟监控、事务路由日志事务内强制主库、Hint强制路由代理CPU高审计日志、复杂解析、无缓存CPU热点分析、开关特性对比关审计日志、配置Statement缓存切换后业务报错旧连接未断开、应用重连不及时代理连接日志、应用报错时间点调整失效连接回收、应用连接池重试写在最后代理层这条路怎么走才不亏做分布式数据库访问层这几年我最大的体会是代理不是万能入口也不是越复杂越好。它不是让数据库变快的魔法而是让你在数据库规模变大之后仍然能对访问流量进行统一治理的一道闸门。如果你正准备引入代理我的建议是阶梯式演进。先通过代理解决连接收敛和读写分离把稳定性和体验跑出来等业务量真正到了一定规模再逐步引入分片路由、分布式事务等高级能力。不要一开始就上一套大而全的方案否则排障时多出来的复杂度会把你埋掉。还有一条经验值得单独提醒代理上线前全量SQL日志默认关闭真需要审计的时候再临时打开用完了立刻关。我亲眼见过一个代理节点因为审计日志直接把CPU烧到100%而业务侧只是偶尔有一次慢查询。这类问题不亲身踩一次光看配置文档是感觉不到疼的。技术选型和架构设计从来不是纸上拼概念最终都要回到线上那一张张告警图、一条条报错日志里去验证。希望这些踩坑经验能帮你把分布式数据库代理的路走得更稳。
返回列表