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 实现的五大核心机制:
- Leader 选举:如何从多个副本中选出唯一的写入者
- 日志复制:写入操作如何被安全地复制到多数派
- 快照机制:落后的 Follower 如何通过快照快速追赶
- 仲裁机制(Arbitrator):双副本场景如何实现高可用
- 配置变更:如何安全地添加/删除副本
核心概念速查表
| 概念 | 说明 |
|---|---|
| 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 只能投一票) |
| 候选人的日志不如本节点新 | 拒绝(安全性保证) |
| 以上都通过 | 同意,重置选举定时器 |
"日志更新"的比较规则:
- 先比较最后一条日志的 Term:Term 更大的更新
- 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 安全性):
- 统计所有 Voter 副本中 matchIndex >= N 的数量
- 数量 >= Quorum(多数派)
- 且 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 快照的作用
快照用于解决两个问题:
- WAL 空间回收:已提交的日志可以被截断,节省磁盘空间
- 落后 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 递增:
- arbToken:每个 VNode 维护一个仲裁令牌,MNode 必须出示正确的令牌才能指定 Leader
- arbTerm:仲裁任期号,每次指定操作递增,防止过期的指定命令生效
- 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,但:
- 旧 Leader 无法获得多数派确认 → 新写入无法提交
- 心跳超时后旧 Leader 停止接受新写入
- 新 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 会拒绝新的写入请求:
- 日志缓冲区已满(4096 条未确认) → 需等待 Follower 追赶
- 心跳超时(20 秒未收到任何响应) → Leader 怀疑自己已被隔离
- 快照传输占满带宽(极端情况)
- WAL 写入失败(磁盘故障)
Q8: 如何判断 Raft 复制是否健康?
sql
-- 检查所有 VGroup 的副本状态
SHOW <db>.VGROUPS;
-- 关注:所有副本都应该是 leader/follower 状态
-- 如果出现 offline/unsynced 状态,说明复制异常
-- 检查 MNode 状态
SHOW MNODES;
-- 应该有 1 个 leader + 2 个 follower
也可以通过 taosKeeper 监控面板观察:
- Leader 切换频率(频繁切换说明网络不稳定)
- 副本延迟(matchIndex 与 Leader 的差距)
参考
第一篇 系统构架
- 01-《TDengine 整体架构全景 --- 深度解析》
- 02-《集群拓扑深度解析 --- 节点发现、EP 机制与负载均衡》
- 03-《MNode 内部机制深度解析 --- SDB、事务引擎与 DDL 处理全链路》
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。