Sharding Proxy环境下分库分表扩容全流程实战与避坑指南
分库分表做了三年多中间也帮几个团队处理过扩容的疑难杂症但真正把扩容全流程在 sharding proxy 上完整压测、演练、甚至故意制造故障来验证的还是最近这个项目最有代表性。这篇就把我在 sharding proxy 环境下测试分库分表扩容问题时踩过的坑、梳理过的方案、以及最后沉淀下来的验证清单完整分享出来。如果你正准备做容量规划、或者马上要面临数据量增长需要扩展分片这篇文章应该能帮你省下不少弯路上的时间。先说一个很多人容易误解的点分库分表的扩容难点从来不在“加机器”或“加数据库实例”这一步而在于存量数据怎么重新分布、路由规则怎么平滑切换、以及切换过程中怎么保证线上读写不出现长时间不可用。我在测试环境里完整跑了一轮之后对这句话的体会特别深。1. 扩容问题的本质路由规则变了存量数据不会自己搬家1.1 扩容真正要解决的是“数据归属”问题先举个例子。假设你原来有 4 个分片某个表用订单 ID 做分片键取模路由order_id % 4。数据分布大概是每片 25%。现在数据量涨了你决定扩容到 8 个分片路由变成order_id % 8。问题来了——原来order_id % 4 1的数据在 1 号库现在order_id % 8的结果可能是 1也可能是 5。如果不做任何处理同一个订单在 8 分片的新路由下会被代理层路由到 5 号库但数据还在 1 号库于是查询直接落空。这就是扩容最核心的矛盾分片数的变化导致路由算法变化而存量数据还停留在旧分片上。所以扩容的本质是一道“数据迁移 路由切换”的组合题而不只是改个配置、重启一下代理那么简单。sharding proxy 这类中间件通常只负责路由和执行 SQL它不会自动帮你把旧分片的数据搬到新分片。你得自己设计迁移方案或者在 proxy 层面借助一些工具完成数据搬迁。我在测试时用的拓扑比较简单一套标准的 MySQL 主从架构在每台分片机上上层部署了 sharding proxy 提供统一的 SQL 入口分片策略是哈希取模。扩容前 4 个物理分片计划扩容到 8 个。测试的数据量大概在千万级虽然不算大但已经足够暴露大部分路由切换和数据迁移的经典问题了。1.2 预分片为什么能规避扩容但无法完全替代“真扩容”有一种常见做法叫“预分片”建表时就按未来 3 年的数据量规划分片数比如直接分 128 片或 1024 片容量不够时通过把多个逻辑分片迁移到新增物理节点上来实现“扩容”。这个方案本质上是把“分片数变多”换成“分片分布变密”路由算法不变迁移的粒度从“重新哈希所有数据”变成“搬动部分分片”。这样做的好处是不需要改路由取模规则只需要在 proxy 层调整“分片到物理节点”的映射关系。坏处是如果你没做预分片只是普通 4 分片跑了一年多数据已经过亿这时候想靠预分片“平滑升级”是行不通的必须走一次真正的全量重新分布。我做测试的时候特意对比了这两种路径。像我们这种从 4 分片起步的系统完全没有预分片所以只能走传统扩容路线新增 4 个物理分片把旧数据按新路由规则重新打散搬运然后切换路由。注意如果你的系统还没到非扩不可的地步建议现在就用预分片思路提前规划分片数。真等到数据量大了再扩容成本和风险完全不是一个量级。但如果你已经“上车”了这篇文章后面讲的内容才是你真正需要的。2. sharding proxy 路由策略的选型直接决定扩容成本2.1 取模路由 vs 范围路由扩容难度的分水岭sharding proxy 支持多种分片策略最常见的就是哈希取模和区间范围。别看只是路由方式不同它对扩容成本的影响是决定性的。路由策略扩容时的数据搬迁量路由切换复杂度适合场景哈希取模几乎全部存量数据需要迁移高需保证新老路由一致性读写均衡、随机查询为主的业务区间范围只需迁移部分数据低按范围边界切割即可按时间、按业务线归档类数据一致性哈希只迁移部分数据但实现复杂中依赖虚拟节点管理大规模分布式缓存类场景我们测试的订单表用的是哈希取模这也就意味着扩容时理论上每个旧分片里大约有一半的数据需要搬到新分片。比如旧 1 号分片里的数据在新 8 分片规则下一部分归属于新 1 号分片一部分归属于新 5 号分片。我在测试环境实测了一个数据4 片扩到 8 片迁移比例大约是 50%。换算到生产环境就是几十 TB 的搬迁量如果不控制好迁移速度和窗口会对源库产生不小的压力。2.2 一致性哈希的真实定位平滑但并非万能聊扩容绕不开一致性哈希。很多文章把它吹得天花乱坠好像用了它扩容就毫无痛苦。我在 sharding proxy 上做过对比测试一致性哈希确实能在新增节点时只迁移部分数据但要注意两个问题第一sharding proxy 对一致性哈希的支持程度、虚拟节点配置方式各不相同有些版本还需要自己实现分片函数扩展第二一致性哈希只是让“数据迁移范围变小”并不等于“不需要迁移”。你仍然需要把属于新节点的数据从旧节点搬运过去这个搬运过程中的增量同步、校验、回滚机制和取模扩容没有本质区别。所以如果你是做技术选型阶段我建议优先想清楚业务是偏“范围查询”还是“随机点查”。随机点查用一致性哈希或取模都行但要意识到将来扩容的成本按时间归档的强业务直接用范围路由扩容时按时间边界一切就行省很多事。2.3 扩容方案设计时必答的四个问题在正式动工之前我习惯先回答下面四个问题答完再设计迁移方案路由规则切换是即时生效还是渐变生效一次性切换风险高但架构简单渐变切换比如新老路由并存一段时间体验好但需要双写或双读实现复杂。迁移过程中业务能否接受只读如果能接受晚上低峰期只读 1 小时方案会简单很多如果完全不能接受就要做在线迁移。proxy 层是否支持路由规则热加载有些 proxy 改路由配置要重启重启期间连接全部断开支持热加载的版本可以平滑切换。失败回滚怎么办迁移到一半发现数据出错怎么回到扩容前状态回滚窗口要保留多久我在这次测试里给出的答案是低峰期执行、迁移过程中旧库保持读写、新库同步追平后执行短时间只读、然后切换路由。这个方案对业务影响最小但对 proxy 和迁移工具的要求最高。3. 扩容全流程拆解从评估到切换的六个关键节点3.1 节点一数据体量与路由分布基线评估这一步别偷懒。我在测试环境里先用 SQL 统计了每个旧分片的行数、数据大小、主键键范围然后按新路由规则模拟计算出每个新分片预期应该容纳多少数据。比如订单表order_id % 8后理论上 0 到 7 号新分片各占约 12.5% 的数据。但因为取模的特性如果旧数据的主键分布不均匀实际比例可能偏差很大。所以我会额外做一个“模拟迁移预演”用 SQL 在旧库上直接按新分片规则分组统计行数确认每个新分片的压力是否会失衡。这一步在测试环境里很容易做但在生产环境要小心——全表扫描统计很重建议在从库上跑或者用抽样估计。3.2 节点二目标物理分片初始化与同步链路搭建新增加 4 个物理分片后先把表结构完整建好包括索引、分区、约束、触发器等。这里有个坑如果直接用CREATE TABLE LIKE复制结构有些 proxy 或分库分表组件会在建表时插入自己的元数据容易出问题。我在测试中用的方式是把旧分片的建表语句导出来人工核对后再执行。建完表后搭建旧分片到新分片的数据同步链路。同步链路我推荐用 binlog 增量同步加初始全量导入的方式。具体分两步第一步先用mysqldump或者数据导入工具把旧分片的数据全量导入到对应新分片第二步启动 binlog 同步任务把全量导入期间产生的新增变更持续同步到新分片直到追平。这一步我通常会预留足够的时间让同步任务跑完并持续观察增量积压为 0。经验全量导入时建议按主键范围分批执行不要一把梭。因为大事务会导致从库延迟飙升也可能触发 proxy 的连接超时。我当时把单批控制在 5 万行以内效果比较稳定。3.3 节点三增量同步追平与数据一致性校验同步链路搭建好之后最关键的就是“追平校验”。增量同步追平不等于数据完全一致因为全量导入那一刻的快照点和 binlog 同步的起点如果没对齐就可能丢数据或重复数据。我在测试环境里验证过两种校验方式一是节点间行数和校验和对比在旧分片和新分片分别跑CHECKSUM TABLE比较结果简单粗暴但可能因为数据量大耗时较长二是抽样明细比对针对订单号、金额、状态等关键字段按新路由规则抽取若干主键分别在旧库和新库查询并逐字段对比。我强烈建议两条腿走路先做全量行的校验和对比再做抽样字段级对比。只做校验和可能漏掉字段值差异只做抽样可能漏掉少量问题数据。3.4 节点四路由规则切换前的“预演与只读窗口设计”路由切换是整个扩容过程中风险最集中的环节。sharding proxy 的配置里如果不支持无缝切换你就必须在切换前让系统进入一个“只读窗口”——也就是让业务流量只读不写或者把写流量暂时切到旁路。我这次测试里设计了一个非常保守的方案先通过 proxy 的流量控制功能或者网关配合把写流量摘掉等待增量同步完全追平确认旧库和新库数据一致修改 proxy 的路由配置把分片规则从 4 分片改成 8 分片重新加载配置恢复读写流量。只读窗口的时间长短取决于增量同步追平的速度和 proxy 配置生效的速度。我在测试环境里全量数据千万级增量追平大概用了 10 分钟只读窗口控制在 15 分钟左右线上如果数据更大这个窗口会成倍拉长必须提前和业务方沟通好。3.5 节点五灰度切换与流量验证如果是生产环境我强烈建议不要一次性切全部流量而是先在 proxy 层面或者网关层面灰度放量。具体做法可以是在路由配置切换后先让 10% 的读流量过来观察 proxy 的错误率、慢查询、数据返回是否正常确认没问题后扩大到 50%最后放全量。我在测试环境里没法模拟完整的灰度比例但至少做了两件事一是切换后立即跑了提前准备的核心查询用例集覆盖每个新分片上的典型查询二是跑了写入用例确认新写的路由都打到正确的分片。这一步做得好基本能避免线上“切完才发现数据查不到”的尴尬。3.6 节点六回滚预案与演练扩容这种操作必须有一键回滚的能力哪怕你认为方案已经很完美了。我在测试环境里完整演练了一次回滚场景是这样的模拟路由切换后读到异常数据决定回滚。回滚步骤是再次把写流量摘掉把 proxy 路由配置恢复到旧 4 分片规则确认旧库数据完整因为旧库在迁移期间始终是生产读写库所以数据是完整的恢复读写流量新库保留等待下一次再尝试扩容。回滚相对容易但我还是建议把回滚步骤写成文档确保执行的人不是凭记忆操作因为真实故障时的紧张情绪会让人出错。4. 我在 sharding proxy 扩容测试中踩过的四个大坑4.1 增量同步的“假追平”binlog 位置不一致导致的数据空洞第一次测试时我全量导入完成之后启动了 binlog 同步等了半小时看到积压消息为 0就认为数据一致了。结果在数据校验阶段发现新分片里的某个订单比旧分片少了一条日志记录。排查下来发现全量导入导出时没有记录准确的 binlog 位点导致增量同步从错误的位置开始消费漏掉了一部分变更。这就是典型的“假追平”——同步任务显示已消费完所有积压但实际上起点错了漏掉的历史变更永远追不回来。之后我的做法是全量导出前记录 binlog 位点导入完成后立刻从该位点启动增量同步并且在校验阶段专门对比“旧库最新 binlog 位点”和“新库已同步位点”必须确保新库位点等于或超过旧库位点才算真正追平。这个坑特别隐蔽因为表面看“同步任务没有积压”很容易让人误判。建议同步任务加一个指标同步时间戳和源库最新时间戳的差值必须持续趋近于 0 才算健康。4.2 路由切换时的连接风暴proxy 连接池被瞬间打满第二次测试时我修改完 proxy 路由配置后恢复读写流量结果五分钟内 proxy 的活跃连接数直接飙升到上限大量查询超时。原因分析路由配置变化导致所有旧连接池里的连接失效客户端重新建立连接时出现“集中重连”远超出了 proxy 的预期连接数。解决方法是把 proxy 的连接池上限提前调大重启或加载配置时分批释放连接而不是一次性全部断开客户端侧保留连接池预热机制比如提前建立若干连接。我在测试环境里把连接池上限调大了两倍并在切换前提前预热了一部分连接之后连接风暴没有再出现。4.3 跨分片 join 和聚合查询在扩容后“变慢”的假象扩容之后新分片数量翻倍理论上单分片的数据量减半查询应该更快才对。但我测试时发现一个跨分片的订单汇总查询反而比扩容前慢了很多。排查后发现sharding proxy 对跨分片查询的处理方式是“下发到所有分片然后汇总”分片数量从 4 变成 8 后一次聚合查询的扇出数翻倍网络开销和汇总计算量大幅增加。如果业务查询条件没带分片键proxy 就要把所有分片都扫一遍分片越多这种查询越慢。这个问题不是扩容本身某个环节出错了而是扩容放大了设计缺陷。我提醒团队在分片数量翻倍之后必须重新梳理业务 SQL凡是没带分片键的查询要么改造要么通过索引或汇总表承接。4.4 数据校验脚本本身成为性能瓶颈第一次跑校验时我写了一个逐行SELECT对比脚本结果跑了几个小时都没结束。单条 select 语句虽然快但千万级数据量下逐行对比的延迟累积起来非常可观。后来换成了“批量拉取 内存对比”的方式每次按主键范围拉取 2000 条记录在脚本进程里做排序和对比整体耗时缩短了 80% 以上。校验脚本也要注意自身对源库的影响建议在从库上执行校验查询不要在主库上做大批量查询。5. 扩容后的验证策略光看数据对齐还不够性能和稳定性必须回归5.1 数据完整性核对清单扩容后的第一件事是数据完整性核对我的核对清单分三层第一层是全局行数对比每个新分片按新路由规则统计的行数等于旧分片按新规则归属的行数之和。这一层有问题就直接报警。第二层是抽样字段一致性抽取关键订单号、用户 ID、金额、状态字段分别在新库旧库查询比对。这一层主要抓“行数对了但字段值错了”的情况。第三层是业务语义校验找几条典型的业务链路比如“创建订单—支付—查询订单详情”走一遍完整流程确认端到端功能正常。5.2 性能回归测试的关键指标扩容不是搬完数据就算成功性能回归是另一道必答题。我测试时重点看了几个指标写入延迟扩容前每个分片写入延迟和扩容后新增分片的写入延迟是否在同一水平聚合查询耗时不带分片键的全局聚合查询的 P99 耗时代理吞吐量proxy 层面的 QPS 和 TPS 在路由切换后是否下降连接稳定性长时间运行后是否有连接泄漏或崩溃。我在测试里发现扩容后聚合查询的 P99 耗时反而上升了约 30%就是因为上面说的扇出数翻倍问题。这个数据要如实记录方便后续做 SQL 改造或增加汇总表。5.3 长期观察窗口的设定扩容完成后不要急着把监控告警撤掉。我一般会设定一个 72 小时的观察窗口重点盯增量同步任务是否还在运行、有没有隐性堆积新旧分片的磁盘增长速度是否均匀proxy 错误日志里有没有路由异常的告警各分片主从延迟是否正常。这个窗口期里如果一切稳定基本可以判定扩容成功一旦有任何异常第一时间回滚或切换流量别犹豫。6. 测试环境里做不到、但生产环境一定要补的几件事测试环境毕竟不能 100% 还原生产的并发压力和数据分布有几件事我建议你一定在生产计划里补上流量影子回放在低峰期把真实的线上读流量在扩容后的 proxy 上做影子回放验证路由规则的正确性而不仅仅靠手工构造的查询用例。这个方法能非常有效地发现那些“你自己根本想不到”的查询路径。故障注入演练模拟某个新分片从库宕机、同步任务中断、proxy 节点重启等故障观察整个系统是否还能保持可用或快速降级。我在测试环境做过一次 proxy 重启演练发现了客户端连接池没有快速回收失效连接的问题这就是靠故障注入才能暴露出来的。容量水位预留连续监控 72 小时之后根据实际数据增长率核算新增分片还能支撑多久并且提前规划下一次扩容的触发条件。不要等快满了才想扩容方案那时候你的选择空间会小很多。其他分片策略的并存验证如果你的系统后续可能会引入新的分片表提前在测试环境验证新旧分片策略能否并存于同一个 proxy 配置中避免将来某一天加新表时触发全局路由重新加载。7. 几点个人体会和最后的提醒这套扩容流程我完整跑下来之后最大的感悟是扩容方案本身不难难的是对细节的把控和对失败路径的穷举。比如 binlog 位点对齐、连接池预热的时机、校验脚本的运行方式这些听起来都是很小的事但任何一个环节出问题都可能导致扩容窗口延长或者数据一致性问题。别抱着“大概没问题”的心态去推进扩容应当在测试环境里把每个环节的失败场景都演练过再上生产。另外sharding proxy 的版本差异也值得关注。不同版本对路由配置热加载、连接管理、监控指标的支持程度不一样动手前先仔细读一下你所用版本的文档确认你依赖的那些特性确实存在而不是想当然。最后再分享一个偏门但有效的做法正式扩容前一两天把整个流程文档打印出来假装你已经成功完成扩容然后照着文档在测试环境再走一遍。这个“预演”能帮你发现文档里漏掉的步骤、混乱的顺序、甚至命令里抄错的参数。我在测试时就因为文档里漏写了旧库从库地址的切换步骤差点让同步任务连错实例。分库分表扩容量级这件事做一次沉淀下来一套可复用的流程和清单后面再遇到就只是执行问题了。如果你正卡在扩容测试的某个环节不妨对照这篇文章里的节点一步步过一遍大概率能找到问题所在。