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

【TiKV / HTAP 内核】Multi-Raft:一 Region 一 Raft 组

文章导航

分类入口
databasestorage
标签入口
#tikv#multi-raft#raft#etcd#raftstore#cross-region-transaction#consensus

目录

一个百节点的 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 事务至少要多付出:

  1. 多组 Raft 往返:prewrite 阶段要对每个涉及的 Region 各发一轮 Raft propose,事务涉及的 Region 数越多,理论最短路径的往返次数越多。
  2. primary/secondary 的提交依赖:Percolator 模型要求先确定 primary key 的提交结果,其余 secondary key 的最终状态才能确定(第 10–11 篇),这意味着跨 Region 事务的可见性不是所有 Region 同时刻生效,而是有一个短暂的、通过锁和 ResolveLock 兜底的中间状态。
  3. 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 Regionraftstore.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 论文没覆盖」的遗漏,而是复制粒度与事务协议必须一起设计的工程事实。


五、常见误解

  1. 「Multi-Raft 就是很多个 etcd 拼起来」:etcd 的单 Raft 组模型里,全局顺序是免费的;TiKV 的每个 Region 有各自的顺序,全局顺序需要额外协议构造。两者不是同一协议的简单堆叠。
  2. 「Region 越多,集群吞吐一定越高」:Region 数量提升并行度的同时,也提升了 raftstore 需要驱动的状态机数量和心跳/tick 开销,超过某个规模后管理开销反而成为瓶颈(第 8 篇的调度目标之一就是控制 Region 数量)。
  3. 「跨 Region 事务是 TiKV 实现的缺陷」:这是切分复制单位带来的结构性代价,不是可以靠打补丁消除的 bug——只要坚持按 range 切分并独立复制,就必须有额外协议处理跨切分边界的操作,这是第二节开放问题要强调的。

六、小结

三句话小结

  1. Multi-Raft 把 Raft 复制的单位从「整个集群」缩小到「单个 Region」,用并行的独立 Raft 组换取随节点数扩展的写吞吐,这是 TiKV 与 etcd 单 Raft 组模型的根本差异。
  2. 代价是失去了单 Raft 组免费提供的全局顺序:跨 Region 操作的原子性必须靠上层的两阶段提交协议(Percolator)弥补,且跨越的 Region 数越多、协议往返代价越高——这是分布式切分复制系统共有的开放问题,不是 TiKV 独有缺陷。
  3. raftstore 用少量线程池轮询驱动海量独立 Raft 状态机,是让 Multi-Raft 在工程上可行的执行框架,下一篇具体拆开一次写请求在这套框架里的 propose 到 apply 路径。

下一篇:raftstore 写路径:propose 到 Apply


参考资料

核心论文

  1. Ongaro, D. & Ousterhout, J., In Search of an Understandable Consensus Algorithm, USENIX ATC 2014(Raft 协议,仅引用安全不变量结论,机制详解见站内 Raft 解读)。

规范 / 文档

  1. TiKV Blog, TiKV Performance Tuning with Massive Regions(B 级,raftstore 驱动海量 Region 的开销来源)。
  2. TiKV Documentation, Replication and Rebalancing(A 级)。
  3. TiDB Documentation, TiKV Configuration File(A 级,hibernate-regions 配置项与默认值)。
  4. tikv/rfcs, 2019-03-04-hibernate-raft.md(A 级,Hibernate Region 设计动机)。

站内

  1. Raft 解读(协议本身不重复讲解)。
  2. distributed/39 分布式 KV 存储对比(etcd 单 Raft 组架构详解)。
  3. 第 3 篇 Region 模型本系列 index

上一篇Region 模型

下一篇raftstore 写路径:propose 到 Apply

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

同主题继续阅读

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

2026-07-16 · database / storage

【TiKV / HTAP 内核】Snapshot、log GC 与副本迁移

拆解 TiKV 落后副本如何靠 Raft Snapshot 追赶进度、Raft 日志如何在 apply 完成后安全截断(log GC),并简述 witness/learner 副本形态;TiFlash Learner 的列存链路留给第 14 篇。

2026-07-16 · database / storage

【TiKV / HTAP 内核】PD 元数据与调度:心跳、算子与调度器

拆解 PD 如何靠 store/region 心跳建立集群视图,用 balance-leader/balance-region/hot-region 等调度器算分,生成 transfer-leader、move-peer 等 Operator,再通过下一次心跳的响应把算子'建议'给 Region Leader;对照 Spanner placement driver 与 CockroachDB 去中心化再平衡的架构分歧。


By .