《数据再平衡》
把 TiKV
归类为”动态分区”策略的代表,讲了分裂/合并的一般性目标:自适应数据量、避免空集群冷启动。但那篇文章里的”负载分裂”只是教学示意——真实
TiKV
里,触发分裂的信号有两条独立管线:一条看
Region 静态大小(字节数、key 数),另一条看
Region 实时负载(QPS、字节速率、CPU
占用),且后者是 TiKV 4.0
之后才补上的能力,专门解决”小表全表扫描”这类静态大小判断完全捕捉不到的热点。合并方向同样不是简单的”数据变小就合并”——它要走一个类似两阶段提交的
PrepareMerge/CommitMerge
流程,避免脏读脏写。本篇钉住这两条管线各自的判定条件、执行路径,以及和
PD 端 hot-region-scheduler(第 7
篇)的分工边界。
本文是「TiKV / HTAP 内核」系列第 8 篇(共 18 篇)。上一篇:PD 元数据与调度;下一篇:TSO;→ 系列目录
版本锚定:TiKV 7.1–8.4。8.4.0 起
region-split-size默认值从 96MiB 改为 256MiB、max-merge-region-size从 20MiB 改为 54MiB,文内区分新旧默认值,不混用。
一、分裂管线一:按大小/按 key 数
TiKV 用两组独立的阈值判断”是否该切”:
| 维度 | 达到阈值即分裂 | 8.4.0 前默认 | 8.4.0 起默认 |
|---|---|---|---|
| Region 字节大小 | region-max-size(=
region-split-size / 2 * 3) |
96MiB → max 144MiB | 256MiB → max 384MiB |
| Region key 数 | region-max-keys |
960,000 → max 1,440,000 | 2,560,000 起算 |
来源:TiKV Development Guide Region;TiDB Docs
TiKV Configuration File、TiDB 8.4.0 Release
Notes。触发后,新分裂出的两个 Region
各自约等于
region-split-size(不是
region-max-size)——分裂点选在让两半大致均分的位置。
为什么两组阈值都要看:region
的字节大小由 RocksDB SST 的 TableProperties
估算得到(不必扫描整个
range),但统计行数在存在大量已删除/重叠版本时会失真——一个刚被批量删除的表可能仍占用较大字节数但
key
数极低,或者反过来。两个维度独立判断,任一触发都会分裂。
推荐 region 大小区间是 [48MiB, 256MiB];调大能减少 Region 总数(降低心跳与元数据开销),代价是单 Region 迁移/分裂耗时变长、查询大范围扫描的并发度下降(TiDB Docs Tune Region Performance)。
二、分裂的执行路径:不拷贝数据
分裂在 Raft 层是一条
BatchSplit admin
命令(早期版本叫
Split,只能切一刀;BatchSplit
支持一次提议切多刀)。raft_cmdpb.proto 里
SplitRequest 携带 split_key 和
new_region_id;分裂命令本身像任何写命令一样,要经过
propose → append → commit → apply(第 5
篇)才算生效。
关键点:分裂不搬运一个字节的数据。原
Region 覆盖 key range [a, b);分裂只是在
apply
阶段把它在元数据上变成两个
Region:[a, m) 和
[m, b),两者仍然共享同一个 RocksDB
实例——之后的读写按各自的 key range 过滤即可。这与 《数据再平衡》
里 B+ 树式”中点分裂”的直觉一致,但 TiKV
是逻辑分裂,物理数据的迁移(如果需要)留给
PD 后续下发的 move-peer 算子去做。
flowchart LR
R0["Region R<br/>[a, b)<br/>same RocksDB instance"]
R0 -- "BatchSplit at key m<br/>(Raft admin cmd, no data copy)" --> R1["Region R (derived)<br/>[a, m)"]
R0 -- "BatchSplit at key m" --> R2["Region R' (new id)<br/>[m, b)"]
R1 -. "still same RocksDB, filtered by key range" .- R2
share_source_region_size
字段(kvproto
SplitRequest)区分两类分裂:用户发起的分裂(例如按表分区手动
split,新 Region size 记为
0,需要重新统计)与负载/大小检查触发的分裂(新
Region 继承原 Region 一半的估算大小)。
分裂点的选择有两种策略,pd-ctl operator add split-region
暴露了这个选项:
scan:真实扫描 Region 找中间 key,精确但耗 I/O。approximate:用 RocksDB SST 的统计信息估算中点,更快但是近似值。
来源:TiKV pd-ctl 文档。默认按大小/key 数触发的自动分裂走的是内部统计路径,不需要每次全量扫描。
三、分裂管线二:Load Base Split
按大小分裂解决不了一个典型场景:一张小表(数据量远低于 96MiB/256MiB 分裂阈值)被大量并发访问——它只占一个 Region,所有请求打到同一个 Raft 组的 Leader,无论集群有多少节点都分不掉这个压力。TiDB 4.0 引入 Load Base Split,判定条件不看大小,看持续负载:
If a Region meets one of the following conditions for 10 consecutive seconds, TiKV tries to split the Region: - the sum of its read requests exceeds
split.qps-threshold; - its traffic exceedssplit.byte-threshold; - its CPU usage in the Unified Read Pool exceedssplit.region-cpu-overload-threshold-ratio。—— TiDB Docs, Load Base Split
| 参数 | 默认值(region-split-size < 4GB 时) | 引入版本 |
|---|---|---|
split.qps-threshold |
3000/s | v4.0 |
split.byte-threshold |
30MiB/s | v5.0 |
split.region-cpu-overload-threshold-ratio |
0.25 | v6.2 |
Load Base Split 在 TiKV
节点本地跑(raftstore::store::worker::split_controller),采样访问的
key range
分布,挑一个能让分裂后两半负载均衡的切点——这与按大小分裂”取中点”不同,它要考虑访问热度分布,可能选在数据量并不居中,但访问量对半分的位置。
这条管线完全不经过 PD:TiKV Store
自己发现某个 Region
持续过载,自己决定切、自己算切点,切完之后才通过 Region
心跳把新 Region 信息报给 PD。PD 端的
hot-region-scheduler(第 7
篇)解决的是另一个问题:Region
已经被分开之后,把热的那个 Region(或它的 Leader)挪到更空的
Store 上。两者是接力关系,不是同一个机制:
flowchart TD
A["Small table, single Region<br/>high QPS but low size"] --> B{"Load Base Split<br/>local to TiKV, no PD involved"}
B -- "qps/byte/cpu over threshold<br/>for 10s" --> C["BatchSplit at load-balanced key"]
B -- "under threshold" --> D["stay as one Region"]
C --> E["Two smaller Regions,<br/>each reported via region heartbeat"]
E --> F{"PD hot-region-scheduler"}
F -- "still hot on this store" --> G["transfer-leader / move-peer<br/>to spread across stores"]
常见的操作误区:把
hot-region-scheduler
调得更激进(缩短判定周期、降低阈值)并不能替代 Load Base
Split——如果热点集中在一个 Region 里,PD
只能把这个 Region 的 Leader 挪到别的 Store,挪不动”一个
Region 装不下的并发度”这个本质问题;必须先分裂,才有多个
Region 可以分散到多个 Leader 上。
Load
Base Split 之前:scatter-range-scheduler 与手动
split-region
Load Base Split 是 TiDB 4.0 才引入的能力。在那之前(以及今天遇到 Load Base Split 覆盖不到的场景),PingCAP 官方运维手册给出的是两条手动兜底路径(Best Practices for PD Scheduling):
- 单 Region
热点(小表被高并发扫描,压力集中在一个
Region):无法靠调度把”一个
Region”的压力分给多个节点,只能先手动下发
operator add split-region <region_id> --policy=scan|approximate把它切开,再让常规调度器把切开后的 Region 分散。 - 一张表的多个 Region 整体负载偏高,但单个 Region
未必触发热点判定:PD 提供
scatter-range-scheduler——给定一段 key range(通常是一张表的范围)和一个调度器名字,让 PD 在这段范围内单独跑一份 balance-leader/balance-region 逻辑,把这段范围内的 Region 强制打散到各个 Store,避免全局调度器因为”总体还算均衡”而忽略这张表内部的倾斜。TiDB HTTP API 把这个操作包装成POST /tables/{db}/{table}/scatter,方便运维直接对表操作,而不必手写 key range。
scatter-range-scheduler 是 PD
端
的调度器(server/schedulers/scatter_range.go),和
Load Base Split 不在同一层:前者只是”把已经存在的 Region
打散到不同 Store”,并不会分裂 Region;后者在 TiKV
端 决定”是否要把一个 Region 切开”。官方文档特别提醒
scatter-range-scheduler
可能和全局调度器打架,不建议长期挂在大表上,用完应该
stop-scatter
关掉——这是一个明确写在文档里的运维边界,不是隐藏细节。
四、合并:两阶段流程,不是简单的反向分裂
合并(Region Merge)由 PD 发起,两个条件必须同时满足,且要相邻:
| 条件 | 8.4.0 前默认 | 8.4.0 起默认 |
|---|---|---|
max-merge-region-size |
20MiB | 54MiB |
max-merge-region-keys |
200,000 | 540,000 |
以及一条时间约束:split-merge-interval(默认
1h)——新分裂出来的 Region、或 PD
刚启动后的一段时间内的
Region,都不会被立即拿来合并,避免”刚分裂就合并回去”的抖动。
合并不是分裂的简单逆操作,因为两个
Region 的副本此刻分布在不同的 Store
组合上,必须先让它们的副本落在同一组 Store
上(PD 先下发 move-peer
对齐副本分布),再走 Raft admin 层面的两阶段流程(源码
components/raftstore/src/store/fsm/apply.rs,ExecResult::PrepareMerge
/ CommitMerge /
RollbackMerge):
- PrepareMerge:源 Region(将被并入目标
Region 的那个)的 Leader 提议一条
PrepareMerge日志,其中记录目标 Region 的 epoch。提交后,源 Region 进入”准备合并”状态并设置一个 write fence——Peer::prepare_merge_fence字段存在的目的是:如果这个 Region 上有悲观锁(第 11 篇),必须等这些锁相关的写命令先 apply 完,才能真正提出PrepareMerge,否则可能把”已经删除的悲观锁”错误地带进合并后的状态。 - CommitMerge:目标 Region 的 Leader
确认源 Region
已经追平日志(
catch_up_logs)后,提议CommitMerge;提交后目标 Region 的 key range 扩展为两者之和,源 Region 的 peer 被销毁。 - RollbackMerge:如果目标 Region 的 epoch 在等待期间发生了变化(例如被其他调度打断),或者多数副本联署要求回滚,源 Region 会退出”准备合并”状态,恢复正常读写。
sequenceDiagram
participant PD
participant Src as Source region leader
participant Dst as Target region leader
PD->>Src: ensure replicas co-located with target (move-peer if needed)
PD->>Src: issue merge (via heartbeat operator)
Src->>Src: propose PrepareMerge (after write fence)
Note over Src: source enters "preparing to merge",<br/>rejects new proposals
Src->>Dst: source catches up logs
Dst->>Dst: propose CommitMerge
Note over Dst: target region range extended,<br/>source peer destroyed
alt epoch conflict or rollback requested
Src->>Src: propose RollbackMerge
Note over Src: source resumes normal serving
end
这套流程本质上是把”跨 Raft 组的原子变更”通过先冻结源、确认追平、再原子提交的方式实现,思路上和 第 10 篇 的 Percolator 两阶段提交同属”先准备、再统一收口”的家族,但这里协调的是 Region 元数据而不是用户事务,不要把两者混为一谈。
五、与 distributed/27 的对齐
《数据再平衡》
第三节把 TiKV 归入”动态分区”一类,用
check_split/check_merge
的教学伪代码说明”分裂看阈值,合并看相邻且都小”的一般规则;第
8.1 节的”负载分裂”示例(隔离热点 key
到独立分区)是示意性的,没有对应到真实参数。
本篇补的是三层机制细节,distributed/27 没有覆盖:
- 分裂在 TiKV 里有两条独立管线(大小/key 数 vs Load Base Split),不是一条统一的”负载判断”逻辑。
- 分裂是逻辑操作,不拷贝数据;只有后续调度才可能触发物理搬迁。
- 合并不是分裂的镜像操作,而是一个专门处理跨 Raft
组一致性的两阶段流程(
PrepareMerge/CommitMerge/RollbackMerge),且要先由 PD 完成副本对齐这个前置条件。
六、学术谱系、争论与开放问题
谱系:按 key range 动态切分存储单元并用共识协议复制,最早的生产级先例是 Bigtable(Chang et al., Bigtable: A Distributed Storage System for Structured Data, OSDI 2006)的 tablet 机制——tablet 是 Bigtable 里可分裂/合并的最小调度单元,Region 与 tablet 是同构概念的不同实现(TiKV 额外把 tablet 的复制协议从 Chubby 锁 + GFS 换成了 Multi-Raft)。TiKV Development Guide 在解释 Region 的设计取舍时,明确用”按 range 分片 vs 按 hash 分片”的对比框架,这正是 Bigtable/HBase 一脉沿用至今的分片范式选择题:range 分片利于扫描和动态分裂,代价是需要一个中心化的元数据服务(PD/Master)维护路由表;hash 分片省掉了路由表,但牺牲了范围查询的局部性。
争论:静态大小阈值和 Load Base Split 该以谁为主,工程实践里没有定论。纯大小阈值的问题是响应不了小表热点(本文 §3 已展开);纯负载阈值的问题是可能对着一个持续增长但访问均匀的大表反复触发不必要的分裂,增加 Region 数量与心跳开销。TiKV 选择的是”两条管线并行,任一触发都切”的折中,而不是用负载阈值完全取代大小阈值——这本身是一种没有理论最优解、只能靠生产反馈迭代阈值的工程判断,PingCAP 官方文档也只给出经验默认值,没有给出理论推导。
开放问题:Load Base Split 的切分点选择依赖采样窗口内的访问分布,如果热点本身极度集中在单个 key(而不是一段 key range),任何切分都无法把这个单 key 的写压力分散到多个 Region——这是范围分片模型的结构性限制,不是参数调优能解决的问题。业界常见的规避手段是应用层加盐(key 前缀打散),但这会牺牲范围扫描的局部性;是否应该在存储引擎层提供”单 key 内部再分片”的原生机制,目前没有被 TiKV 或同类系统采纳,仍是开放的工程权衡。
七、常见误解
| 误解 | 事实 |
|---|---|
| 「分裂会先复制一份数据再切」 | BatchSplit 只改元数据和 key range
过滤规则,两个新 Region 仍指向同一个 RocksDB
实例,不拷贝数据 |
| 「热点问题靠调大 hot-region-scheduler 的敏感度就能解决」 | 单 Region 装不下的并发度,必须先分裂出多个 Region 才有”挪 Leader”的空间 |
| 「合并是分裂的逆操作,一步到位」 | 合并需要先对齐副本分布,再走
PrepareMerge/CommitMerge/RollbackMerge
三态流程 |
| 「Region 越小越好,方便迁移」 | Region 数量过多会增加心跳、元数据与调度开销;推荐区间是 48MiB–256MiB,不是越小越好 |
| 「按 key 数和按大小是同一个判断」 | 两者独立统计、独立触发,删除产生的大量 tombstone 会让两个维度的判断结果不一致 |
八、小结
三句话小结:
- TiKV 的分裂由两条独立管线驱动:静态的大小/key 数阈值(8.4.0 起默认 256MiB/2,560,000 keys),和响应持续 10 秒过载的 Load Base Split(QPS/字节/CPU 三个阈值),后者专门解决静态阈值捕捉不到的小表热点。
- 分裂在 Raft 层是一条不拷贝数据的
BatchSplit命令;合并则是需要先对齐副本分布、再走PrepareMerge → CommitMerge(或RollbackMerge)两阶段流程的跨 Region 操作。 - PD 的
hot-region-scheduler和 TiKV 本地的 Load Base Split 是接力关系——后者先把一个扛不住的 Region 切开,前者再把切开后的 Region/Leader 分散到不同 Store,谁也替代不了谁。
下一篇进入 TSO:PD 如何用物理+逻辑混合时钟分配全局单调递增的时间戳,以及它和 Spanner TrueTime 的路线分歧。
上一篇:PD 元数据与调度
下一篇:TSO
返回 系列目录
参考资料
规范 / 文档(A 级)
- TiKV Development Guide, Region(分裂/合并阈值、range 分片 vs hash 分片取舍)。
- TiDB Docs, TiKV Configuration
File(
region-max-size、region-split-size、region-max-keys、region-split-keys)。 - TiDB Docs, TiDB 8.4.0 Release Notes(region-split-size 96MiB→256MiB、max-merge-region-size 20MiB→54MiB 的版本边界)。
- TiDB Docs, Tune Region Performance、Region
Merge
Config(合并触发条件、
split-merge-interval)。 - TiDB Docs, Load Base
Split(
split.qps-threshold、split.byte-threshold、split.region-cpu-overload-threshold-ratio及引入版本)。 - TiKV pd-ctl
文档(
operator add split-region --policy=scan|approximate、operator add merge-region)。
源码(A 级)
pingcap/kvproto,proto/raft_cmdpb.proto(SplitRequest、BatchSplitRequest、share_source_region_size)。tikv/tikv,components/raftstore/src/store/fsm/apply.rs(ExecResult::PrepareMerge/CommitMerge/RollbackMerge、exec_batch_split)。tikv/tikv,components/raftstore/src/store/peer.rs(prepare_merge_fence、pending_merge_state、catch_up_logs)。tikv/tikv,components/raftstore/src/store/worker/split_controller.rs(Load Base Split 采样与切点选择)。
论文(A 级)
- Chang, Fay, et al. Bigtable: A Distributed Storage System for Structured Data. OSDI 2006(tablet 分裂/合并,Region 模型的谱系起点)。
站内对读
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【TiKV / HTAP 内核】Region 模型:range、epoch、peer
拆解 TiKV Region 的 range、RegionEpoch(conf_ver/version)、peer/leader 三个核心字段,纠正常见误解;split 只给直觉,机制留给第 8 篇。
【TiKV / HTAP 内核】生产排障:Region 过多、热点、TSO 抖动、锁冲突、apply 积压
用官方 Grafana 指标与系统表钉住五类高频生产故障的根因链:Region 过多、写/读热点、PD TSO 抖动、悲观锁冲突、raftstore apply 积压;给出症状到机制的决策树,不重复调参手册。
【TiKV / HTAP 内核】HTAP / 分布式 KV 全景:TiDB–TiKV–PD–TiFlash 四件套
定位 TiKV 7.x/8.x 在 RocksDB 单机 LSM、Raft 协议与 HTAP 范式之间补的那一层:TiDB、TiKV、PD、TiFlash 四组件各自的抽象边界,以及本系列 18 篇的读法。
【TiKV / HTAP 内核】Multi-Raft:一 Region 一 Raft 组
拆解 TiKV 的 Multi-Raft 架构:为什么每个 Region 是独立的 Raft 组,与 etcd 单 Raft 组对照差在哪;跨 Region 事务代价作为开放问题收束,不重写 Raft 协议本身。