☰
分库分表集群扩容实战:从4分片到8分片全流程测试复盘
2026/10/15 14:03:37 网站建设 项目流程

分库分表做到一定规模之后,扩容就是绕不开的坎。我在一套基于 sharding proxy 的测试环境里,完整验证了分库分表集群从 4 个物理分片扩容到 8 个物理分片的全过程,包括存量数据迁移、路由规则切换、增量同步、一致性校验和回滚演练。今天把这次测试中暴露的核心问题、实际操作步骤和排查记录做个复盘。如果你是 DBA、后端开发或者中间件测试同学,正在评估分库分表架构的扩展能力,或是准备给现有 sharding 集群做容量扩容,这篇文章能给你一份比较完整的参考路线。

1. 扩容问题为什么在分库分表里这么棘手

1.1 一次扩容的本质:数据和路由的重新分配

分库分表之后,业务数据按照分片键被拆到多个物理库和物理表。常见的方式有哈希取模、按时间范围、按业务维度等,互联网场景里用得最多的还是哈希取模,比如order_id % 4把数据分布到 4 个分片。这里就埋下了扩容的核心难点:当你把分片数从 4 变成 8,数据分布规则从% 4变成% 8,原本落在分片 0 的数据,经过新的取模规则后,可能应该落在分片 0 或者分片 4。于是两件事同时发生:存量数据要重新分布,路由规则要切换。这两个动作只要有一个不平滑,就会出现数据查不到、写入分错片、IO 倾斜严重等连锁问题。

从容量角度看,扩容解决的是“单分片容量逼近上限”或“集群整体吞吐不够”的问题。但扩容动作本身如果处理不好,反而会把一个小问题放大成一次故障。我见过不少团队以为扩容就是“加两台机器,改一下配置”,结果加完机器之后数据还是落在老分片上,新机器一点流量都没接到。扩容的意义不在于“加了机器”,而在于“加了机器之后数据和流量真的能用起来”,这才是测试要证明的核心。

1.2 sharding proxy 在扩容链路中的角色分工

sharding proxy 是位于应用和数据库之间的中间层,对应用伪装成单库单表的数据库协议服务,实际负责把 SQL 改写并路由到后端多个数据库节点。在分库分表扩容过程中,proxy 至少承担三个职责:

  • 维护路由规则。分片规则变更后,proxy 要能按照新规则把请求送到正确分片,并且切换过程对应用尽量透明。
  • 管理后端连接。分片数量变化时,proxy 与每个分片之间的连接池要动态调整,旧分片和新分片在迁移期间会同时存在。
  • 作为迁移媒介。很多在线扩容方案依赖 proxy 层做数据搬迁协调和流量切换,而不是让业务直连数据库去搬数。

所以在测试扩容时,不能只测“把数据拷过去”,而是要验证 proxy 在规则切换过程中能不能做到对应用透明、对数据完整、对连接无损。如果 proxy 的路由表更新不及时,或者连接池没扩容,即使物理节点加得再多,依然会出现超时和数据不一致。这也是我选择把测试重心放在 sharding proxy 这一层的原因。

2. 扩容测试准备:环境、表结构和基线数据

2.1 测试环境与分片规划

我这次测试用的是 4 个物理 MySQL 实例,每个实例上建了同一个逻辑库,表名统一为t_order。中间部署一个 sharding proxy 节点,应用只连 proxy,不直连后端。分片键选了order_id,路由规则先用order_id % 4,目标规则是order_id % 8。

物理资源方面:

  • 每个分片实例 4 核 8G,磁盘 200G SSD;
  • proxy 节点 4 核 8G,单节点部署;
  • 应用侧用模拟写入程序,并发 32 线程,持续产生订单数据。

测试数据量我准备了 4000 万行订单记录,平均每分片 1000 万行。为什么选这个量级?因为只有单分片数据量到千万级,扩容时的数据搬迁耗时、proxy 内存压力、路由变更对在线写入的影响才会被充分暴露出来。数据量太小,很容易测出“看起来扩容很轻松”的假象,换到生产环境就被打脸。

2.2 表结构设计与分片键选择

表结构简化成核心几列:

CREATE TABLE `t_order` ( `order_id` bigint NOT NULL, `user_id` bigint NOT NULL, `amount` decimal(10,2) DEFAULT NULL, `status` int DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`order_id`) ) ENGINE=InnoDB;

分片键我选了order_id,理由是这张表几乎所有查询都带订单号,范围查询走create_time,不做跨分片 join。这里要提醒一句:分片键的选择直接决定扩容的复杂度。如果业务里存在大量按user_id查询的场景,建议主分片键设成user_id,再把order_id作为二级索引或者建冗余表,否则扩容时还得额外处理二级路由。

从测试角度,我故意设计了一部分数据让user_id和order_id之间的对应关系不均匀,这样扩容后可以验证“不均匀数据在重新分布后会不会造成某个分片特别满”的问题。这个点很多人会忽略,但恰恰是生产环境扩容后最容易出现的隐患。

2.3 扩容前的性能基线与容量水位

在动任何东西之前,我先做了基线测试:

  • 写入 TPS 基线:32 并发下稳定在 2200 左右;
  • 查询 P99 延迟:按order_id点查,P99 在 8ms 左右;
  • 单分片数据量:平均 1000 万行,最大分片约 1150 万行;
  • 磁盘占用率:最大分片到了 68%,这是触发扩容的直接原因。

基线数据必须留好,否则扩容完之后,你没法说清楚变慢是因为数据量正常增长,还是扩容过程引入的问题。我后来在做扩容后回归对比时,全靠这一份基线数据当标尺,否则很多指标波动根本无法判断是否正常。

3. 从4分片到8分片:完整扩容流程测试

3.1 扩容方案选型

扩容思路大致有几种:

  • 停服扩容:把所有写请求停掉,重新计算 hash 并迁移数据,然后把路由切换成新规则。这种方式简单可靠,但需要接受一段不可用时间。
  • 双写扩容:扩容期间同时写旧集群和新集群,等两边一致后再切换读流量,最后下线旧集群。复杂度高,对业务侵入大。
  • 在线迁移扩容:数据按新规则搬迁到新集群,proxy 负责把增量变更同步过去,在某个窗口内完成路由切换,对应用影响最小,但对中间件能力要求最高。

在 sharding proxy 场景下,我优先验证的是在线迁移扩容,因为这是生产环境里最有价值、对测试要求也最高的一种。停服方案逻辑简单,测试价值和风险提示都有限,但我把它留作回滚保底方案。下面讲的都是在线迁移扩容的实测过程。

3.2 存量数据迁移:不是简单的“把数据导出来再导进去”

存量数据迁移我拆成了四步:

  1. 先在 4 个新分片(分片 4、5、6、7)上把表结构建好;
  2. 写迁移任务,按旧分片逐表扫描,对每一行数据用order_id % 8重新计算该去哪个新分片;
  3. 用批量 insert 方式写入目标分片,每批 5000 行,分批提交;
  4. 迁移过程中持续记录迁移行数和耗时。

这里有个关键点:迁移不能只按旧分片顺序搬。按% 8计算后,旧分片 0 里的数据一部分去新分片 0,一部分去新分片 4。如果按旧分片顺序一路搬过去,会造成新分片写入压力不均衡,而且效率很低。我后来改成按旧分片并行扫描,同时向 8 个目标分片写入,整体搬迁耗时从单线程的 7 个多小时压到了 2 小时左右。

迁移行数校验也要做好。搬完后,我在每个目标分片上执行select count(*)和checksum table,和源分片的总数做对比。checksum table在 MySQL 里会对整表做一致性校验,迁移后如果源表和目标表的 checksum 一致,基本能确认数据没丢。但千万级数据量下,checksum table本身比较耗时,所以我先比对行数,再抽样做 checksum,校验效率高很多。

3.3 增量同步与路由切换验证

存量迁移做完不代表扩容结束。迁移期间,业务写入还在继续,这部分增量数据必须同步到新拓扑。我这里用的方式是:

  • 让 proxy 在迁移期间开启增量日志采集,记录order_id维度的写操作;
  • 定期回放增量日志到新分片;
  • 在切换窗口前做最后一次增量追平,让旧拓扑和新拓扑的数据差保持在秒级以内。

路由切换是重头戏。切换前,proxy 里配置的是order_id % 4,切换后要变成order_id % 8。如果直接改配置并 reload,正在执行的请求有一部分已经在旧路由上跑了,新请求却走新路由,前后不一致。所以在线切换必须让 proxy 支持一个短暂的“只读禁写”窗口:先把写入口暂停,等存量增量都追平,然后原子切换路由,再放开写入口。

我在测试里实际用了大约 30 秒的窗口完成切换。具体操作是:把 proxy 写入口流量摘掉,应用侧写入线程停下来,确认增量日志追平到 0 延迟,更新分片规则并 reload,然后用几个关键订单号分别验证路由是否正确,最后放开写流量。整个过程很快,但任何一步没确认都不能继续,这是扩容里最不能抢时间的环节。

3.4 回滚预案与演练

扩容这种操作,不可能保证一次成功。动手前我就列了回滚条件:

  • 如果增量追平超过 30 分钟还没完成,回滚;
  • 如果切换后点查成功率低于 99.9%,回滚;
  • 如果新分片写入延迟比迁移前基线高 2 倍以上,回滚。

回滚方案分两层。第一层是 proxy 配置回滚,把路由规则改回% 4,同时让应用重连;第二层是数据回滚,新分片上的数据直接清掉,旧分片继续作为权威数据源。测试环境数据可以重建,真实回滚操作我没有做太多次,但特意在测试计划里安排了一次“模拟切换失败后的回滚”,确保流程走得通。

回滚演练中发现一个小问题:配置回滚后,proxy 里旧路由规则已经 reload,但应用侧连接池里的长连接还缓存着新路由的预处理语句,导致部分查询继续跑新分片。这个需要在回滚时强制断开应用连接池,让应用重新建立连接。回滚不只是改配置,连接生命周期也要一并管理。

4. 扩容测试中高频踩坑与问题排查

4.1 分片键不一致导致路由错乱

第一次切换新规则时,我遇到一个非常典型的问题:部分查询走了旧路由规则,部分查询走新规则。排查后发现,proxy 配置了多个逻辑表,其中一张从表的 sharding 规则没有同步更新,导致从表数据仍然按% 4路由。表面上看主表扩容成功了,但从表一旦发生关联查询,就会路由到旧分片查不到数据。

这个问题的教训是:扩容时不仅主表要改规则,所有附属表、索引表、关联表的规则都要一起检查和更新。我在后续测试里加了一步“路由规则全量比对”,把 proxy 里所有表的分片键、分片算法、分片数量都导出成配置清单,和测试预期做 diff,确认没有遗漏后才开始切换。

4.2 扩容期间的写入延迟突刺

迁移任务在跑的时候,新分片写入和旧分片业务写入同时进行。第一个明显的问题是写入延迟出现突刺,点查 P99 从 8ms 飙到 120ms。观察监控后发现,迁移任务把 IO 打满了,尤其旧分片在扫描阶段 CPU 占用率接近 90%。后面我把迁移并发从 8 降到了 4,同时避开业务流量高峰期,延迟突刺才得到控制。测下来真正稳定的迁移速率是单分片每秒 3000 到 4000 行,再往上提就会影响在线业务。

这也验证了一个原则:扩容迁移的速度不是越快越好,而是在不影响业务基线的前提下越快越好。测试时一定要边迁移边观察业务指标,一旦出现明显劣化,马上降并发。

4.3 proxy 连接风暴导致连接数耗尽

扩容期间,proxy 和后端分片之间的连接数也有隐患。旧拓扑 4 个分片、新拓扑 8 个分片,迁移和业务同时访问,proxy 需要同时维护到 12 个物理库的连接。默认连接池上限是 100,结果一扩容就出现too many connections。后来我把 proxy 到后端的最大连接数调大,并按分片维度做连接池隔离,避免某个分片连接打满连累其他分片。

这个问题的根因是:扩容过程中,proxy 的连接数是“新旧叠加”的,不是简单从 4 变成 8。如果提前按新拓扑的 8 分片去调连接池,迁移期间连接可能不够用;如果按“8 分片 + 迁移并发”去调,又需要评估后端 MySQL 的最大连接数。这里没有标准答案,只能通过压测结果去找平衡点。我最后用的方案是给迁移任务单独走一个独立连接池,不占用业务连接的额度,两个池互不影响。

4.4 proxy 内存压力与 GC 抖动

数据迁移时,proxy 节点需要缓冲大量待回放的增量日志和批量结果集,内存占用明显上升。测试中有一版 proxy 堆内存配置是 2G,结果在回放增量日志时频繁 Full GC,导致所有经过 proxy 的 SQL 延迟上涨。后来把堆内存调到 4G,改用 G1 回收器,并把大结果集查询改为流式读取,情况才好转。

这类问题容易被忽视,很多团队把注意力放在数据库节点上,却忘了 proxy 本身也是扩容链路里的一个瓶颈点。扩容测试一定要把 proxy 节点的 CPU、内存、GC 时间都纳入监控,否则你看到的现象可能是“数据库没毛病但整个链路很慢”,排查方向很容易走偏。

5. 扩容成功的衡量标准与测试观测方法

5.1 关键指标与判定阈值

扩容不是“数据搬完就算成功”。我这次测试用了下面一组指标做判定,分享出来供参考:

指标扩容前基线扩容后目标说明
写入 TPS2200不低于 2000扩容后分片增加,但短期不应有大幅回落
点查 P99 延迟8ms不高于 20ms数据重新分布初期允许一定抖动,但要有上限
数据一致性100%100%行数加 checksum 双重校验
分片数据倾斜度1150万/1000万各分片差异不超过 10%扩容后不能出现新的热点分片
迁移期间业务成功率99.99%99.99%迁移过程不允许产生大量业务报错

这里重点解释分片数据倾斜度。按% 8重新分布后,理论上每个分片约 500 万行。但由于我提前注入了不均匀数据,有的分片可能会到 600 万,有的只有 450 万。倾斜度超过 10%,后续会出现“加了分片但热点还是没解决”的尴尬局面。所以扩容测试里一定要单独关注新分布的数据是否均匀,而不仅仅是总量有没有变化。

5.2 扩容后的全链路回归验证

扩容后我做了一轮全链路回归,内容不局限于“能查能写”:

  • 按order_id点查新老订单,验证路由正确;
  • 按user_id查询某用户订单列表,验证索引表和关联表规则同步更新;
  • 跑一批典型的聚合统计 SQL,确认跨分片聚合结果和扩容前一致;
  • 用压测工具打 15 分钟写流量,观察新拓扑在高并发下的稳定性。

其中跨分片聚合是重点。哈希分片下,聚合 SQL 往往需要 proxy 把多个分片的结果集拉回来再计算。扩容前 4 个分片,扩容后 8 个分片,数据路由范围变了,聚合结果如果和扩容前不一致,说明 proxy 的聚合逻辑对分片数变化处理得不对。

回归验证里我发现了一个比较隐蔽的问题:有一类count(*) group by status的 SQL,扩容后返回的 count 比扩容前多了几千行。排查了很久,最后发现是增量回放阶段,有部分数据被重复写入了新分片,同一个order_id在新分片里出现两条记录。原因出在回放任务的重试机制:网络抖动导致一批增量日志回放超时,任务重试时把上一批已经提交的数据又执行了一遍。后来我给回放任务加上了基于order_id的去重约束,才把这个坑填平。

5.3 数据一致性校验的实用方法

数据一致性校验我是分层做的,针对不同阶段各有侧重:

  • 存量迁移后:行数比对加抽样checksum table;
  • 增量追平前:比对旧分片与对应新分片最近 5 分钟的写入日志序列号;
  • 路由切换后:对核心订单号做一轮“新旧双读”,同时查询旧分片和新分片,比对关键字段是否一致。

“新旧双读”听起来简单,实际操作要注意细节:旧分片在切换后已经不是权威数据源,如果应用此时还在线上写,查出来的数据可能本身就存在时间差。所以双读一定要在写流量完全停掉的窗口内进行,否则很容易误判一致性校验失败。

6. 给同样在做扩容测试的朋友几点建议

6.1 测试排期与时间窗口设计

扩容测试最容易被低估的是时间。我当时预估“数据迁移 1 小时搞定”,实际从开始迁移到完成路由切换,花了大半天,中间还包含排障、回滚模拟和重复校验。建议按“迁移耗时 x 2 + 半天排障缓冲”来规划测试时间。

另外时间窗口要避开业务高峰期,这一点在测试和生产都一样。扩容过程中有 IO 争抢、proxy 内存压力和连接数上涨,这些都会被高峰流量放大。测试环境虽然没有真实业务流量,但你要模拟高流量场景时,迁移任务和压测任务就不要同时开到最大,否则干扰太严重,问题定位会很困难。

6.2 扩容自动化与演练常态化

手工跑扩容流程非常容易出错,尤其是“配置更新”和“一致性校验”这种步骤,漏一步后面就要花几小时排查。我建议把下面这些动作脚本化:

  • 新分片初始化建表;
  • 存量数据迁移任务的启停;
  • 路由规则变更与 proxy reload;
  • 行数比对和 checksum 校验;
  • 切换前写流量摘除和恢复。

脚本化之后,至少能保证测试环境里每次操作都是可重复的。生产环境扩容时,这些脚本经过充分验证,也能显著降低操作失误的概率。我这次测试环境里最后跑通的一版流程,就是通过一套脚本连续执行了三次成功扩容,才敢说整个方案是可靠的。

6.3 一些容易忽略的细节

如果只能总结几个点,我会说:

  • 扩容不是数据库节点的事,proxy 的路由、连接、内存和 GC 都要一起测;
  • 存量迁移之后一定要做增量回放测试,很多数据问题都出在这一步;
  • 回滚方案要提前写好,回滚演练要真正执行,不要只停留在文档里;
  • 扩容成功后不要急着清理旧分片,保留一段时间作为恢复手段;
  • 分片键如果选得不好,扩容时所有关联表都要跟着改,工作量翻倍。

我个人实际操作中的体会是:分库分表扩容这件事,难点永远不在“能不能搬数据”,而在“搬数据的过程中能不能保证业务无感、数据不丢、路由正确”。sharding proxy 把很多底层细节封装掉了,但正因为有这层封装,测试时更需要把每个环节拆开看清楚,否则它在帮你隐藏复杂性的同时,也可能把问题藏到你发现不了的地方。

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

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

立即咨询