一个百节点的 TiKV 集群,同一时刻可能有几十万个 Region 在跑,也就意味着几十万个并发运行、彼此独立的 Raft 组。这和很多人对 Raft 的第一印象——「一个集群共享一份日志、一个 Leader」——完全不同。etcd 就是后一种模型:整个集群只有一个 Raft 组,所有 key 都在同一份复制日志里排队。TiKV 选择了前一种模型,叫 Multi-Raft:Region 是 Raft 复制的单位,每个 Region 独立选主、独立复制、独立提交。
这个设计决策解决了什么问题、又带来了什么新问题,是本文(系列第 4 篇)要讲的核心内容。Raft 协议本身——选举、日志复制、安全性证明——本站已有专门的 Raft 解读 讲清楚,这里只引用其安全不变量的结论,不重复讲解协议细节。
本文是「TiKV / HTAP 内核」系列第 4 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 3 篇 · Region 模型 range、epoch、peer 第 4 篇 · Multi-Raft 一 Region 一 Raft 组、跨 Region 事务开放问题 第 5 篇 · raftstore 写路径 propose → apply
版本锚定:TiKV 7.x/8.x raftstore 设计(TiKV Blog TiKV Performance Tuning with Massive Regions,B 级);Ongaro, D. & Ousterhout, J., In Search of an Understandable Consensus Algorithm, USENIX ATC 2014(Raft 论文,A 级,仅引用安全不变量)。
一、为什么不是一个集群一个 Raft 组
etcd 的单 Raft 组模型简单、正确性容易论证,但有一个结构性限制:所有写请求都要经过同一个 Leader,串行写入同一份日志。集群整体的写吞吐上限,等于单个 Raft 组(本质上是单机 Leader)能处理的日志复制速度上限。etcd 的定位是「小规模、强一致的元数据存储」(本站 distributed/39 已有详细对照),这个限制在它的目标场景里可以接受——协调服务本身不需要海量写入。
TiKV 的目标场景不同:它要承载一整个 TiDB 集群的 OLTP 流量,数据量可以到 TB 甚至 PB 级,写入并发可以到几万 QPS。如果仍然用单 Raft 组,所有 Region 的写请求都要排到同一条日志里,写吞吐完全无法随节点数扩展。
Multi-Raft 的做法:把 Key 空间切成很多 Region(第 3 篇),每个 Region 各自维护一份独立的 Raft 日志、独立选主。不同 Region 的写请求完全并行,互不阻塞——只要它们的 Leader 分布在不同节点上。TiKV 官方博客用「Region 数量增多如何影响性能」这一角度描述了这个架构的直接后果:每个 Region 都需要独立驱动自己的 Raft 状态机(tick、心跳、日志持久化),所以 Region 数量不能无限增长(TiKV Blog Performance Tuning with Massive Regions,B 级)——这是 Multi-Raft 用「并行度」换来的新代价,第 8 篇讨论 split/merge 调度时会具体展开。
flowchart TB
subgraph etcd_model ["etcd: single Raft group"]
e_leader["Leader"]
e_f1["Follower"]
e_f2["Follower"]
e_leader --- e_f1
e_leader --- e_f2
e_all_keys["ALL keys go through one log"]
e_leader --- e_all_keys
end
subgraph tikv_model ["TiKV: Multi-Raft"]
r1["Region A Raft group"]
r2["Region B Raft group"]
r3["Region C Raft group"]
r1 -.independent.- r2
r2 -.independent.- r3
end
1.1 与 etcd 的对照表
| 维度 | etcd(单 Raft 组) | TiKV(Multi-Raft) |
|---|---|---|
| 复制单位 | 整个集群共享一份日志 | 每个 Region 一份独立日志 |
| 写并行度 | 受限于单 Leader 串行提交 | Region 数量级的并行写 |
| 选主开销 | 一次选举影响整个集群 | 单个 Region 选举只影响该 Region 的读写 |
| 典型规模 | 数据量 GB 级、几千 key 更新频率 | 数据量 TB–PB 级、大量并发 Region |
| 扩容方式 | 加节点提升容错,不提升写吞吐上限 | 加节点承载更多 Region,提升整体写吞吐 |
| 跨 key 事务 | 天然在同一 Raft 日志里,顺序有保证 | 跨 Region 需要额外的分布式事务协议(第 10–11 篇) |
这张表最后一行是 Multi-Raft 最大的代价来源,下一节展开。
二、Multi-Raft 的代价:跨 Region 操作变复杂
单 Raft 组模型下,任意两个 key 的写入天然在同一条日志里排队,谁先谁后有唯一确定的全局顺序,两阶段提交、分布式锁这类协议根本不需要——这是 etcd 简单性的来源。
Multi-Raft 打破了这个免费的全局顺序:Region A 和 Region B 的 Raft 组各自独立提交,互相不知道对方的日志内容和进度。一旦一个事务需要同时修改分布在两个 Region 里的 key(这在 TiDB 里是常态:一次跨行更新几乎必然跨 Region),就必须依赖更上层的协议来保证原子性——这正是 Percolator 式两阶段提交(第 10 篇)存在的原因。
sequenceDiagram
participant TiDB
participant RA as Region A leader
participant RB as Region B leader
TiDB->>RA: prewrite key1 (primary)
TiDB->>RB: prewrite key2 (secondary)
RA-->>TiDB: lock ok
RB-->>TiDB: lock ok
TiDB->>RA: commit primary (commit_ts)
RA-->>TiDB: commit ok
TiDB->>RB: commit secondary (async)
Note over RA,RB: 两个 Raft 组各自独立提交,<br/>原子性靠上层 2PC 协议保证,非 Raft 本身提供
开放问题:跨 Region 事务的代价模型
跨 Region 事务至少要多付出:
- 多组 Raft 往返:prewrite 阶段要对每个涉及的 Region 各发一轮 Raft propose,事务涉及的 Region 数越多,理论最短路径的往返次数越多。
- primary/secondary 的提交依赖:Percolator 模型要求先确定 primary key 的提交结果,其余 secondary key 的最终状态才能确定(第 10–11 篇),这意味着跨 Region 事务的可见性不是所有 Region 同时刻生效,而是有一个短暂的、通过锁和 ResolveLock 兜底的中间状态。
- Region 数量越多,单事务潜在跨越的 Region 越多:这与第 8 篇讨论的 split 调度目标(避免热点 Region)之间存在张力——切分太细会增加跨 Region 事务的概率,切分太粗又会让单 Region 承载过多流量。
2.1 单 Region 事务的特殊路径
反过来看这个代价模型也有一个自然的推论:如果一个事务涉及的所有 key 恰好都落在同一个 Region,跨 Region 协调的开销就不存在,TiDB 会走一条更短的路径(TiDB Development Guide Transaction on TiKV,B 级)——单 Region 事务可以把 prewrite 和 commit 合并成一次 Raft propose 完成(1PC,one-phase commit),不需要 primary/secondary 的两阶段协调。这不是绕开了 Multi-Raft 的代价模型,而是代价模型在「跨越 Region 数 = 1」这个边界条件下退化成了单机事务问题。多 Region 事务还有一个折中优化叫 async commit:客户端不必等待所有 secondary key 都提交完成才认为事务成功,只需 primary key 提交后即可返回,其余 secondary 的提交异步跟进(同一份 TiDB 开发指南文档,B 级)——这降低了客户端感知到的延迟,但不改变「跨越的 Region 越多、需要的 Raft propose 往返越多」这条基本关系,只是把部分往返移出了关键路径。
这个代价目前没有被 Raft 协议本身解决,也不是 TiKV 独有的问题——任何按 range/hash 切分数据并独立复制的系统(CockroachDB 的 range、Spanner 的 tablet)都要面对同样的权衡。业界现有回应大体分两条路:一是尽量把频繁一起访问的数据聚合到同一个 Region/range(应用层的数据建模配合),二是优化跨节点两阶段提交的延迟(如异步提交 async commit、1PC 优化,仅在事务恰好落在单个 Region 时适用,第 10 篇会具体讲)。这仍是分布式事务系统设计中一个持续被讨论、没有统一解法的方向,本系列不承诺给出比现有工程实践更强的结论。
三、raftstore:驱动海量 Raft 组的执行框架
TiKV 不会给每个 Region 开一个独立的操作系统线程去跑 Raft 状态机——几十万个线程显然不现实。TiKV 用一套叫 raftstore 的执行框架,通过少量线程池轮询处理所有 Region 的消息(TiKV Blog,B 级):Region 的读写请求、心跳 tick、Raft 消息都被封装成任务,由 raftstore 线程池按批处理的方式驱动。
flowchart LR
subgraph raftstore ["raftstore thread pool"]
poll["poller"]
end
msg1["Region A message"]
msg2["Region B message"]
msg3["Region A heartbeat tick"]
msg1 --> poll
msg2 --> poll
msg3 --> poll
poll --> fsm1["Region A Raft FSM"]
poll --> fsm2["Region B Raft FSM"]
这个「多个独立状态机、共享少量执行线程」的设计,是 Multi-Raft 在工程上能落地的关键——协议层面每个 Region 逻辑独立,执行层面靠调度框架把开销摊薄。第 5 篇会具体讲一次写请求如何进入这套框架、经过 propose、append、commit 走到 apply。
3.1 Hibernate Region:给空闲 Region 减负
即便有 raftstore 线程池分摊开销,Region
数量本身仍然是成本——标准 Raft 实现要求 Leader 持续向
Follower 发送心跳(避免 Follower 认为 Leader
失联而触发不必要的选举),Region
数量越多,心跳消息总量越大,即便这些 Region
完全没有读写流量。TiKV 从 5.0.2 起默认启用 Hibernate
Region(raftstore.hibernate-regions,TiDB
Docs TiKV Configuration File,A 级;设计动机见
tikv/rfcs
2019-03-04-hibernate-raft):一个长时间空闲的
Region 会被标记为休眠状态,raftstore 不再给它的 Raft
状态机发送常规 tick
消息,从而不再产生周期性心跳;只有真正有读写请求到达时,才会唤醒对应的状态机恢复正常心跳节奏。
flowchart LR
active["Active region<br/>regular tick + heartbeat"]
idle["No read/write for a while"]
hibernate["Hibernated region<br/>tick suppressed"]
wake["New request arrives"]
active --> idle --> hibernate
hibernate --> wake --> active
Hibernate Region 直接回应了第一节末尾提到的代价:Region 数量能扩展写并行度,但也线性增加空闲状态机的心跳开销;这个特性把「空闲」和「活跃」两种状态分开处理,让代价只发生在真正有负载的 Region 上,是 raftstore 应对海量 Region 场景的一个具体工程解法,而不是理论上的免费扩展。
四、学术谱系:从单组 Raft 到 Multi-Raft 工程化
| 阶段 | 代表 work | 与本篇关系 |
|---|---|---|
| 共识基线 | Ongaro & Ousterhout, Raft, USENIX ATC 2014 | 单 Raft 组的安全不变量;Multi-Raft 不修改协议,只改变复制单位粒度 |
| 切分复制 | Huang et al., TiDB: A Raft-based HTAP Database, VLDB 2020 | Region + Multi-Raft 是 TiKV 工程主线的学术锚点 |
| 海量状态机 | TiKV Blog Performance Tuning with Massive Regions;RFC hibernate-raft | 心跳/tick 开销与 Hibernate Region 的工程回应 |
| 对照系统 | Taft et al., CockroachDB, SIGMOD 2020 | 同为 range + 独立 Raft,见第 16 篇 |
争论边界:Multi-Raft 是否优于「单 Raft + 上层分片代理」没有统一结论——前者把并行度推到数据切分边界,后者把复杂度留在代理层。TiKV 选择前者,代价是跨 Region 事务必须显式建模(第 10 篇)。这不是「Raft 论文没覆盖」的遗漏,而是复制粒度与事务协议必须一起设计的工程事实。
五、常见误解
- 「Multi-Raft 就是很多个 etcd 拼起来」:etcd 的单 Raft 组模型里,全局顺序是免费的;TiKV 的每个 Region 有各自的顺序,全局顺序需要额外协议构造。两者不是同一协议的简单堆叠。
- 「Region 越多,集群吞吐一定越高」:Region 数量提升并行度的同时,也提升了 raftstore 需要驱动的状态机数量和心跳/tick 开销,超过某个规模后管理开销反而成为瓶颈(第 8 篇的调度目标之一就是控制 Region 数量)。
- 「跨 Region 事务是 TiKV 实现的缺陷」:这是切分复制单位带来的结构性代价,不是可以靠打补丁消除的 bug——只要坚持按 range 切分并独立复制,就必须有额外协议处理跨切分边界的操作,这是第二节开放问题要强调的。
六、小结
三句话小结
- Multi-Raft 把 Raft 复制的单位从「整个集群」缩小到「单个 Region」,用并行的独立 Raft 组换取随节点数扩展的写吞吐,这是 TiKV 与 etcd 单 Raft 组模型的根本差异。
- 代价是失去了单 Raft 组免费提供的全局顺序:跨 Region 操作的原子性必须靠上层的两阶段提交协议(Percolator)弥补,且跨越的 Region 数越多、协议往返代价越高——这是分布式切分复制系统共有的开放问题,不是 TiKV 独有缺陷。
- raftstore 用少量线程池轮询驱动海量独立 Raft 状态机,是让 Multi-Raft 在工程上可行的执行框架,下一篇具体拆开一次写请求在这套框架里的 propose 到 apply 路径。
下一篇:raftstore 写路径:propose 到 Apply。
参考资料
核心论文
- Ongaro, D. & Ousterhout, J., In Search of an Understandable Consensus Algorithm, USENIX ATC 2014(Raft 协议,仅引用安全不变量结论,机制详解见站内 Raft 解读)。
规范 / 文档
- TiKV Blog, TiKV Performance Tuning with Massive Regions(B 级,raftstore 驱动海量 Region 的开销来源)。
- TiKV Documentation, Replication and Rebalancing(A 级)。
- TiDB Documentation, TiKV Configuration File(A
级,
hibernate-regions配置项与默认值)。 tikv/rfcs, 2019-03-04-hibernate-raft.md(A 级,Hibernate Region 设计动机)。
站内
- Raft 解读(协议本身不重复讲解)。
- distributed/39 分布式 KV 存储对比(etcd 单 Raft 组架构详解)。
- 第 3 篇 Region 模型、本系列 index。
上一篇:Region 模型
下一篇:raftstore 写路径:propose 到 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 内核】raftstore 写路径:propose 到 Apply
拆解一次 TiKV 写请求经 propose、append、commit、apply 四个阶段落到 RocksDB 的路径,钉住 raft 线程与 apply 线程的异步边界;不编造未实测的 QPS 数字。
【TiKV / HTAP 内核】Snapshot、log GC 与副本迁移
拆解 TiKV 落后副本如何靠 Raft Snapshot 追赶进度、Raft 日志如何在 apply 完成后安全截断(log GC),并简述 witness/learner 副本形态;TiFlash Learner 的列存链路留给第 14 篇。
【TiKV / HTAP 内核】PD 元数据与调度:心跳、算子与调度器
拆解 PD 如何靠 store/region 心跳建立集群视图,用 balance-leader/balance-region/hot-region 等调度器算分,生成 transfer-leader、move-peer 等 Operator,再通过下一次心跳的响应把算子'建议'给 Region Leader;对照 Spanner placement driver 与 CockroachDB 去中心化再平衡的架构分歧。