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% 远好过半年后扩容。扩容路径(翻倍可行性)与缓存失效方案要在定分片数时一起想清楚,这两件事一旦延后,都会在扩容当天集中爆发;分片数是成本、性能与运维复杂度三者的平衡点,宁可富余不可紧绷。
延伸阅读
- PHP 实战:SaaS 系统:多租户分片数的另一种约束维度(租户隔离)
- go 实战:微服务 CPS:微服务集群容量规划的对照方法
- php 实战:秒杀系统:秒杀库存的分片键选择与热点约束
- MySQL 实战:分库分表订单系统:回到本书,2.4 节讲 Redis 路由缓存
