TiKV 把整个集群的数据看成一张全局有序的 Key-Value 大表,然后按 Key 的字典序切成一段一段,每一段就是一个 Region。这个设计初看很像「分库分表」,但如果照着「一张业务表对应一个 Region」的直觉去理解,很快就会被现实打破:一张几十 GB 的表可能对应成百上千个 Region,一个小表也可能和别的表共享着某个 Region 的 Key 区间边界。
本文是「TiKV / HTAP 内核」系列第 3 篇,只讲 Region
这个数据结构本身:它的 range
怎么定义、RegionEpoch 的两个字段各管什么、peer
和 leader 是什么关系。Region 如何用 Multi-Raft 复制留给第 4
篇;split/merge 的调度机制留给第 8
篇——这里只建立起后续篇目都要用到的静态模型。
本文是「TiKV / HTAP 内核」系列第 3 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 2 篇 · Key 编码与 MVCC 三 CF、时间戳 第 3 篇 · Region 模型 range、epoch、peer 第 4 篇 · Multi-Raft 一 Region 一 Raft 组
版本锚定:TiKV 7.x/8.x
kvprotometapb.Region/metapb.RegionEpoch/metapb.Peer定义;PD Wiki Metadata Management(A 级,tikv/pd官方 wiki)。
一、Region:一段连续的 Key 区间
metapb.Region(TiKV/PD 元数据协议,A
级)的核心字段是:
message Region {
uint64 id = 1;
bytes start_key = 2;
bytes end_key = 3;
RegionEpoch region_epoch = 4;
repeated Peer peers = 5;
}一个 Region 代表左闭右开的区间 \([start\_key, end\_key)\)。TiKV 官方文档把 TiKV 整体描述为「一个巨大的、有序的 Key-Value Map」,Region 是这个 Map 上做数据切分和迁移的最小单位(TiKV Replication and Rebalancing,A 级)。集群里所有 Region 的区间拼起来,覆盖整个 Key 空间,互不重叠——这一点由 split/merge 的实现逻辑保证,第 8 篇会具体讲这个不变量如何在源码里维护。
初始状态下,一个全新的 TiKV 集群只有一个 Region,覆盖 \((-\infty, +\infty)\)。随着数据写入增多,PD 会指挥某个 Region 执行 split,把它切成两个更小的 Region(第 8 篇)。因此一个 Region 与一张 SQL 表之间不存在固定的对应关系——这正是下面要纠正的误解。
flowchart LR
subgraph keyspace ["Global sorted key space"]
r1["Region 1<br/>[-inf, k1)"]
r2["Region 2<br/>[k1, k2)"]
r3["Region 3<br/>[k2, +inf)"]
end
r1 --> r2 --> r3
1.1 表和索引如何落进 Key 空间
要理解「Region 与表的关系」,先要知道 TiDB 怎么把表数据编码进 Key(TiDB Docs, TiDB Computing,A 级):
行数据 Key: t{TableID}_r{RowID}
唯一索引 Key: t{TableID}_i{IndexID}_{IndexedColumnsValue}
普通索引 Key: t{TableID}_i{IndexID}_{IndexedColumnsValue}_{RowID}
固定的 t{TableID}
前缀保证同一张表的所有行数据在 Key
空间里连续排列,同一个索引的所有条目也连续排列。这带来一个直接结果:一张表通常对应
Key 空间上的一段连续区间,但这段区间可能跨越多个
Region——表越大,横跨的 Region 越多;反过来,一个 Region
的区间边界完全由 split/merge
动作决定,不会特意在表的边界上停下(除非某些调度策略专门做了「表级隔离」优化,那属于第
8 篇的调度话题)。
flowchart LR
subgraph keyspace ["Key space (sorted)"]
t1a["t100_r1 .. t100_r5000"]
t1b["t100_r5001 .. t100_r9000"]
t2["t101_r1 .. t101_r200"]
end
subgraph regions ["Region boundaries (independent of table id)"]
ra["Region 1: [t100_r1, t100_r6000)"]
rb["Region 2: [t100_r6000, t101_r50)"]
rc["Region 3: [t101_r50, +inf)"]
end
t1a --> ra
t1b --> ra
t1b --> rb
t2 --> rb
t2 --> rc
上图刻意画出 Region 2 同时跨越表 100 的尾部和表 101 的头部——这在小表或刚建表、split 还没触发时是真实会发生的情况,也是「一张表一个 Region」说法站不住脚的直接证据。
常见误解:「一张表一个 Raft 组」
这个说法在两个方向上都错。第一,TiDB
会把每张表(以及每个索引)的行编码到各自独立的 Key
前缀区间(表 ID 编码在 Key
头部),但一张表的数据可能横跨很多个
Region——只要这张表足够大,超过单 Region
的默认体积上限(默认约 96 MiB–256 MiB 量级,具体阈值由
region-max-size
一类调度参数决定,且随版本调整),PD 就会持续对它做
split,产生更多 Region。第二,反过来,一个 Region
也不天然对应一张表:早期或小规模场景下,多张小表的数据可能落在同一个
Region 的 Key 区间内,直到触发 split。Region 与 Raft
组的对应关系是一对一的(第 4 篇),但 Region
与「表」的对应关系是一对多、动态变化的——这是本系列反复强调的第一个纠错点。
二、RegionEpoch:conf_ver 与 version 各管什么
RegionEpoch
只有两个字段(kvproto
metapb.RegionEpoch,A 级):
message RegionEpoch {
uint64 conf_ver = 1; // 配置变更版本号:加/删 peer 时自增
uint64 version = 2; // Region 版本号:split/merge 时自增
}这两个计数器各管一类变化,互不影响:
| 字段 | 何时自增 | 语义 |
|---|---|---|
conf_ver |
每次 Region 的 peer 集合发生变化(增/删副本、切换 Learner/Voter) | 「这个 Region 现在应该有哪些副本」的版本号 |
version |
每次这个 Region 参与 split 或 merge | 「这个 Region 现在管理哪段 Key 区间」的版本号 |
split 时的更新规则(PD/TiKV split 源码分析,A 级):原
Region 与新产生的 Region 的 version 都等于原
version 加上新产生 Region 的数量;merge
时,合并后 Region 的 version 等于两个原 Region
version
的较大值加一。这条规则带来一个有用的性质:只要两个
Region 的 range 有重叠,比较它们的 version
就能确定谁的元数据更新——version
更大的一定是更新的状态,不存在打平的情况(TiKV
源码分析系列对该结论的证明:range 只在 split/merge
时改变,每次改变都会把涉及 Region 的 version
抬高到比原值更大)。
2.1 为什么需要 epoch:陈旧路由信息的检测
TiDB 会在本地缓存 Region 路由信息(Key 区间 → Leader
所在节点)以减少每次请求都要问 PD
的开销。但缓存会过期:Region 可能已经
split、merge,或者副本已经迁移。TiKV
收到请求时会拿请求里携带的 RegionEpoch
与本地当前的 epoch 比较,不一致就返回
EpochNotMatch 错误,TiDB 收到后重新向 PD
拉取最新路由并重试(PD Wiki Metadata Management,A
级)。
sequenceDiagram
participant TiDB as TiDB (cached epoch=v1)
participant PD as PD
participant TiKV as TiKV leader
TiDB->>TiKV: request with region_epoch=v1
TiKV->>TiKV: compare with local epoch=v2 (after split)
TiKV-->>TiDB: EpochNotMatch error (carries latest region info)
TiDB->>PD: refresh region cache
PD-->>TiDB: updated region_epoch=v2
TiDB->>TiKV: retry with region_epoch=v2
这个机制的实际意义不止是「防止读错地方」。TiKV 的一个历史
bug 修复(tikv/tikv PR #4125,check region
epoch strictly,A
级源码证据)说明了更严格的场景:在启用了 merge
的三节点集群里,如果某个副本 A 已经完成 commit merge、告知
TiDB 最新 Region 信息,而副本 B 还没来得及应用这次
merge,TiDB 按新信息把请求发到 B,若不严格校验
epoch,请求可能落在一个已经不属于该 Region 逻辑范围的旧
snapshot 上,产生 KeyNotInRegion
之类的错误。严格比较
epoch,才能保证请求要么打到确实持有最新状态的副本,要么被拒绝重试,不会读到中间态。
三、Peer 与 Leader:同一个 Region 的多个副本
一个 Region 默认有 3 个副本(可配置),每个副本叫一个 Peer,分布在不同的 TiKV 节点(Store)上。这些 Peer 组成一个 Raft 组(第 4 篇),其中恰好一个 Peer 是 Leader,其余是 Follower(或 Learner,第 6 篇简述)。
| 概念 | 定义 | 归属关系 |
|---|---|---|
| Store | 一个 TiKV 进程实例 | 一个 Store 上有很多个 Region 的 Peer |
| Region | 一段 Key 区间 + 一个 Raft 组 | 一个集群里有大量并发的 Region |
| Peer | 某个 Region 在某个 Store 上的一个副本 | 一个 Region 通常有 3 个 Peer |
| Leader | 该 Region 当前 Raft 组里的主副本 | 每个 Region 恰好一个(或短暂缺失,重新选举期间) |
只有 Leader 才能处理该 Region 的读写请求(默认配置下;Follower Read/Stale Read 是可选优化,不改变「写必须经 Leader」这条规则)。这意味着同一个 TiKV 节点上,有的 Region 它是 Leader,有的 Region 它只是 Follower——TiKV 节点本身没有全局的角色,角色是逐 Region 定义的。
flowchart TB
subgraph store1 ["Store 1"]
p1a["Region A: Leader"]
p1b["Region B: Follower"]
end
subgraph store2 ["Store 2"]
p2a["Region A: Follower"]
p2b["Region B: Leader"]
end
subgraph store3 ["Store 3"]
p3a["Region A: Follower"]
p3b["Region B: Follower"]
end
p1a <-.raft group A.-> p2a
p1a <-.raft group A.-> p3a
p1b <-.raft group B.-> p2b
p1b <-.raft group B.-> p3b
PD 会主动做 leader 均衡调度(transfer-leader,第 7 篇),避免某个 Store 上堆积过多 Leader 而其余 Store 负载过轻。这也是为什么「Region 数量均衡」和「Leader 数量均衡」是两个独立的调度目标——一个 Store 上 Peer 很多不代表它负载很重,如果这些 Peer 大多是 Follower,实际读写压力会小很多。
3.1 副本数量与物理隔离
默认副本数(max-replicas)是 3,即 1 个
Leader 加 2 个 Follower(PD 配置文档,A
级)。副本数只是「有多少份」,PD 的拓扑标签(topology
labels)机制进一步决定「这几份分散在哪」:给每个 TiKV
节点打上机架、可用区一类标签后,PD 调度时会尽量把同一个
Region 的多个 Peer
分散到不同的物理隔离域,使单个机架或可用区故障不会同时打掉一个
Region 的多数副本(TiDB Docs, Schedule Replicas by
Topology Labels,A 级)。这一机制不改变
Region/Peer/Leader 的模型本身,但决定了「3
个副本」在故障场景下能提供多强的容灾能力——它是 PD
调度目标的一部分,第 7 篇会具体展开调度算子如何生成。
常见误解
- 「一张表一个 Raft 组」:已在第一节纠正——Region 与表不是稳定的一对一映射,Region 与 Raft 组才是。
- 「Peer 越多,Region 处理能力越强」:Peer 数量只影响容错能力(能容忍多少节点失败)和读扩展性(Follower Read),不影响写入吞吐——写入始终要经过 Leader 一个节点串行处理再复制给 Follower,Peer 数量增加反而会略微增加复制开销。
- 「Region 的 Key
区间是永久固定的」:
RegionEpoch.version的存在本身就说明区间会变——split 让区间变小、merge 让区间变大,这是第 8 篇要讲的调度动作,而不是 bug。
四、小结
三句话小结
- Region 是 TiKV 全局有序 Key 空间上的一段连续区间,由
start_key/end_key定义,与 SQL 表不是稳定的一对一关系。 RegionEpoch用两个独立计数器区分「谁在这个 Region 里」(conf_ver)和「这个 Region 管哪段 Key」(version),配合严格比较能检测并拒绝陈旧路由信息,避免读到中间态。- 一个 Region 的多个 Peer 组成一个 Raft 组,只有 Leader 处理默认读写,Region 数量均衡与 Leader 数量均衡是 PD 要分别优化的两个调度目标。
下一篇讲这些 Peer 如何用 Raft 复制、以及 Multi-Raft 和 etcd 单 Raft 组的本质差异:Multi-Raft:一 Region 一 Raft 组。
参考资料
规范 / 文档
- TiKV/PD
kvproto,metapb.Region/metapb.RegionEpoch/metapb.Peer定义(A 级)。 - PD Wiki(
tikv/pd),Metadata Management(A 级,路由缓存与EpochNotMatch机制)。 - TiKV Documentation, Replication and Rebalancing(A 级,Region/Peer/Store 基本概念)。
tikv/tikvPR #4125, check region epoch strictly(A 级源码与故障描述,严格 epoch 校验的必要性)。- TiDB Documentation, TiDB Computing(A
级,行/索引 Key 编码规则
t{TableID}_r{RowID}等)。 - TiDB Documentation, Schedule Replicas by Topology Labels(A 级,拓扑标签与副本物理隔离调度)。
- PD Configuration File Documentation(A
级,
max-replicas默认值)。
站内
上一篇:Key 编码与 MVCC
下一篇:Multi-Raft:一 Region 一 Raft 组
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【TiKV / HTAP 内核】Split / Merge / 热点:Region 何时该切、何时该并
拆解 TiKV Region 何时触发 size/key-count 分裂、如何用 BatchSplit 在不拷贝数据的前提下切分 RocksDB key range;PrepareMerge/CommitMerge 两阶段合并;Load Base Split 与 PD hot-region-scheduler 两层热点机制的分工,并对齐 distributed/27 的负载分裂叙事。
【TiKV / HTAP 内核】HTAP / 分布式 KV 全景:TiDB–TiKV–PD–TiFlash 四件套
定位 TiKV 7.x/8.x 在 RocksDB 单机 LSM、Raft 协议与 HTAP 范式之间补的那一层:TiDB、TiKV、PD、TiFlash 四组件各自的抽象边界,以及本系列 18 篇的读法。
【TiKV / HTAP 内核】Region · Multi-Raft · PD · Percolator · TiFlash
补齐 RocksDB 单 Region、Raft 协议与 HTAP 范式之间的工程链路:以 TiKV 7.x/8.x 拆解 Region、Multi-Raft、raftstore、PD/TSO 与 Percolator 事务,并以 TiFlash Learner 收束新鲜度;CockroachDB 对照与选型。
【TiKV / HTAP 内核】Key 编码与 MVCC:三 CF 与时间戳
拆解 TiKV 的 user key + start_ts/commit_ts 编码规则与 default/lock/write 三 CF 分工,并说明为什么这套 MVCC 与 RocksDB Snapshot 的 sequence 号是两层完全不同的机制。