
TDengine 存储引擎深度解析行列格式、TDB 元数据引擎与 TSDB LSM 存储架构【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengineTDengine 的高写入与查询性能源于其为时序场景量身定制的一套存储技术栈区分 NONE/NULL 语义的自研行列格式、基于 BTree 的 TDB 元数据引擎以及采用 LSMLog-Structured Merge-Tree结构的 TSDB 时序存储引擎。本文基于 TDengine 官方的“存储引擎”内部原理文档展开并结合当前仓库中的源码实现完整还原从写入请求落 WAL、内存池刷盘到 head/data/sma/tomb/stt 各类数据文件的组织方式帮助开发者建立对 TDengine 数据在磁盘上“长什么样”的完整认知。为什么需要时序专属的存储算法相较于通用型数据库TDengine 在诞生之初就专注于时序数据场景的独特性数据按时间天然有序、写入持续且高并发、单条记录往往只覆盖 schema 中很少的列。TDengine 利用这些特点自主研发了针对时序数据的写入与存储算法对数据进行预处理与压缩——既大幅提升写入速度又显著降低存储占用从而在大量实时数据持续涌入时仍能保持高吞吐与低延迟响应。具体而言这套算法体现在三个层次行列格式层一套自研的行列式内存数据结构支持 NONE、NULL、有值三态共存与稀疏数据高效处理元数据层META/TDB基于 BTree 的键值存储引擎负责表、标签、索引等元信息时序数据层TSDBLSM 结构 文件组切分 多种辅助文件负责海量时序数据本体。行列格式NONE、NULL 与稀疏数据的统一表示业内已有 Apache Arrow 等标准化的行列格式库但 TDengine 面向的场景更加聚焦、性能要求更高因此自行实现了一套行列格式。其核心需求有三点支持**未指定值NONE与空值NULL**的区分支持 NONE、NULL 以及有值共存的不同场景对稀疏数据和稠密数据都能高效处理。在源码中可以直接看到三态的定义。include/common/trow.h 中定义了行级值类型#define TD_VTYPE_NORM 0x00U // normal val: not none, not null #define TD_VTYPE_NULL 0x01U // null val #define TD_VTYPE_NONE 0x02U // none or unknown/undefined这与 INSERT 语义一一对应NULL表示客户端显式写入的空值NONE表示客户端根本没有提供该列——两者在存储与查询语义上必须区分开。行格式Tuple 与 Key-Value 双编码TDengine 的行格式有两种编码具体采用哪一种由数据特征决定1. Tuple 编码格式Tuple 编码面向非稀疏数据即所有列全部有值、或只有少量 NONE 的场景。Tuple 行直接根据表 schema 提供的偏移量信息访问列数据访问复杂度为 O(1)速度最快。其布局由时间戳主键独立存放、各列连续排布的字段区flen 部分以及末尾的 bitmap 组成。源码中的 ASCII 布局注释与这一描述完全对应见 include/common/trow.h/* * |----------------------------- tlen ----------------------------------| * |--------- flen -------------|-- blen --| | * --------------------------------------------------------------------- * | first part | bitmap | second part | * --------------------------------------------------------------------- */行头STSRow结构include/common/trow.h中有一个statis位当行中存在 null 或 none 时置 1让读取路径可以快速跳过 bitmap 检查。2. Key-Value 编码格式Key-Value 编码专门用于稀疏数据schema 中定义了数千列但实际有值的列很少。此时若用 Tuple 编码会为海量未指定列预留空间造成极大浪费Key-Value 编码只记录“有值”的列通过offset 数组索引各列的值——访问速度比 Tuple 直接寻址略慢但显著减少空间占用。仓库源码印证了这套机制并且比文档描述走得更远。include/common/trow.h 中 KV 行的索引项为{colId, offset}对typedef struct { col_id_t colId; uint32_t offset; } SKvRowIdx; typedef struct { uint16_t ncols; SKvRowIdx cidx[]; } SKvRow;而是否选择 KV 行由两个比例阈值决定include/common/trow.h 与 L197#define KvConvertRatio (0.9f) #define isSelectKVRow(klen, tlen) ((klen) ((tlen)*KvConvertRatio))即 KV 行编码后的长度小于 Tuple 行tlen的一定比例时才启用 KV 编码——这正是“flag 选项优化空间占用”思想在实现中的体现不是简单的“稀疏就 KV”而是按实际编码长度对比决策取空间更优者。列格式bitmap 数据数组 offset 数组列格式用于列式存储路径。对于定长数据列本质上是一个数组但由于 NONE、NULL、有值三种状态并存列还需要一个bitmap标识每个行位置的状态对于变长数据如 binary、nchar、JSON除了数据数组外还有一个offset 数组索引每个值在数据区的起始位置单个值的长度由相邻两个 offset 之差得到。在 include/common/tdatablock.h 中可以验证这两种列结构定长列的空值判断基于 bitmap 宏colDataIsNull_f每 8 个行共享 1 字节见 L39-L44变长列则以varmeta.offset[row] -1作为空值标记L72-L73取值统一走colDataGetData宏按类型分流L80-L81。这与文档中“变长列 数据数组 offset 数组”的描述一一对应。vnodeTDengine 数据存储的基本单元vnode 存储架构vnode 是 TDengine 中数据存储、查询与备份的基本单元。每个 vnode 中存放着部分表的元数据信息以及属于这些表的全部时序数据表在 vnode 上的分布由一致性哈希决定因此每个 vnode 都可以被看作一个单机数据库。vnode 的存储由三部分组成WAL 文件的存储元数据的存储META底层是 TDB 引擎时序数据的存储TSDBLSM 结构。写入流程为vnode 收到写入请求 →预处理确保多副本间数据一致保障安全性与一致性→ 写入WAL保证持久性 → 写入vnode 内存池→ 内存池占用达到阈值后后台线程将数据刷新到硬盘META 与 TSDB→ 标记内存中对应的 WAL 编号为已落盘。由于 TSDB 采用 LSM 结构在打开数据库的“多表低频”参数时后台还会对 TSDB 数据文件执行合并减少文件数量并提高查询性能。从源码结构看这一链条对应 source/dnode/vnode/src/vnd/ 下的vnodeTxn.c、vnodeTxnWalMgr.c、vnodeBufPool.c内存池、vnodeCommit.c等文件WAL 读写由 source/libs/wal/src/ 中的walWrite.c、walRead.c、walTxn.c实现。vnode 还通过vnodeHash.c承载一致性哈希相关的表分布逻辑。元数据的存储TDB BTree 引擎vnode 中的元数据主要包括超级表名称、超级表 schema 定义、标签 schema 定义、子表名称、子表标签信息以及标签索引等。由于元数据读多写少TDengine 选择 BTree 作为存储结构——其查询性能高、插入删除稳定非常适合这类负载。元数据的写入过程是META 模块收到写入请求后生成多个 Key-Value 对存入底层的TDB 存储引擎。TDB 是 TDengine 自研的 BTree 引擎由三部分组成内置 Cache页缓存TDB 存储主文件TDB 日志文件。数据写入 TDB 时先写入 CacheCache 内存不足时系统会向 vnode 内存池申请额外内存。若写入涉及已有数据页的更改系统会在修改前先把未更改的数据页写入 TDB 日志文件作为备份——这样在断电等故障发生时可以通过日志回滚到原始数据保证数据更新的原子性与完整性。对应到源码TDB 引擎位于 source/libs/tdb/tdbBtree.c 实现 BTree 核心tdbPage.c 与 tdbPager.c 管理页与页分配tdbPCache.c 实现内置页缓存tdbTxn.c 负责事务与日志恢复语义。source/libs/tdb/test/下还有tdbPageFlushTest.cpp、tdbPageRecycleTest.cpp等针对页刷盘与回收的单元测试。多 BTree 与根索引页由于元数据查询需求多样化vnode 内部会创建多个 BTree存放不同维度的索引如按表 ID、按标签、按名称等这些树都存储在同一个共享存储文件中并通过一个根页编号为 1 的“索引 BTree”来索引各个 BTree 的根页编号对于变长 Key/ValueTDB 有两个关键工程手段溢出页overflow page当 Key 或 Value 超过文件页大小时超出部分存入溢出页非溢出页中 Key/Value 长度受限以此控制 BTree 的扇出度不低于 4从而压低树的高度、控制查询路径长度。source/libs/tdb/test/tdbExOVFLTest.cpp即为针对 Key/Value 溢出场景的测试用例tdbBtreeToStackTest.cpp验证 BTree 与栈式遍历的一致性。时序数据的存储LSM 与 MemTable若用传统 BTree 存储海量时序数据随着数据增长树高会迅速上升查询与写入性能急剧下降直至引擎不可用。因此 TDengine 用LSM 存储结构处理时序数据写入以日志方式顺序追加写性能优异后台通过合并减少空间占用、提升查询效率。MemTable红黑树 SkipList 组合索引内存中的 MemTable 采用Red-Black Tree 与 SkipList 相结合的索引方式不同表之间的索引存在 Red-Black Tree 中自平衡二叉树通过着色与旋转保持平衡查询/插入/删除均为 O(log n)同一张表内部的数据索引存在 SkipList 中基于多级索引的有序链表操作同样 O(log n)能快速定位特定时间范围的数据。这种“表级用平衡树、表内时间序列用跳表”的组合恰好匹配时序数据“多表并存、单表内时间连续”的访问模式先 O(log n) 找到表再在表内按时间区间快速扫描。从源码结构看MemTable 的实现位于 source/dnode/vnode/src/tsdb/tsdbMemTable.c其中tsdbTbDataIterCreate/Open/Next等函数围绕“按 (ts, version) 行键顺序遍历表内数据”的迭代器展开tsdbMemTable.c。按 (ts, version) 排序与文件组切分TSDB 中无论是在内存中还是数据文件里数据都按(ts, version)元组排序——version 用于多副本场景下以版本号解决写冲突与乱序提交。为组织这些按时间排序的数据TSDB 将数据文件按时间范围切分为多个数据文件组FSet每个文件组覆盖一段时间范围保证数据的连续性与完整性也便于分片分区。查询时根据查询条件的时间范围可以直接计算出文件组编号快速定位目标文件组避免扫描无关文件——这是时序数据库相比通用数据库在点查/范围查上天然占优的关键设计之一。数据文件组的五种文件每个文件组内包含五类文件各司其职1. head 文件BRIN 索引head 文件是 data 文件的BRINBlock Range Index块范围索引记录每个数据块的时间范围、偏移量offset与长度。查询引擎据此高效过滤出时间范围相交的数据块并直接取得其位置信息。head 文件内部由多个 BRIN 记录块及其索引构成记录块采用列存压缩方式存储大幅减少空间占用同时保持高查询性能2. data 文件数据本体data 文件存储真实时序数据数据以数据块为单位组织每个块内含一定量数据的列式存储。不同数据类型与压缩配置对应不同的压缩算法以压缩存储空间、提升传输效率。与 stt 文件不同data 文件中每个数据块独立存储代表一张表在特定时间范围内的数据——块级独立使得按表时间范围的管理与查询更灵活也是 LSM 后台合并的基本操作单元。3. sma 文件预计算sma 文件存储各数据块中每列的预计算结果包括 sum、min、max 等统计信息即 TDengine 的超级表预聚合。执行聚合类查询时查询引擎可直接利用这些预计算结果避免读取原始数据显著降低聚合查询的 I/O 与计算开销。vnode 侧对应的提交与聚合逻辑见source/dnode/vnode/src/sma/smaCommit.c、smaRollup.c等。4. tomb 文件删除标记tomb 文件存储删除记录记录结构为(suid, uid, start_timestamp, end_timestamp, version)元组超级表 ID、子表 ID、删除时间区间与版本号。LSM 中删除并不物理抹除数据而是以标记叠加的方式表达查询时据此过滤后台合并时才能真正丢弃。5. stt 文件碎片暂存与合并stt 文件服务于“落盘时产生的碎片数据”在不同场景下有不同策略少表高频场景系统只维护一个stt 文件专门存放每次数据落盘后剩余的碎片。下一次落盘时碎片与内存中的新数据合并成更大的数据块再一并写入 data 文件——避免数据文件碎片化保证连续性与高效性多表低频场景建议配置多个stt 文件。此时单张表每次落盘的数据量不大但同一超级表下所有子表累积的数据量可观通过跨表合并生成大块数据减少碎片提升写入效率与查询性能。TSDB 侧的文件读写与合并实现集中在 source/dnode/vnode/src/tsdb/ 目录tsdbFSetRW.c/tsdbFSetRAW.c负责文件组读写tsdbSttFileRW.c专门处理 stt 文件tsdbMerge.c/tsdbMergeTree.c实现后台合并tsdbSnapshot.c/tsdbSnapshotMEDIUM.c支持不同规模快照tsdbRetention.c处理数据保留策略tsdbRepair.c提供文件修复能力并有 source/dnode/vnode/test/ 下tsdbRepairTest.cpp、tsdbSmaTest.cpp、tsdbCacheTest.cpp等配套测试。总结三层结构如何共同支撑高性能TDengine 存储引擎的设计可以归纳为一条主线让每一层结构都精准匹配时序数据的访问特征。层次结构选择匹配的场景特征仓库实现位置行列格式Tuple / Key-Value 双行编码 定长/变长双列编码NONE/NULL/有值三态、稀疏与稠密混合include/common/trow.h、include/common/tdatablock.h元数据共享文件上的多棵 BTreeTDB页级日志保证原子性读多写少、查询维度多样source/libs/tdb/时序数据LSM MemTable(RBTSkipList) 按时间切分的文件组(head/data/sma/tomb/stt)海量顺序写入、按时间范围查询、跨表合并source/dnode/vnode/src/tsdb/WAL 先落盘保证持久性、内存池阈值触发后台刷盘保证吞吐、(ts, version) 全局有序保证多副本一致性、BRIN 预计算保证查询只读必要的数据块、stt 文件按场景合并碎片保证块密度——这些机制环环相扣构成了 TDengine 在工业物联网等 IIoT 场景下高写入、低延迟查询的底层基础。【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考