土法炼钢兴趣小组的算法知识备份

【TiKV / HTAP 内核】Region 模型:range、epoch、peer

文章导航

分类入口
databasestorage
标签入口
#tikv#region#region-epoch#peer#leader#split#key-range

目录

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 kvproto metapb.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 篇会具体展开调度算子如何生成。

常见误解

  1. 「一张表一个 Raft 组」:已在第一节纠正——Region 与表不是稳定的一对一映射,Region 与 Raft 组才是。
  2. 「Peer 越多,Region 处理能力越强」:Peer 数量只影响容错能力(能容忍多少节点失败)和读扩展性(Follower Read),不影响写入吞吐——写入始终要经过 Leader 一个节点串行处理再复制给 Follower,Peer 数量增加反而会略微增加复制开销。
  3. 「Region 的 Key 区间是永久固定的」RegionEpoch.version 的存在本身就说明区间会变——split 让区间变小、merge 让区间变大,这是第 8 篇要讲的调度动作,而不是 bug。

四、小结

三句话小结

  1. Region 是 TiKV 全局有序 Key 空间上的一段连续区间,由 start_key/end_key 定义,与 SQL 表不是稳定的一对一关系。
  2. RegionEpoch 用两个独立计数器区分「谁在这个 Region 里」(conf_ver)和「这个 Region 管哪段 Key」(version),配合严格比较能检测并拒绝陈旧路由信息,避免读到中间态。
  3. 一个 Region 的多个 Peer 组成一个 Raft 组,只有 Leader 处理默认读写,Region 数量均衡与 Leader 数量均衡是 PD 要分别优化的两个调度目标。

下一篇讲这些 Peer 如何用 Raft 复制、以及 Multi-Raft 和 etcd 单 Raft 组的本质差异:Multi-Raft:一 Region 一 Raft 组


参考资料

规范 / 文档

  1. TiKV/PD kvprotometapb.Region / metapb.RegionEpoch / metapb.Peer 定义(A 级)。
  2. PD Wiki(tikv/pd),Metadata Management(A 级,路由缓存与 EpochNotMatch 机制)。
  3. TiKV Documentation, Replication and Rebalancing(A 级,Region/Peer/Store 基本概念)。
  4. tikv/tikv PR #4125, check region epoch strictly(A 级源码与故障描述,严格 epoch 校验的必要性)。
  5. TiDB Documentation, TiDB Computing(A 级,行/索引 Key 编码规则 t{TableID}_r{RowID} 等)。
  6. TiDB Documentation, Schedule Replicas by Topology Labels(A 级,拓扑标签与副本物理隔离调度)。
  7. PD Configuration File Documentation(A 级,max-replicas 默认值)。

站内

  1. 第 2 篇 Key 编码与 MVCC本系列 index

上一篇Key 编码与 MVCC

下一篇Multi-Raft:一 Region 一 Raft 组

返回 系列目录 · 数据库索引

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-16 · database / storage

【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 的负载分裂叙事。


By .