TDengine 存储引擎概览 — TSDB 分层存储架构与数据流转全景

分类 :3.存储引擎 | 篇章:01 存储引擎概览

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

TDengine 的存储引擎以 VNode 为基本单元,每个 VNode 内部集成了 WAL、MemTable、TSDB、META、Cache 五大子系统,协同完成从写入到持久化到查询的全部数据生命周期管理。本文提供存储层的全景视图。

核心概念速查表

概念 说明
VNode 数据存储的基本单元,一个 VGroup 的一个副本
TSDB 时序数据存储引擎,管理数据文件的写入、读取、合并
META(TDB) 元数据存储引擎(B+树),管理子表信息和 Tag 索引
WAL 预写日志,保障写入的持久性和 Raft 复制
MemTable 内存中的写入缓冲区,数据落盘前的暂存地
Cache 基于 RocksDB 的 Last 值缓存,加速 LAST()/LAST_ROW()
STT Sorted String Table,排序落盘文件(类似 LSM-Tree 的 L0)
File Set 按时间范围组织的一组数据文件(.head/.data/.sma/.stt/.tomb)

详细解析

1. VNode 存储架构总览

复制代码
VNode 内部存储组件:

  ┌────────────────────────────────────────────────┐
  │                    VNode                         │
  │                                                  │
  │  ┌──────────┐  ┌──────────┐  ┌──────────────┐  │
  │  │   WAL    │  │ MemTable │  │    TSDB      │  │
  │  │ (预写日志)│  │ (写缓冲) │  │ (时序文件)   │  │
  │  └────┬─────┘  └────┬─────┘  └──────┬───────┘  │
  │       │              │               │           │
  │  ┌────┴──────────────┴───────────────┴──────┐   │
  │  │              Buffer Pool                  │   │
  │  │         (内存池,3 段轮转)                 │   │
  │  └──────────────────────────────────────────┘   │
  │                                                  │
  │  ┌──────────┐  ┌──────────┐  ┌──────────────┐  │
  │  │   META   │  │  Cache   │  │     TQ       │  │
  │  │(B+树元数据)│ │(Last缓存)│  │ (订阅队列)   │  │
  │  └──────────┘  └──────────┘  └──────────────┘  │
  └────────────────────────────────────────────────┘

2. 磁盘目录结构

复制代码
VNode 在磁盘上的目录布局:

  /var/lib/taos/vnode/vnode<N>/
  │
  ├── vnode.json              ← VNode 配置和状态
  ├── raft/
  │   └── raft_store.json    ← Raft 持久化状态(term, vote)
  │
  ├── wal/                    ← WAL 目录
  │   ├── 00000000.log       ← WAL 日志文件
  │   ├── 00000000.idx       ← WAL 索引文件
  │   ├── 00000001.log
  │   ├── 00000001.idx
  │   └── meta-ver<N>        ← WAL 元数据
  │
  ├── meta/                   ← META 元数据目录(TDB B+树)
  │   ├── main.tdb           ← 子表元数据
  │   └── main.tdb-journal   ← TDB 事务日志
  │
  ├── tsdb/                   ← TSDB 时序数据目录
  │   ├── current            ← 当前文件集元信息
  │   ├── v<fid>f<N>.head    ← 数据块索引文件
  │   ├── v<fid>f<N>.data    ← 列数据文件
  │   ├── v<fid>f<N>.sma     ← 预聚合文件
  │   ├── v<fid>f<N>.stt     ← STT 文件(排序落盘)
  │   └── v<fid>f<N>.tomb    ← 删除记录文件
  │
  ├── cache.rdb/              ← RocksDB Last 缓存
  │
  └── tq/                     ← 订阅/流计算状态

3. 写入数据流转

复制代码
一条数据从写入到落盘的完整路径:

  客户端 INSERT
       │
       ▼
  ① WAL 追加写入(持久化保障)
       │
       ▼
  ② Raft 复制到 Follower(多副本)
       │
       ▼
  ③ 写入 MemTable(内存跳表,按 UID+时间有序)
       │
       ├── 同步更新 Last Cache(如果启用 CACHEMODEL)
       │
       ▼
  ④ MemTable 达到阈值 → 转为 Immutable MemTable
       │
       ▼
  ⑤ Commit 线程后台刷盘:
       │
       ├── STT_TRIGGER > 1 → 写入 STT 文件(无序容忍)
       │                       后续合并到有序文件
       │
       └── STT_TRIGGER = 1 → 直接与已有文件合并写入
                              生成新的 .head + .data + .sma
       │
       ▼
  ⑥ 截断已提交的 WAL(释放磁盘)

4. 读取数据流转

复制代码
查询数据的读取路径:

  SELECT 查询
       │
       ▼
  ① 确定时间范围 → 定位相关 File Set
       │
       ▼
  ② 多层数据源合并读取:
       │
       ├── MemTable(最新,内存)
       │     └── 跳表遍历,按时间有序输出
       │
       ├── Immutable MemTable(正在刷盘的旧缓冲)
       │     └── 同上
       │
       ├── STT 文件(已落盘但未合并)
       │     └── 按块索引定位 + 解压读取
       │
       └── .data 文件(已合并的有序数据)
             └── .head 索引定位 → .data 读取 → 解压
       │
       ▼
  ③ 多路归并排序(Merge Sort by timestamp)
       │
       ▼
  ④ 应用过滤条件(WHERE)
       │
       ▼
  ⑤ 返回结果集

5. 各组件职责总结

组件 职责 存储介质 关键参数
WAL 写前日志,崩溃恢复 磁盘(顺序写) WAL_LEVEL, WAL_FSYNC_PERIOD
MemTable 写入缓冲,有序组织 内存 BUFFER
TSDB 时序数据持久存储 磁盘(文件组) DURATION, COMP, STT_TRIGGER
META 子表/Tag 元数据 磁盘(B+树) PAGES, PAGESIZE
Cache Last 值快速查询 磁盘(RocksDB) CACHEMODEL, CACHESIZE
TQ 数据订阅 offset 管理 磁盘 WAL_RETENTION_*

6. Buffer Pool 内存管理

VNode 的写入内存通过 Buffer Pool 统一管理,采用三段轮转机制:

复制代码
Buffer Pool 三段轮转:

  ┌──────────┐    ┌──────────┐    ┌──────────┐
  │  Free    │ →  │  InUse   │ →  │ OnCommit │
  │  (空闲)  │    │ (当前写入)│    │ (正在刷盘)│
  └──────────┘    └──────────┘    └──────────┘
       ↑                                │
       └────────────────────────────────┘
                  刷盘完成后回收
  
  段大小 = BUFFER / 3
  默认:256MB / 3 ≈ 85MB 每段
  
  当 InUse 段写满 → 转为 OnCommit → 触发刷盘
  Free 段成为新的 InUse → 继续接受写入

7. META 元数据引擎

META 子系统负责存储和索引 VNode 内所有子表的元信息,是查询路由和 Tag 过滤的基础:

复制代码
META 存储的内容:

  ① 子表注册信息:
     - uid: 子表唯一 ID(64 位整数,全局唯一)
     - name: 子表名称
     - suid: 所属超级表的 uid
     - createdTime: 创建时间
     - ttl: 子表级过期设置
     
  ② Tag 值:
     - 每张子表的所有 Tag 列的实际值
     - 作为查询中 Tag 过滤条件的数据源
     
  ③ Schema 版本:
     - 超级表的列 Schema(schemaVersion 递增)
     - 超级表的 Tag Schema(tagVersion 递增)
     - 用于处理 Schema 变更后新旧数据块的兼容


META 的存储引擎(TDB):

  底层使用 B+ 树实现,存储在 meta/main.tdb 文件中
  
  ┌─────────────────────────────────────────────────┐
  │  B+ Tree (main.tdb)                              │
  │                                                   │
  │  Key-Value 存储模式:                              │
  │                                                   │
  │  Table 1: name → uid 的映射                       │
  │    "d1001" → uid=880001                           │
  │    "d1002" → uid=880002                           │
  │                                                   │
  │  Table 2: uid → 子表元信息                        │
  │    uid=880001 → {suid, tags[], schema_ver, ...}  │
  │                                                   │
  │  Table 3: Tag 索引                                │
  │    (suid, tag_col_id, tag_value) → [uid列表]     │
  │                                                   │
  └─────────────────────────────────────────────────┘
  
  
  Tag 过滤的执行路径:
  
    SELECT * FROM meters WHERE location = 'Beijing'
         │
         ▼
    META Tag 索引查找:
      Key=(suid_meters, col_location, 'Beijing')
      → 返回 [uid=880001, uid=880005, uid=880023, ...]
         │
         ▼
    仅对这些 uid 查询 TSDB 数据
    → 避免全表扫描所有子表


  Schema 版本管理:
  
    ALTER TABLE meters ADD COLUMN pressure FLOAT;
    → tagVersion 不变,schemaVersion +1
    
    新写入的数据按新 Schema(含 pressure 列)
    旧数据块仍按旧 Schema(无 pressure 列)
    
    查询时:
    - 检查数据块的 schemaVersion
    - 如果块的版本 < 当前版本 → 缺失列填 NULL
    - 如果列已被删除 → 跳过该列
    → 无需重写旧数据,零成本 Schema 变更
META 参数 默认值 说明
PAGES 256 B+ 树的缓存页数
PAGESIZE 4 KB 每页大小
META 内存 PAGES × PAGESIZE ≈ 1MB 元数据缓存占用

8. 文件集(File Set)组织

TSDB 按时间范围将数据组织为多个文件集,每个文件集覆盖 DURATION 指定的时间跨度:

复制代码
文件集按时间线排列:

  时间轴 →
  ├── FileSet 0: [T0, T0+DURATION)
  │     ├── v0f0.head  (块索引)
  │     ├── v0f0.data  (列数据)
  │     ├── v0f0.sma   (预聚合)
  │     └── v0f0.stt   (STT)
  │
  ├── FileSet 1: [T0+DURATION, T0+2×DURATION)
  │     ├── v1f0.head
  │     ├── v1f0.data
  │     ├── v1f0.sma
  │     └── v1f0.stt
  │
  └── FileSet N: [T0+N×DURATION, T0+(N+1)×DURATION)
        └── ...

  fid = (timestamp - 起始时间) / DURATION
  
  查询时:根据 WHERE 时间条件确定需要读取的 fid 范围
          跳过不相关的文件集 → 减少 I/O

性能考量

存储引擎的设计权衡

设计决策 优势 代价
WAL 顺序写 写入延迟低(~0.1ms) 额外磁盘空间(临时)
MemTable 跳表 有序插入 + 快速范围查询 内存占用
按时间分文件 过期删除=删文件(O(1)) 文件数随时间增长
STT 延迟合并 写入吞吐高(追加写) 查询需多路合并
列式存储 压缩率高 + 列裁剪 点查需拼装行

关键性能指标

指标 典型值(SSD)
单 VNode 写入延迟 0.5~2ms(3 副本)
WAL 写入带宽 200~500 MB/s
压缩后存储效率 原始大小的 5%~20%
冷数据查询延迟 5~50ms(取决于数据量)
Last Cache 查询延迟 < 0.1ms

FAQ

Q1: 一个 VNode 占用多少内存?

主要内存 = BUFFER + PAGES×PAGESIZE + CACHESIZE。默认配置下约 260MB。通过 BUFFER × VGROUPS × REPLICA 估算整个数据库的总内存需求。

Q2: 数据是先写 WAL 还是先写 MemTable?

先写 WAL。WAL 保障了即使进程崩溃,已确认的数据也不会丢失。重启时通过 WAL 重放恢复 MemTable。

Q3: 为什么需要 STT 文件?

STT 是写入性能和查询性能的折中。高频写入时直接追加到 STT(快),后台异步合并到有序文件(保证查询性能)。STT_TRIGGER 参数控制这个权衡点。

Q4: VNode 目录可以放在不同磁盘上吗?

可以。通过配置多个 dataDir 并指定不同的 level(0/1/2)实现多级存储:level 0 放 SSD(热数据),level 1/2 放 HDD(冷数据)。

参考

系统构架篇

数据模型

数据模型

关于 TDengine

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

相关推荐
PascalMing10 小时前
TDengine 3.0+ 数据库数据导出与导入工具
tdengine·import·export·导入导出
kunge201310 小时前
1. Tmux 使用指南(入门篇)
后端·架构·操作系统
Full Stack Developme10 小时前
SQL like 与 正则 区别
数据库·sql·mysql
程序员老邢10 小时前
《技术底稿 41》从三机混跑到四机隔离:微服务集群环境拆分实战复盘
微服务·云原生·架构·devops·服务器运维·技术底稿·环境隔离
砍材农夫10 小时前
物联网 基于netty控制报文结构(报文分类)
网络·物联网·struts
zhojiew10 小时前
使用Debezium读取CDC事件并通过Flink任务写入Paimon表来构建实时数据管道的实践
大数据·flink
zhojiew10 小时前
使用AWS中国区Lambda集成Glue Schema Registry消费Kafka消息的实践
大数据·spark·etl
pixcarp10 小时前
Redis ZSet:底层设计与实践
数据库·redis·后端·学习·golang·web
我是一颗柠檬10 小时前
【MySQL全面教学】MySQL多表查询与JOIN Day6(2026年)
数据库·后端·sql·mysql