TDengine架构设计与存储结构

TDengine是一款轻量级、高效且单机开源的面向物联网的数据处理引擎,其核心是一个时序数据库(Time-Series Database)。作为一款专门为物联网设计并实现的数据引擎,TDengine在数据的写入、查询以及存储方面拥有其他数据库无法比拟的优势。本文主要探讨了TDengine在架构设计和存储方面的创新,以方便用户理解TDengine强大性能背后的逻辑。

TDengine架构设计

如图1所示,TDengine服务主要包含两大模块:管理节点模块(MGMT)数据节点模块(DNODE)。整个TDengine还包含客户端模块

TDengine Database
图 1 TDengine架构示意图

管理节点模块

管理节点模块主要负责元数据的存储和查询等工作,其中包括用户信息的管理、数据库和表信息的创建、删除以及查询等。应用连接TDengine时会首先连接到管理节点。在创建/删除数据库和表时,请求也会首先发送请求到管理节点模块。由管理节点模块首先创建/删除元数据信息,然后发送请求到数据节点模块进行分配/删除所需要的资源。在数据写入和查询时,应用同样会首先访问管理节点模块,获取元数据信息。然后根据元数据管理信息访问数据节点模块。

数据节点模块

写入数据的存储和查询工作是由数据节点模块负责。 为了更高效地利用资源,以及方便将来进行水平扩展,TDengine内部对数据节点进行了虚拟化,引入了虚拟节点(vnode)的概念,作为存储、资源分配以及数据备份(商业版本中)的单元。如图2所示,在一个dnode上,通过虚拟化,可以将该dnode视为多个虚拟节点的集合。每个虚拟节点存储一定数量的表中的数据。不同的vnode之间资源互不共享。每个虚拟节点都有自己的缓存,在硬盘上也有自己的存储目录。而同一vnode内部无论是缓存还是硬盘的存储都是共享的。通过虚拟化,TDengine可以将dnode上有限的物理资源合理地分配给不同的vnode,大大提高资源的利用率和并发度。一台物理机器上的虚拟节点个数可以根据其硬件资源进行配置。

TDengine Database
图 2 TDengine虚拟化

客户端模块

TDengine客户端模块主要负责将应用传来的请求(SQL语句)进行解析,转化为内部结构体再发送到服务端。TDengine的各种接口都是基于TDengine的客户端模块进行开发的。

TDengine写入流程

TDengine的完整写入流程如图3所示。为了保证写入数据的安全性和完整性,TDengine在写入数据时采用[预写日志算法]。客户端发来的数据在经过验证以后,首先会写入预写日志中,以保证TDengine能够在断电等因素导致的服务重启时从预写日志中恢复数据,避免数据的丢失。写入预写日志后,数据会被写到对应的vnode的缓存中。随后,服务端会发送确认信息给客户端表示写入成功。TDengine中存在两种机制可以促使缓存中的数据写入到硬盘上进行持久化存储:

TDengine Database
图 3 TDengine写入流程
  1. 时间驱动的落盘:TDengine服务会定时将vnode缓存中的数据写入到硬盘上,默认为一个小时落一次盘。落盘间隔可在配置文件中配置。
  2. 数据驱动的落盘:当vnode中缓存的数据达到一定规模时,为了不阻塞后续数据的写入,TDengine也会拉起落盘线程将缓存中的数据清空。数据驱动的落盘会刷新定时落盘的时间。

TDengine在数据落盘时会打开新的预写日志文件,在落盘后则会删除老的预写日志文件,避免日志文件无限制的增长。

元数据的存储

TDengine中的元数据信息包括TDengine中的数据库,表等信息。元数据信息默认存放在 /var/lib/taos/mgmt/ 文件夹下。

/var/lib/taos/
       +--mgmt/
           +--db.db
           +--meters.db
           +--user.db
           +--vgroups.db

元数据文件只进行追加操作,即便是元数据的删除,也只是在数据文件中追加一条删除的记录。

写入数据的存储

TDengine中写入的数据在硬盘上是按时间维度进行分片的。同一个vnode中的表在同一时间范围内的数据都存放在同一文件组中,如下图中的v0f1804*文件。这一数据分片方式可以大大简化数据在时间维度的查询,提高查询速度。在默认配置下,硬盘上的每个文件存放10天数据。用户可根据需要进行配置。

数据在文件中是按块存储的。每个数据块只包含一张表的数据,且数据是按照时间主键递增排列的。数据在数据块中按列存储,这样使得同类型的数据存放在一起,可以大大提高压缩的比例,节省存储空间。

TDengine的数据文件默认存放在 /var/lib/taos/data/ 下。而 /var/lib/taos/tsdb/ 文件夹下存放了vnode的信息、vnode中表的信息以及数据文件的链接。完整目录结构如下所示:

/var/lib/taos/
       +--tsdb/
       |   +--vnode0
       |        +--meterObj.v0
       |        +--db/
       |            +--v0f1804.head->/var/lib/taos/data/vnode0/v0f1804.head1
       |            +--v0f1804.data->/var/lib/taos/data/vnode0/v0f1804.data
       |            +--v0f1804.last->/var/lib/taos/data/vnode0/v0f1804.last1
       |            +--v0f1805.head->/var/lib/taos/data/vnode0/v0f1805.head1
       |            +--v0f1805.data->/var/lib/taos/data/vnode0/v0f1805.data
       |            +--v0f1805.last->/var/lib/taos/data/vnode0/v0f1805.last1
       |                   :
       +--data/
           +--vnode0/
                 +--v0f1804.head1
                 +--v0f1804.data
                 +--v0f1804.last1
                 +--v0f1805.head1
                 +--v0f1805.data
                 +--v0f1805.last1
                         :

meterObj文件

每个vnode中只存在一个meterObj文件。该文件中存储了vnode的基本信息(创建时间,配置信息,vnode的统计信息等)以及该vnode中表的信息。其结构如下所示:

<文件开始>
 [文件头]
 [表记录1偏移量和长度]
 [表记录2偏移量和长度]
 …
 [表记录N偏移量和长度]
 [表记录1]
 [表记录2]
 …
 [表记录N]
 [表记录]
 <文件结尾>

其中,文件头大小为512字节,主要存放vnode的基本信息。每条表记录代表属于该vnode中的一张表在硬盘上的表示。

head文件

head文件中存放了其对应的data文件中数据块的索引信息。该文件组织形式如下:

<文件开始>
 [文件头]
 [表1偏移量]
 [表2偏移量]
 …
 [表N偏移量]
 [表1数据索引]
 [表2数据索引]
 …
 [表N数据索引]
 <文件结尾>

文件开头的偏移量列表表示对应表的数据索引块的开始位置在文件中的偏移量。每张表的数据索引信息在head文件中都是连续存放的。这也使得TDengine在读取单表数据时,可以将该表所有的数据块索引一次性读入内存,大大提高读取速度。表的数据索引块组织如下:

[索引块信息]
 [数据块1索引]
 [数据块2索引]
 …
 [数据块N索引]

其中,索引块信息中记录了数据块的个数等描述信息。每个数据块索引对应一个在data文件或last文件中的一个单独的数据块。索引信息中记录了数据块存放的文件、数据块起始位置的偏移量、数据块中数据时间主键的范围等。索引块中的数据块索引是按照时间范围顺序排放的,这也就是说,索引块M对应的数据块中的数据时间范围都大于索引块M-1的。这种预先排序的存储方式使得在TDengine在进行按照时间戳进行查询时可以使用折半查找算法,大大提高查询速度。

data文件

data文件中存放了真实的数据块。该文件只进行追加操作。其文件组织形式如下:

<文件开始>
 [文件头]
 [数据块1]
 [数据块2]
 …
 [数据块N]
 <文件结尾>

每个数据块只属于vnode中的一张表,且数据块中的数据按照时间主键排列。数据块中的数据按列组织排放,使得同一类型的数据排放在一起,方便压缩和读取。每个数据块的组织形式如下所示:

[列1信息]
 [列2信息]
 …
 [列N信息]
 [列1数据]
 [列2数据]
 …
 [列N数据]

列信息中包含该列的类型,列的压缩算法,列数据在文件中的偏移量以及长度等。除此之外,列信息中也包含该内存块中该列数据的预计算结果,从而在过滤查询时根据预计算结果判定是否读取数据块,大大提高读取速度。

last文件

为了防止数据块的碎片化,提高查询速度和压缩率,TDengine引入了last文件。当要落盘的数据块中的数据条数低于某个阈值时,TDengine会先将该数据块写入到last文件中进行暂时存储。当有新的数据需要落盘时,last文件中的数据会被读取出来与新数据组成新的数据块写入到data文件中。last文件的组织形式与data文件类似。

小结

TDengine通过其创新的架构和存储结构设计,有效提高了计算机资源的使用率。一方面,TDengine的虚拟化使得TDengine的水平扩展及备份非常容易。另一方面,TDengine将表中数据按时间主键排序存储且其列式存储的组织形式都使TDengine在写入、查询以及压缩方面拥有非常大的优势。