TDengine WAL 预写日志机制 — 持久性保障与崩溃恢复

分类 :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 文件占磁盘太大怎么办?

  1. 确认 Commit 是否正常进行(检查是否有刷盘阻塞)
  2. 如果启用了 WAL_RETENTION_*,考虑减小保留量
  3. 检查是否有消费者消费过慢导致 WAL 无法截断

Q4: fsync 延迟太高导致写入慢?

  1. 使用 SSD 替代 HDD(SSD fsync ≈ 0.1ms vs HDD ≈ 5ms)
  2. 增大 WAL_FSYNC_PERIOD(攒批减少 fsync 次数)
  3. 3 副本场景下直接用 WAL_LEVEL=1

Q5: WAL 文件损坏了怎么办?

  • 3 副本:TDengine 自动通过 Raft 快照 + 日志复制从其他健康副本恢复
  • 单副本:损坏位置之后的数据丢失。这就是多副本和 WAL_LEVEL=2 的价值

参考

系统构架篇

数据模型

存储引擎

关于 TDengine

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

相关推荐
Geometry Fu7 小时前
《物联网安全》第3.1章 RFID安全
物联网·安全·rfid
HZZSDSCYZ8 小时前
2026年杭州电商新趋势:专业公司如何引领未来市场
大数据·人工智能·python
Ws_8 小时前
Git + Gerrit 第四课:合并冲突解决
大数据·elasticsearch·搜索引擎
城管不管8 小时前
什么是Prompt?
android·java·数据库·语言模型·llm·prompt
平安的平安8 小时前
从“数据孤岛“到“数据融合“:DolphinDB 多模引擎如何打通工业物联网的“任督二脉“
物联网
搞科研的小刘选手9 小时前
【经管方向EI会议】第七届经济管理与大数据应用国际学术会议(ICEMBDA2026)
大数据·区块链·可视化·管理·供应链·经济·消费者行为
这个DBA有点耶9 小时前
数据库管理工具+开发工具的融合:AI如何重塑DBA工作流?
开发语言·数据库·人工智能·sql·云计算·dba
小李云雾9 小时前
Redis 从入门到实战:核心知识点与架构搭建全解析
数据库·redis·架构
久菜盒子工作室9 小时前
港股创新药趋势走坏了吗
大数据·人工智能