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 持久化到磁盘
vgIdRaft 组标识,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 选举相关参数
参数默认值范围说明
syncElectInterval4000 ms10ms ~ 约 2.8 天选举超时基础值
syncHeartbeatInterval1000 msLeader 心跳发送间隔
syncHeartbeatTimeout20000 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 仲裁相关参数
参数默认值说明
tsArbHeartBeatIntervalMs2000 msMNode 向 VNode 发送仲裁心跳的间隔
tsArbCheckSyncIntervalMs3000 msMNode 检查双副本同步状态的间隔
4.6 仲裁 vs 3 副本对比
方面双副本 + 仲裁3 副本
存储开销2 份数据3 份数据
正常写入延迟需要 2 个确认需要 2 个确认(相同)
故障恢复时间仲裁检测 + 指定(数秒)自动选举(数秒)
容忍故障数1 节点(需仲裁介入)1 节点(自动)
数据安全性依赖仲裁者的 isSync 判断严格多数派保证

5. 配置变更

5.1 副本数变更

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

-- 将数据库从单副本改为 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 协议要求以下状态在节点重启后不丢失:

状态存储方式说明
currentTermJSON 文件当前任期号
votedForJSON 文件本任期投票给谁
日志条目WAL 文件所有未截断的日志
commitIndexWAL 元数据已提交的最高日志序号
快照数据文件已应用的状态快照
6.2 持久化文件格式
// 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 状态

-- 查看 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

副本管理

-- 创建 3 副本数据库
CREATE DATABASE power REPLICA 3 VGROUPS 4;

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

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

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

MNode 高可用

-- 部署 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;

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

# /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 复制是否健康?

-- 检查所有 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 提供实时分析、可视化、事件管理与报警等功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TDengine (老段)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值