TDengine RAFT共识协议 — 选举、日志复制、快照与仲裁

1.系统架构 > 06 Raft

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-16

概述

TDengine 使用 Raft 共识协议实现多副本数据一致性。集群中每个需要高可用的组件------MNode(vgId=1)和每个 VGroup(vgId>1)------都运行一个独立的 Raft 实例。Raft 保证在多数派节点存活的情况下,系统可以持续提供读写服务,且所有副本最终保持一致。

本文深入解析 TDengine Raft 实现的五大核心机制:

  1. Leader 选举:如何从多个副本中选出唯一的写入者
  2. 日志复制:写入操作如何被安全地复制到多数派
  3. 快照机制:落后的 Follower 如何通过快照快速追赶
  4. 仲裁机制(Arbitrator):双副本场景如何实现高可用
  5. 配置变更:如何安全地添加/删除副本

核心概念速查表

概念 说明
Leader 唯一接受写入请求的节点,负责将日志复制到 Follower
Follower 被动接收 Leader 的日志复制,响应读请求(配置允许时)
Candidate 选举过程中的临时状态,正在请求其他节点投票
Learner 只接收日志但不参与投票和选举的副本(用于扩容过渡)
Assigned Leader 仲裁模式下被 MNode 指定的 Leader(双副本场景)
Term 任期号(逻辑时钟),每次选举递增,用于识别过期的消息和 Leader
Log Index 日志条目的连续递增序号
Commit Index 已被多数派确认的最高日志序号,之前的日志保证不会丢失
Quorum 多数派数量 = 副本数/2 + 1(3 副本需要 2 个确认)
WAL 每个 Raft 实例的日志通过 WAL 持久化到磁盘
vgId Raft 组标识,MNode 固定使用 vgId=1,VNode 使用数据库分配的 vgId

详细解析

1. Leader 选举

1.1 选举触发

当 Follower 在选举超时时间内没有收到 Leader 的心跳时,触发选举:

复制代码
选举超时计算:

  基础值 = syncElectInterval(默认 4000ms)
  实际超时 = 随机值 ∈ [基础值, 2 × 基础值]
  
  默认情况:超时 ∈ [4000ms, 8000ms]
  
  随机化的目的:防止多个 Follower 同时发起选举导致选票分裂
1.2 选举流程
复制代码
Leader 选举流程:

  ┌────────────┐
  │  Follower  │
  └─────┬──────┘
        │ 选举超时(未收到心跳)
        ▼
  ┌────────────┐
  │ Candidate  │
  └─────┬──────┘
        │ 1. 任期号 +1(currentTerm++)
        │ 2. 投票给自己
        │ 3. 向所有 VOTER 副本发送 RequestVote
        │
        ├── 收到多数派投票 → 成为 Leader
        │     ├── 开始发送心跳
        │     └── 开始接受写入请求
        │
        ├── 收到更高 Term 的消息 → 退回 Follower
        │
        └── 超时未获得多数派 → 重新选举(Term 再 +1)
1.3 投票规则

一个节点收到投票请求时,按以下规则决定是否投票:

条件 投票决定
请求的 Term < 本节点 currentTerm 拒绝(过期请求)
本任期已投过票给其他节点 拒绝(每 Term 只能投一票)
候选人的日志不如本节点新 拒绝(安全性保证)
以上都通过 同意,重置选举定时器

"日志更新"的比较规则

  1. 先比较最后一条日志的 Term:Term 更大的更新
  2. Term 相同则比较 Index:Index 更大的更新

这确保了只有拥有最完整日志的候选人才能当选 Leader。

1.4 Learner 的特殊规则

Learner(学习者)节点:

  • 不参与投票:收到投票请求时返回拒绝
  • 不发起选举:永远不会变成 Candidate
  • 不计入多数派:Quorum 计算不包含 Learner
  • 正常接收日志:数据复制与 Follower 完全相同

Learner 的用途:在添加新副本时,先以 Learner 身份加入,等数据追齐后再提升为 Voter。

1.5 选举相关参数
参数 默认值 范围 说明
syncElectInterval 4000 ms 10ms ~ 约 2.8 天 选举超时基础值
syncHeartbeatInterval 1000 ms --- Leader 心跳发送间隔
syncHeartbeatTimeout 20000 ms --- 心跳响应超时(超时后 Leader 停止接受写入)

调优建议 :选举超时应该远大于网络往返延迟。跨机房部署时需要增大 syncElectInterval,否则网络抖动会频繁触发不必要的选举。

2. 日志复制

2.1 复制架构 --- 流水线模式

TDengine 采用流水线(Pipeline)复制模型,而非传统 Raft 的逐条确认模式:

复制代码
流水线复制 vs 逐条确认:

  逐条确认(传统 Raft):
    发送 entry 1 → 等待确认 → 发送 entry 2 → 等待确认 → ...
    延迟 = N × RTT(N 条日志需要 N 次往返)

  流水线复制(TDengine):
    发送 entry 1, 2, 3, 4, 5 → 收到 entry 1 确认 → 发送 6, 7, 8 → ...
    延迟 ≈ 1 × RTT + 处理时间(批量发送,持续流动)

流水线复制的核心思想:不等待前一条日志的确认就发送下一条,大幅提升多副本写入吞吐。

2.2 日志内存缓冲区

每个 Raft 实例维护一个环形日志缓冲区(容量 4096 条),用于暂存待复制和待提交的日志:

复制代码
日志缓冲区结构:

  容量:4096 条日志条目
  保留:已提交的日志保留最近 256 条(供慢 Follower 重传)
  
  ┌──────────────────────────────────────────────┐
  │ [committed entries (retained)] | [uncommitted entries] |
  │  ← 256 条保留 →              │← 待确认 →             │
  └──────────────────────────────────────────────┘
                                  ↑
                            commitIndex
2.3 写入通过 Raft 的完整流程
复制代码
写入 Raft 复制全流程:

  客户端写入请求
       │
       ▼
  Leader 收到请求
       │
       │ 1. 构建日志条目(当前 Term + 下一个 Index)
       │ 2. 追加到日志缓冲区
       │ 3. 写入本地 WAL
       │ 4. 触发流水线复制
       │
       ├─────────────────────────────────────┐
       ▼                                     ▼
  发送 AppendEntries 到 Follower A     发送 AppendEntries 到 Follower B
       │                                     │
       ▼                                     ▼
  Follower A:                          Follower B:
    验证 Term 和 prevLog                  验证 Term 和 prevLog
    写入本地 WAL                          写入本地 WAL
    返回 matchIndex                       返回 matchIndex
       │                                     │
       └──────────┬──────────────────────────┘
                  ▼
  Leader 检查多数派:
    matchIndex(A) >= entry.index ✓
    matchIndex(self) >= entry.index ✓
    → 多数派(2/3)已确认
    → 推进 commitIndex
       │
       ▼
  应用到状态机(FSM):
    VNode: 写入 MemTable
    MNode: 更新 SDB
       │
       ▼
  返回成功给客户端
2.4 Commit Index 推进规则

Leader 推进 commitIndex 的条件(严格遵循 Raft 安全性):

  1. 统计所有 Voter 副本中 matchIndex >= N 的数量
  2. 数量 >= Quorum(多数派)
  3. Index N 对应日志条目的 Term == 当前 Term

第 3 条是 Raft 的关键安全规则------Leader 不能提交前任 Term 的日志条目(必须通过当前 Term 的新日志间接提交),防止已提交日志被覆盖的极端场景。

2.5 慢 Follower 处理

当 Follower 响应慢或暂时不可达时:

复制代码
慢 Follower 重试策略:

  第 1 次重试:等待 100ms
  第 2 次重试:等待 200ms(100 × 2¹)
  第 3 次重试:等待 400ms(100 × 2²)
  ...
  第 5 次重试:等待 3200ms(100 × 2⁵)← 上限
  
  超过上限 → 重置复制管理器,重新探测 Follower 的 matchIndex
  
  如果 Follower 需要的日志已被 WAL 回收 → 触发快照传输
2.6 单副本优化

当 VGroup 或 MNode 只有 1 个副本时,跳过 Raft 协议开销

  • 不发送 AppendEntries(没有 Follower)
  • 直接写入 WAL 后立即推进 commitIndex
  • 直接应用到状态机并返回结果
  • 延迟 ≈ WAL 写入时间(通常 < 1ms)
2.7 心跳机制

Leader 定期向所有 Follower 发送心跳(默认每 1 秒):

  • 维持领导权:Follower 收到心跳后重置选举定时器
  • 推进 Follower 的 commitIndex:心跳中携带 Leader 的 commitIndex
  • 检测 Leader 存活 :如果 Leader 在 syncHeartbeatTimeout(默认 20 秒)内未收到任何 Follower 的心跳回复,Leader 会停止接受新的写入请求(返回错误),防止网络分区时出现"僵尸 Leader"
  • 慢心跳告警:心跳延迟超过 1500ms 时记录告警日志

3. 快照机制

3.1 快照的作用

快照用于解决两个问题:

  1. WAL 空间回收:已提交的日志可以被截断,节省磁盘空间
  2. 落后 Follower 追赶:当 Follower 需要的日志已被截断时,通过快照一次性同步全量状态
3.2 WAL 保留策略
复制代码
WAL 保留与截断:

  MNode:
    保留最近 tsMndLogRetention 条日志
    超出部分可被安全截断

  VNode:
    保留最近 257 条(固定保留量)
    + 额外保留:基于最慢 Follower 的 matchIndex
    WAL 总大小上限:8GB
    
  截断时机:
    快照完成后,截断 snapshot.lastApplyIndex 之前的日志
3.3 快照传输协议
复制代码
快照传输流程:

  Leader (Sender)                    Follower (Receiver)
      │                                    │
      │  发现 Follower 需要的日志已不存在     │
      │                                    │
      │  1. PREP 阶段                       │
      │  发送快照元数据(lastIndex, lastTerm) │
      │───────────────────────────────────→│
      │                                    │  打开快照写入器
      │  2. DATA 阶段                       │
      │  分块读取快照数据                     │
      │  逐块发送(每块附带序列号)            │
      │───────────────────────────────────→│  逐块写入
      │───────────────────────────────────→│  逐块写入
      │───────────────────────────────────→│  逐块写入
      │  ...                               │
      │                                    │
      │  3. END 阶段                        │
      │  发送完成信号                        │
      │───────────────────────────────────→│
      │                                    │  应用快照
      │                                    │  重建 WAL 基线
      │                                    │  恢复正常日志复制

超时与重传

  • 每个快照块发送后等待确认,超过 60 秒未确认则重发
  • 整个快照传输超时 180 秒
  • 发送缓冲区容量 1024 块
3.4 快照内容
Raft 实例 快照内容
MNode (vgId=1) 整个 SDB 的内存哈希表序列化(所有元数据:数据库、超级表、VGroup、用户等)
VNode (vgId>1) TSDB 数据文件 + 元数据存储 + VNode 状态信息

快照传输期间不阻塞 Leader 的正常写入。新的写入操作继续追加到 WAL,快照传输完成后再通过日志复制追赶增量。

4. 仲裁机制(Arbitrator)

4.1 双副本的困境

标准 Raft 要求多数派确认,对于 3 副本(Quorum=2)可容忍 1 节点故障。但如果只有 2 副本

  • Quorum = 2(需要两个都存活才能写入)
  • 任何一个节点故障 → 无法形成多数派 → 写入不可用
  • 等于没有高可用
4.2 仲裁方案

TDengine 引入 Arbitrator(仲裁者) 角色,由 MNode 担任,为双副本 VGroup 提供高可用:

复制代码
仲裁机制示意:

  正常情况(双副本同步):
    VNode A (Leader)  ←→  VNode B (Follower)
         ↕                      ↕
         └──── MNode (Arbitrator) ────┘
               周期性心跳检查
  
  故障情况(VNode B 下线):
    VNode A (等待仲裁)        VNode B (离线)
         ↕
    MNode 检测到 B 离线
    且确认 A 和 B 之前是同步的
         ↓
    MNode 指定 A 为 Assigned Leader
         ↓
    VNode A (Assigned Leader) → 可单独接受写入
4.3 Assigned Leader 状态

被仲裁者指定为 Leader 的节点进入特殊的 Assigned Leader 状态:

  • 可以本地提交:不需要多数派确认,直接写入 WAL 后即可推进 commitIndex
  • 不参与正常选举:不会因为选举超时而切换状态
  • 安全保证 :只有在仲裁者确认两个副本之前是同步的(isSync=true)时才会指定
4.4 仲裁安全机制

为防止脑裂,仲裁机制使用 Token 匹配Term 递增

  1. arbToken:每个 VNode 维护一个仲裁令牌,MNode 必须出示正确的令牌才能指定 Leader
  2. arbTerm:仲裁任期号,每次指定操作递增,防止过期的指定命令生效
  3. isSync 确认 :MNode 通过主动检查(ARB_CHECK_SYNC)确认两个副本数据一致后才标记 isSync=true
4.5 仲裁相关参数
参数 默认值 说明
tsArbHeartBeatIntervalMs 2000 ms MNode 向 VNode 发送仲裁心跳的间隔
tsArbCheckSyncIntervalMs 3000 ms MNode 检查双副本同步状态的间隔
4.6 仲裁 vs 3 副本对比
方面 双副本 + 仲裁 3 副本
存储开销 2 份数据 3 份数据
正常写入延迟 需要 2 个确认 需要 2 个确认(相同)
故障恢复时间 仲裁检测 + 指定(数秒) 自动选举(数秒)
容忍故障数 1 节点(需仲裁介入) 1 节点(自动)
数据安全性 依赖仲裁者的 isSync 判断 严格多数派保证

5. 配置变更

5.1 副本数变更

TDengine 支持在线变更 VGroup 的副本数(ALTER DATABASE REPLICA):

sql 复制代码
-- 将数据库从单副本改为 3 副本
ALTER DATABASE power REPLICA 3;

-- 将数据库从 3 副本改为单副本
ALTER DATABASE power REPLICA 1;
5.2 变更策略 --- 单步变更

TDengine 采用单步变更(一次只增减 1 个副本),而非 Raft 论文中的联合共识(Joint Consensus):

复制代码
3 副本 → 1 副本的变更路径:

  REPLICA 3 (A, B, C)
       │ 移除 C
       ▼
  REPLICA 2 (A, B)
       │ 移除 B
       ▼
  REPLICA 1 (A)

1 副本 → 3 副本的变更路径:

  REPLICA 1 (A)
       │ 添加 B(先作为 Learner)
       │ B 追赶完成后提升为 Voter
       ▼
  REPLICA 2 (A, B)
       │ 添加 C(先作为 Learner)
       │ C 追赶完成后提升为 Voter
       ▼
  REPLICA 3 (A, B, C)
5.3 Learner 追赶阈值

新副本以 Learner 身份加入后,当其 matchIndex 落后 Leader 不超过 10 条日志时,视为"追赶完成",可以被提升为 Voter 参与投票和 Quorum 计算。

6. Raft 状态持久化

6.1 必须持久化的状态

Raft 协议要求以下状态在节点重启后不丢失:

状态 存储方式 说明
currentTerm JSON 文件 当前任期号
votedFor JSON 文件 本任期投票给谁
日志条目 WAL 文件 所有未截断的日志
commitIndex WAL 元数据 已提交的最高日志序号
快照 数据文件 已应用的状态快照
6.2 持久化文件格式
json 复制代码
// raft_store.json
{
  "current_term": 15,
  "vote_for_addr": 1234567890,
  "vote_for_vgid": 2
}

写入采用原子更新模式:先写临时文件 → fsync → rename 覆盖,防止宕机导致文件损坏。

6.3 重启恢复流程
复制代码
节点重启恢复:

  1. 读取 raft_store.json → 恢复 currentTerm 和 votedFor
  2. 读取 WAL → 重建日志缓冲区(从 commitIndex 到最后一条)
  3. 获取快照信息 → 验证 WAL 与快照的衔接
  4. 以 Follower 身份启动
  5. 等待收到 Leader 心跳(加入现有 Raft 组)
     或选举超时后发起选举(如果自己应该成为 Leader)

代码示例

查看 Raft 状态

sql 复制代码
-- 查看 VGroup 的 Raft 角色分布
SHOW power.VGROUPS;
-- vgId | v1_dnode | v1_status | v2_dnode | v2_status | v3_dnode | v3_status
--    2 |        1 | leader    |        2 | follower  |        3 | follower
--    3 |        2 | leader    |        3 | follower  |        1 | follower

-- 查看 MNode 的 Raft 状态
SHOW MNODES;
-- id | endpoint     | role     | role_time
--  1 | node1:6030   | leader   | 2024-01-15 10:00:00
--  2 | node2:6030   | follower | 2024-01-15 10:00:00
--  3 | node3:6030   | follower | 2024-01-15 10:00:00

副本管理

sql 复制代码
-- 创建 3 副本数据库
CREATE DATABASE power REPLICA 3 VGROUPS 4;

-- 在线改变副本数(3→1)
ALTER DATABASE power REPLICA 1;

-- 手动均衡 Leader 分布
BALANCE VGROUP LEADER;

-- 查看 Leader 分布是否均匀
SHOW power.VGROUPS;

MNode 高可用

sql 复制代码
-- 部署 3 个 MNode 副本
CREATE MNODE ON DNODE 2;
CREATE MNODE ON DNODE 3;

-- 验证 MNode Raft 状态
SHOW MNODES;

-- 如果需要替换 MNode
DROP MNODE ON DNODE 3;
CREATE MNODE ON DNODE 4;

调整选举参数(跨机房场景)

bash 复制代码
# /etc/taos/taos.cfg

# 跨机房网络延迟较高,增大选举超时防止频繁选举
syncElectInterval  8000

# 心跳间隔也相应增大
syncHeartbeatInterval  2000

性能考量

Raft 对写入延迟的影响

复制代码
写入延迟构成(3 副本):

  客户端 → Leader:         网络 RTT₁
  Leader WAL 写入:         ~0.1ms(SSD)
  Leader → Follower:       网络 RTT₂(流水线,不等单条确认)
  Follower WAL 写入:       ~0.1ms
  Follower → Leader 确认:  网络 RTT₃
  
  总延迟 ≈ RTT₁ + max(本地WAL, RTT₂ + 远程WAL + RTT₃)
  
  同机房(RTT<1ms): 总延迟 ~1-2ms
  跨机房(RTT=5ms): 总延迟 ~10-15ms

吞吐优化

机制 效果
流水线复制 不等单条确认,持续发送,吞吐提升 5-10×
日志缓冲区 减少 WAL 随机读,提升重传效率
单副本优化 跳过网络复制,延迟接近裸写
批量 commit 多条日志一次性应用到状态机

关键参数影响

参数 增大的效果 减小的效果
syncElectInterval 减少网络波动导致的误选举 更快检测 Leader 故障
syncHeartbeatInterval 减少网络开销 更快维持 Follower 信任
syncHeartbeatTimeout 容忍更长的网络中断 更快发现僵尸 Leader

FAQ

Q1: 3 副本写入性能会下降多少?

相比单副本,3 副本写入延迟增加约 1 个网络 RTT(因为需要等待至少 1 个 Follower 确认)。在同机房部署下,额外延迟通常 < 1ms。吞吐方面,由于流水线复制机制,吞吐下降通常在 20-30% 以内。

Q2: Leader 故障后多久能恢复写入?

从 Leader 故障到新 Leader 当选的时间 = 选举超时 + 选举过程。默认配置下约 4-8 秒(随机选举超时 4000-8000ms)。减小 syncElectInterval 可以加快故障切换,但要权衡误选举风险。

Q3: Follower 临时下线再上线,数据会自动追赶吗?

是的。Follower 重新上线后,Leader 会自动检测其 matchIndex 落后的情况:

  • 如果需要的日志还在 WAL 中 → 通过日志复制追赶
  • 如果需要的日志已被截断 → 通过快照传输追赶
    整个过程对用户透明。

Q4: 网络分区时会出现双 Leader 吗?

不会出现数据不一致的双 Leader。虽然网络分区时旧 Leader 可能暂时不知道自己已不是 Leader,但:

  1. 旧 Leader 无法获得多数派确认 → 新写入无法提交
  2. 心跳超时后旧 Leader 停止接受新写入
  3. 新 Leader 在另一个分区通过更高 Term 当选

网络恢复后,低 Term 的旧 Leader 自动退回 Follower。

Q5: BALANCE VGROUP LEADER 做了什么?

该命令不迁移数据,只是触发 Leader 切换,使各 VGroup 的 Leader 均匀分布在所有 dnode 上。这样可以均衡各节点的写入负载,避免所有 Leader 集中在同一节点。

Q6: 双副本 + 仲裁 和 3 副本哪个更好?

  • 3 副本:更可靠,标准 Raft 保证,自动选举无需外部介入。推荐用于核心业务。
  • 双副本 + 仲裁:节省 1/3 存储空间,但依赖 MNode 作为仲裁者(MNode 本身需要高可用)。适用于存储成本敏感但可接受稍低可靠性的场景。

Q7: 什么情况下 Raft 复制会阻塞写入?

以下场景 Leader 会拒绝新的写入请求:

  1. 日志缓冲区已满(4096 条未确认) → 需等待 Follower 追赶
  2. 心跳超时(20 秒未收到任何响应) → Leader 怀疑自己已被隔离
  3. 快照传输占满带宽(极端情况)
  4. WAL 写入失败(磁盘故障)

Q8: 如何判断 Raft 复制是否健康?

sql 复制代码
-- 检查所有 VGroup 的副本状态
SHOW <db>.VGROUPS;
-- 关注:所有副本都应该是 leader/follower 状态
-- 如果出现 offline/unsynced 状态,说明复制异常

-- 检查 MNode 状态
SHOW MNODES;
-- 应该有 1 个 leader + 2 个 follower

也可以通过 taosKeeper 监控面板观察:

  • Leader 切换频率(频繁切换说明网络不稳定)
  • 副本延迟(matchIndex 与 Leader 的差距)

参考

第一篇 系统构架

关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。

相关推荐
码云之上2 小时前
万星入坞:我们如何用三层插件体系干掉巨石应用
前端·架构·前端框架
kyriewen2 小时前
一口气讲清楚 Monorepo、Turborepo、pnpm、Changesets 到底是什么?
前端·架构·前端工程化
Full Stack Developme2 小时前
Spring Boot 事务管理完整教程
java·数据库·spring boot
zzmgc44 小时前
纯静态 + Web Worker + 虚拟滚动:我是怎么让浏览器吃下 10MB JSON 不卡的
前端·架构
m0_702036534 小时前
mysql如何通过索引减少行锁范围_mysql索引与加锁逻辑
jvm·数据库·python
Tingjct4 小时前
git/gdb指令
大数据·git·elasticsearch
qxwlcsdn4 小时前
如何用 IndexedDB 存储从 API 获取的超大列表并实现二级索引
jvm·数据库·python
星辰_mya4 小时前
彩云之上——[特殊字符]的架构师
java·后端·微服务·面试·架构
YF02114 小时前
深入剖析 Kotlin 的高效之道与核心实战
android·kotlin·app