2.3 分片规则:多少片合适

分片数是分库分表项目里最尴尬的决策:定少了很快要扩容迁移,定多了成本与运维复杂度直接翻倍。常见的错误是按「现在的数据量」算分片数,而不是按「三五年后的数据量」。这一节给出我们实际使用的三段式估算方法,以及 16 个分片的完整论证过程。

场景:分片数受三个下限约束

分片数不是越大越好。每多一个分片,就多一份实例费、多一套监控、多一倍扩容时的工作量。但它有三个下限约束:容量下限(每片装得下规划期末的数据)、性能下限(每片扛得住峰值 QPS)、管理下限(每片数据量在运维可操作范围内,比如单片备份恢复时间不超过 2 小时)。

约束维度 计算方式 得到的下限 说明
容量下限 三年末主表行数 / 单片目标行数 12 片 目标单片 5000 万行内
性能下限 峰值 QPS / 单片可承载 QPS 11 片 单片实测可承载 400 QPS
管理下限 全量数据备份时间约束 8 片 单片备份恢复 2 小时内
综合取值 三个下限取最大后向上取 2 的幂 16 片 顺手兼顾翻倍扩容

取 2 的幂(8、16、32)是分库领域的惯例:翻倍扩容时路由从 %16 变 %32,旧分片恰好一分为二,数据边界清晰。取 16 而不是 12,就是为了让未来扩容路径自然。

方案:16 片的容量账与成本账

把 1.3 节的容量估算细化到分片维度:规划期末(三年)主表规模约 6 亿行,16 片后单片约 3750 万行,距离 5000 万行的舒适线还有 25% 余量。成本上,16 片单规格 4 核 24GB 实例的月费约为主干实例的 1/8,总成本比单库巨型实例(128 核 512GB)低 20%,但换来了独立的容灾边界与线性扩展能力。

分片数与查询 fan-out 的关系经常被忽略:广播类查询的耗时分片数越多越长。16 片时一次广播查询的网络往返加归并耗时约 900 毫秒,8 片时只要 500 毫秒。所以非广播查询占比高时可以少分片,广播查询多时宁可每片小一点、片数多一点。订单系统的广播查询只占 7%,16 片完全可以接受。如果对成本账感兴趣,5.4 节有完整的月度账单复盘。

对比项 8 片方案 16 片方案 24 片方案
三年末单片行数 7500 万(超标) 3750 万 2500 万
广播查询耗时 500ms 900ms 1300ms
月度实例成本 1 倍 1.7 倍 2.4 倍
运维复杂度 低 中 高
扩容路径 翻倍可直接 翻倍可直接 非二幂次

分片数之外的三个决策

分片数定了 16 之后,还有三个小决策影响最终规则。第一个是分库与分表是否同维度:我们 16 个物理库每个库 1 张订单表(库即分片),如果用「2 库乘 8 表」也能得到 16 分片,但同库 8 表共享实例资源,等于放弃了独立扩容能力。第二个是分片命名:从 ds_0 到 ds_15 顺序命名而不是按可用区命名,是为了让 %16 的结果与名字直接对应,路由排障时一眼可读。第三个是是否做分片预留:我们没有预留空分片(比如开通 ds_16 到 ds_31),因为空分片也要监控与备份成本,翻倍扩容时直接新建即可。

决策点 选项 我们的选择 理由
库表维度 16 库 1 表 / 2 库 8 表 / 1 库 16 表 16 库 1 表 资源完全隔离
命名方式 ds_0 到 ds_15 / 按可用区命名 顺序命名 与取模结果一一对应
预留分片 预留空片 / 不预留 不预留 空片也有维护成本
库内分表 无 / 每库再拆 2 表 无 单片 3750 万行不需要

行业经验数值参考

分片规模的判断需要一些经验锚点,下面是我们收集到的常见值,供对照参考。注意这些是「单表干到什么程度开始考虑拆」的参考线,不是硬性阈值:

数据规模 常见观点 我们的实测体感
单表 1000 万行以下 无需分表 索引优化即可,分库收益≈零
单表 1000 到 3000 万行 开始留意 配合冷热归档可再撑一两年
单表 5000 万行以上 性能明显劣化 buffer pool 受限,查询 IO 化
单表 2 亿行 必须处理 慢查询常态化,DDL 影响巨大
单库连接数打满 分库信号 先排查慢查询挤占连接
单库磁盘 IO 95% 以上 加实例或分库 加 IO 不如分库减负

这些数字与网上的公开讨论大体一致,但每家的业务模式不同:写多读少的系统阈值更靠后,范围查询多的系统阈值更靠前。真正的决策依据永远是自己系统的瓶颈观测数据,而不是别人表格里的行数。

补充一点容易搞混的概念:分片数与分片实例数可以不相等。运维成熟度高之后,可以在一台高配实例上跑多个分片(容器化部署),分片数保持 16 不变,实例数按成本优化。我们第一年坚持一对一(一个分片对应一个实例),是因为隔离性优先;第二年才逐步合并部分低负载分片到同实例。前提是监控能精确到分片维度,否则合并后性能互相干扰时无法定位。

再给一个实操提醒:分片数要在应用配置与数据库实例两个地方保持一致,任何一处错配都会产生静默故障。我们用一份中心化的路由配置同时生成应用配置与实例清单,从源头杜绝两边对不上。

配置:分片数与库表分布的写法

16 片的规则用 ShardingSphere 的行表达式写起来很简洁。行内代码里的 ds_${0..15} 就表示 16 个物理库,t_order 每库一张:

tables:
  t_order:
    actualDataNodes: ds_${0..15}.t_order
  t_order_detail:
    actualDataNodes: ds_${0..15}.t_order_detail

广播表(如渠道字典)写成 actualDataNodes: ds_${0..15}.t_channel 配合 broadcast: true 即可,写入时中间件自动向 16 个库广播。绑定表(t_order 与 t_order_detail)通过 bindingTables 配置保证同库,join 不跨片。

踩坑:8 片方案的半年之痒

我们最初定的就是 8 片方案,理由是「每片 7500 万行也能跑」。半年后大促订单量翻倍,某个分片率先到 4000 万行时性能开始劣化(buffer pool 装不下热数据),扩容到 16 片的迁移整整做了 5 天。账是这么算的:初期多买 8 个分片一个月多花约一倍实例费,而扩容迁移光人力成本就远超这个数,还搭上了一周的窗口风险。分片数宁可富余 30%,这是花真金白银买来的教训。

这次教训之后,分片数规划从「够用就行」变成了「按翻倍路径反推」:先确认未来三年最多翻多少倍,再把分片数定在第一次翻倍之前都不用动的位置。16 片的正下舒适区是 6 亿行(3750 万每片),翻倍后是 12 亿行,足够覆盖三年。

第二个坑是分片数变更影响缓存。路由缓存的 Key 里编码了分片数(见 2.4 节),扩容后旧缓存全部失效,必须做布隆式清理,否则应用会按旧分片号查到实为空的库。任何涉及分片数的变更都要配套缓存失效预案。

第三个坑是分片数会影响跨库场景的语义。项目里另一个用 8 分片的系统(小说阅读历史)对接订单大表做联合查询时,两套路由算法无法对齐,只能走应用层归并。分片数在公司层面如果完全不统一,跨系统集成时要付出双倍联通成本。

小结

分片数估算三步走:按期末数据量算容量下限、按峰值 QPS 算性能下限、按备份恢复时间算管理下限,三者取最大后凑到 2 的幂。分片数对成本与广播查询耗时都是线性影响,富余 30% 远好过半年后扩容。扩容路径(翻倍可行性)与缓存失效方案要在定分片数时一起想清楚,这两件事一旦延后,都会在扩容当天集中爆发;分片数是成本、性能与运维复杂度三者的平衡点,宁可富余不可紧绷。

延伸阅读