分类 :3.存储引擎 | 篇章 :03 WAL
适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-27
WAL(Write-Ahead Log)是 TDengine 保障数据持久性的核心机制------所有写入必须先追加到 WAL 日志文件,确认持久化后才向客户端返回成功。即使进程崩溃或机器宕机,WAL 中已写入的数据都可以在重启后恢复。
核心概念速查表
| 概念 | 说明 |
|---|---|
| WAL 文件(.log) | 二进制日志文件,存储 Raft 日志条目 |
| 索引文件(.idx) | 日志版本号到文件偏移的快速定位索引 |
| WAL_LEVEL | 日志级别:0=不写 / 1=写但不 fsync / 2=写且 fsync |
| fsync | 操作系统将文件缓冲区强制刷到磁盘的系统调用 |
| 版本号(ver) | 每条日志的递增序号,对应 Raft 的 log index |
| commitVer | 已通过 Raft 多数派确认的最高版本号 |
| appliedVer | 已应用到状态机(MemTable)的最高版本号 |
| WAL 截断 | 已提交且已刷盘的日志条目可被安全删除 |
详细解析
1. WAL 文件格式
1.1 日志文件结构(.log)
WAL .log 文件内部格式:
┌─────────────────────────────────────────────────────┐
│ WAL 日志文件 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Entry 1: SWalCkHead + Body │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ Entry 2: SWalCkHead + Body │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ Entry 3: SWalCkHead + Body │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ ... │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
单条 Entry 格式(SWalCkHead + Body):
┌──────────────────────────────────────┐
│ headCksum (4B) │ 校验头部完整性 │
│ bodyCksum (4B) │ 校验 Body 完整性 │
│ magic (8B) │ 魔数标识 │
│ ver (8B) │ 版本号(Raft index) │
│ msgType (2B) │ 消息类型 │
│ bodyLen (4B) │ Body 长度 │
│ ... 其他元数据 │ │
├──────────────────────────────────────┤
│ Body (bodyLen bytes) │
│ (实际的写入数据或 Raft 命令) │
└──────────────────────────────────────┘
1.2 索引文件结构(.idx)
WAL .idx 文件格式:
每条索引记录固定 16 字节:
┌────────────────────────────┐
│ ver (8B) │ offset (8B) │
│ 版本号 │ 在.log中偏移 │
└────────────────────────────┘
用途:按版本号快速定位日志条目在 .log 文件中的位置
查找复杂度:O(1)(版本号连续,直接计算偏移)
2. WAL 级别(WAL_LEVEL)
| 级别 | 行为 | fsync | 数据安全 | 写入性能 |
|---|---|---|---|---|
| 0 | 不写 WAL | --- | 崩溃必丢数据 | 最高 |
| 1 | 写入 OS 缓冲区 | 不调用 | 进程崩溃不丢,宕机可能丢 | 高 |
| 2 | 写入 + 定期 fsync | 按周期调用 | 宕机最多丢一个 fsync 周期 | 中 |
WAL_LEVEL 与 fsync 的关系:
WAL_LEVEL = 1:
write() → OS 缓冲区 → 后台由 OS 决定何时写盘
风险:OS 缓冲区在断电时丢失
WAL_LEVEL = 2, WAL_FSYNC_PERIOD = 3000:
write() → OS 缓冲区 → 每 3 秒 fsync() 强制写盘
风险:最多丢失 3 秒内的数据
WAL_LEVEL = 2, WAL_FSYNC_PERIOD = 0:
write() → fsync() → 确认持久化后才返回
风险:几乎无丢失风险
最安全但延迟最高
3. WAL 写入流程
数据写入 WAL 的完整流程:
VNode 收到 SUBMIT 请求
│
▼
① 分配版本号:ver = lastVer + 1
│
▼
② 构造 WAL Entry:
- 填写 SWalCkHead(ver, msgType, bodyLen)
- 计算 bodyCksum(CRC32)
- 计算 headCksum
│
▼
③ 追加写入 .log 文件:
- write(fd, entry, entrySize)
- 追加写入 .idx 文件(ver → offset 映射)
│
▼
④ 根据 WAL_LEVEL 决定是否 fsync:
- LEVEL 1: 不 fsync,直接返回
- LEVEL 2: 检查距上次 fsync 是否超过 WAL_FSYNC_PERIOD
- 超过 → fsync() → 返回
- 未超过 → 不 fsync,返回
│
▼
⑤ WAL 写入完成 → 可以进行 Raft 复制
4. WAL 文件轮转
单个 WAL 文件不会无限增长,达到条件后创建新文件:
WAL 文件轮转策略:
条件(满足任一即轮转):
- 单文件大小超过阈值(WAL_SEGMENT_SIZE,默认 0=不限)
- 达到轮转周期(WAL_ROLL_PERIOD,默认 0=不限)
- 手动截断时
文件编号:
00000000.log / 00000000.idx
00000001.log / 00000001.idx
00000002.log / 00000002.idx
...
meta 文件(meta-ver<N>):
记录当前有效的 WAL 文件范围和版本号范围
5. WAL 截断与空间回收
已提交且已刷盘的 WAL 可以被截断(删除旧文件):
WAL 截断条件:
可截断的条件:ver <= min(commitVer, appliedVer, snapshotVer)
即:该版本的数据已经:
① 通过 Raft 多数派确认(commitVer)
② 已应用到 MemTable(appliedVer)
③ 已刷盘到 TSDB 文件(通过 snapshot 确认)
截断后:
- 删除旧的 .log 和 .idx 文件
- 更新 meta 文件中的起始版本号
- 释放磁盘空间
额外保留(WAL_RETENTION_PERIOD / WAL_RETENTION_SIZE):
为数据订阅(TMQ)额外保留 WAL
消费者可以回溯消费历史数据
6. 崩溃恢复
VNode 重启时的 WAL 恢复流程:
VNode 启动
│
▼
① 读取 meta 文件 → 确定 WAL 文件范围和版本号范围
│
▼
② 读取 raft_store.json → 恢复 currentTerm 和 votedFor
│
▼
③ 确定恢复起点:
- snapshotVer = 最后一次成功刷盘的版本号
- 从 snapshotVer + 1 开始重放
│
▼
④ 逐条重放 WAL Entry:
for ver = snapshotVer + 1 to lastVer:
a. 从 .idx 文件获取偏移量
b. 从 .log 文件读取 Entry
c. 校验 headCksum 和 bodyCksum
- 校验失败 → 截断此处及之后的日志(不完整写入)
- 校验通过 → 继续
d. 重新应用到 MemTable
│
▼
⑤ 恢复完成:
- MemTable 恢复到崩溃前状态
- 以 Follower 身份加入 Raft 组
- 等待 Leader 心跳或发起选举
7. WAL 校验与损坏处理
WAL 完整性保障:
每条 Entry 有双重校验:
- headCksum: CRC32 校验头部字段(防止元数据损坏)
- bodyCksum: CRC32 校验 Body 内容(防止数据损坏)
恢复时发现损坏:
┌────────────────────────────────────────────┐
│ Entry N: ✓ headCksum OK, ✓ bodyCksum OK │ → 正常重放
│ Entry N+1: ✓ headCksum OK, ✓ bodyCksum OK │ → 正常重放
│ Entry N+2: ✗ 校验失败 │ → 截断此处
│ Entry N+3: (不再读取) │ 之后全部丢弃
└────────────────────────────────────────────┘
截断后丢失的数据:
- 如果是 3 副本,其他节点有完整数据 → 通过 Raft 日志复制补齐
- 如果是单副本 → 这部分数据永久丢失(这就是 WAL_LEVEL=2 的价值)
8. WAL 与 Raft 的关系
WAL 在 Raft 中的角色:
Leader 视角:
① 客户端写入 → 构造 Raft 日志条目
② 写入本地 WAL(ver = Raft log index)
③ 发送 AppendEntries 到 Follower(携带 WAL 内容)
④ 收到多数派确认 → 推进 commitVer
⑤ 应用到 MemTable → 推进 appliedVer
Follower 视角:
① 收到 Leader 的 AppendEntries
② 写入本地 WAL
③ 返回确认给 Leader
④ Leader 推进 commitVer 后通知 → 应用到 MemTable
WAL = Raft 日志的持久化形式
ver = Raft log index
一一对应,不分离
9. WAL 性能特征
WAL 写入性能分析:
顺序追加写(Sequential Write):
SSD: 500~2000 MB/s
HDD: 100~200 MB/s(但顺序写,性能可接受)
单条 Entry 大小:
- SUBMIT 消息:几十字节 ~ 数 KB(取决于批量行数)
- 典型 batch insert:1~4 KB
fsync 延迟:
SSD: 0.05~0.5 ms
HDD: 2~10 ms(受磁盘旋转延迟影响)
WAL_LEVEL=2 对延迟的影响:
如果 WAL_FSYNC_PERIOD > 0:攒批 fsync,平摊开销
如果 WAL_FSYNC_PERIOD = 0:每写必 fsync,延迟增加 0.05~10ms
代码示例
配置 WAL 参数
sql
-- 高安全性配置(金融/核心业务)
CREATE DATABASE critical_data
WAL_LEVEL 2
WAL_FSYNC_PERIOD 0; -- 每写必刷
-- 高性能配置(可容忍少量丢失)
CREATE DATABASE fast_data
WAL_LEVEL 1; -- 不 fsync
-- 均衡配置(推荐)
CREATE DATABASE balanced
WAL_LEVEL 2
WAL_FSYNC_PERIOD 3000; -- 每 3 秒 fsync
-- 为数据订阅保留 WAL
CREATE DATABASE with_tmq
WAL_LEVEL 2
WAL_RETENTION_PERIOD 86400 -- 保留 24 小时
WAL_RETENTION_SIZE 10485760; -- 最多保留 10GB
在线修改 WAL 级别
sql
-- 从低安全切换到高安全
ALTER DATABASE power WAL_LEVEL 2;
ALTER DATABASE power WAL_FSYNC_PERIOD 1000;
性能考量
WAL 参数选择矩阵
| 场景 | WAL_LEVEL | WAL_FSYNC_PERIOD | 说明 |
|---|---|---|---|
| 3 副本集群 | 1 | --- | Raft 多数派已保证安全 |
| 单副本 + 零丢失 | 2 | 0 | 每写必刷,最高安全 |
| 单副本 + 高吞吐 | 2 | 3000 | 最多丢 3 秒数据 |
| 测试/非关键数据 | 1 | --- | 追求极致写入速度 |
WAL 磁盘空间估算
WAL 空间 = 写入速率 × 保留时间
示例:
写入速率:10 万行/秒 × 100 字节/行 = 10 MB/s
Commit 间隔:约 30 秒(Buffer 85MB / 10 MB/s)
正常 WAL 大小:10 MB/s × 30s = 300 MB
如果启用 TMQ 保留(24 小时):
WAL 峰值:10 MB/s × 86400s = 864 GB
→ 需要限制 WAL_RETENTION_SIZE!
FAQ
Q1: WAL_LEVEL=0 适合什么场景?
几乎不推荐。仅适用于纯测试环境或可以随时重新导入的临时数据。任何进程异常退出都会丢失 MemTable 中的全部数据。
Q2: 3 副本时 WAL_LEVEL 需要设为 2 吗?
通常不需要。3 副本中数据已复制到多个节点------即使一个节点宕机丢失 WAL 缓冲区中的数据,其他节点仍有完整数据。WAL_LEVEL=1 在 3 副本场景下是安全且高效的选择。
Q3: WAL 文件占磁盘太大怎么办?
- 确认 Commit 是否正常进行(检查是否有刷盘阻塞)
- 如果启用了 WAL_RETENTION_*,考虑减小保留量
- 检查是否有消费者消费过慢导致 WAL 无法截断
Q4: fsync 延迟太高导致写入慢?
- 使用 SSD 替代 HDD(SSD fsync ≈ 0.1ms vs HDD ≈ 5ms)
- 增大 WAL_FSYNC_PERIOD(攒批减少 fsync 次数)
- 3 副本场景下直接用 WAL_LEVEL=1
Q5: WAL 文件损坏了怎么办?
- 3 副本:TDengine 自动通过 Raft 快照 + 日志复制从其他健康副本恢复
- 单副本:损坏位置之后的数据丢失。这就是多副本和 WAL_LEVEL=2 的价值
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
- 07-《端到端的消息流》
数据模型
- 01-《数据库创建与参数详解》
- 02-《超级表/子表/普通表》
- 03-《支持数据类型深度解析》
- 04-《TDengine Tag 设计哲学与 Schema 变更机制》
- 05-《TDengine 虚拟表实现原理》
存储引擎
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。
