TSDB 存储引擎简介

本文简介 DolphinDB 在 2.0 版本中新推出的 TSDB 存储引擎、其适用场景以及与 OLAP 存储引擎的特点差异。

1. OLAP 与 TSDB 适用的场景

OLAP 是 DolphinDB 在 2.0 版本之前的唯一存储引擎。数据表中每个分区的每一列存为一个文件。数据在表中的存储顺序与数据写入的顺序一致,数据写入有非常高的效率。

OLAP 引擎的主要局限:

  1. 分区内不支持索引,即使只是检索几行数据,也必须加载整个分区(的相关列)。
  2. 写入过程设计简单,不支持去重;
  3. 不适合几百几千列的宽表存储。
  4. 即使修改一条记录,也必须重写整个分区。

TSDB 是 DolphinDB 基于 LSM 树自研的新的存储引擎。它很好地克服了 OLAP 引擎的局限性,功能更加全面。每一个分区的数据写在一个或多个 level file 中。每一个 level file 内部的数据按照指定的列进行排序且创建块索引。

TSDB 引擎具有以下优点:

  1. 过滤条件使用分区列以及分区内排序字段的点查询非常高效。
  2. 可以在写入时对数据进行排序和去重。
  3. 适合存储几百几千列的宽表,也适合存储 array vector 和 BLOB 类型的数据。
  4. 若指定去重时保留最后一条记录(设置 keepDuplicates=LAST),则修改数据时重写数据所在 level file 即可,不需要重写整个分区。

与 OLAP 引擎相比,TSDB 引擎的不足之处在于:

  1. 写入吞吐量低。TSDB 引擎中,数据需在 cache engine 中进行排序;level file 会进行合并与压缩。
  2. 读取整个分区数据或整个分区的某几列数据时,效率低于 OLAP。

下面通过一个例子来解释 OLAP 与 TSDB 引擎适合的 query。假设数据表 t 按照股票代码与交易日进行 COMPO 分区。以下是该数据表中的 4 行数据:

StockIDTimestampBid
AAPL2021.08.05T09:30:001.5
MSFT2021.08.05T09:30:001.3
AAPL2021.08.05T09:31:001.6
GOOG2021.08.05T09:31:001.4
..................

OLAP 存储引擎的建库建表脚本如下:

dbTime = database("", VALUE, 2021.08.01..2021.09.01)
dbStockID = database("", HASH, [SYMBOL, 100])

db = database(directory="dfs://stock",partitionType=COMPO,partitionScheme=[dbTime,dbStockID],engine="OLAP")

schema = table(1:0, `Timestamp`StockID`bid, [TIMESTAMP, SYMBOL, DOUBLE])
stocks = db.createPartitionedTable(table=schema, tableName=`stocks, partitionColumns=`Timestamp`StockID)

TSDB 存储引擎的建库建表脚本如下:

dbTime = database("", VALUE, 2021.08.01..2021.09.01)
dbStockID = database("", HASH, [SYMBOL, 100])

db = database(directory="dfs://stock",partitionType=COMPO,partitionScheme=[dbTime,dbStockID],engine="TSDB")

schema = table(1:0, `Timestamp`StockID`bid, [TIMESTAMP, SYMBOL, DOUBLE])
stocks = db.createPartitionedTable(table=schema, tableName=`stocks, partitionColumns=`Timestamp`StockID, sortColumns=`StockID`Timestamp)

和 OLAP 的脚本相比,TSDB 的脚本在 database 初始化的时候指定存储引擎为 "TSDB",并在创建表格的时候指定 sortColumnsStockIDTimestamp

OLAP 引擎适合执行以下的 query:

select avg(Bid) from t where date=2021.08.05 group by StockID

TSDB 引擎适合执行以下的 query:

select * from table where StockID='AAPL', Timestamp > 2021.08.05T09:30:00, Timestamp < 2021.08.05T09:35:00

使用 OLAP 引擎所创建的数据库中,每个分区内部无索引,一个 query 从数据库中读取数据的最小单位是一个分区的一列。即使要查找一行数据,也必须读取该行数据所在分区的所有行,耗时一般在 100 毫秒以上。TSDB 引擎则在每个分区内部设置了索引,查找少量数据时无需读取分区内所有行,耗时可以低至几毫秒。TSDB 引擎在海量数据查询与分析方面的性能仅仅略慢于 OLAP 引擎。因此,若对查询少量数据有极致性能要求,推荐使用 TSDB 引擎。

2. TSDB 原理简介

本节叙述中,为简便起见,我们将结合上一节中的例子。

2.1. redo log

redo log 类似于 Write-Ahead Log。数据加载到内存之后,会先持久化到 redo log 中。即便数据库在写入时宕机了,重启时,系统仍能够从 redo log 中恢复数据。

2.2. Cache Engine

确认数据写入 WAL 之后,数据会写入内存中的 cache engine。

新写入 cache engine 的数据是直接追加(append)的,且未经排序的(unsorted write buffer)。待缓存的数据量达到一个阈值之后,对其按照 StockID 排序(sorted write buffer)。sorted write buffer 是只读的,可在内存中开启压缩,以在内存中存储更多的数据,降低查询数据的时延。待产生多个 sorted write buffer,其总数据量达到一个阈值(由配置参数 TSDBCacheEngineSize 设定)之后,TSDB 会将所有 sorted write buffer 中的数据按照 Timestamp 排序后写入磁盘的数据文件(称为 level file)。

TSDB cache engine 的这种从 unsorted write buffer 到 sorted write buffer 两阶段的设计,与大多数的基于 LSMT 系统的 cache engine(或称 MemTable)有所不同。这种独特的设计是为了平衡系统的读性能与写性能。

请注意,在数据从 cache engine 写入磁盘的过程中,如果又有数据写入 cache engine,则 cache engine 会分配新的空间来存储新写入 cache engine 的数据。因此,在极端情况下,TSDB 的 cache engine 最多会占用两倍的 TSDBCacheEngineSize 的空间。

2.3. Sort Columns

类似上例中 StockID 与 Timestamp 这样用于将数据进行排序的列,在 TSDB 中被称为 sort columns。sort columns 的最后一列必须为时间类型。sortColumns 除了最后一列的其他列通常为在点查中过滤条件会用到的列,其唯一值组合,称为 sort key。使用 TSDB 引擎,查询时可利用 sort key 直接定位到过滤条件所指定的数据块,然后仅读取这些数据块的数据,点查性能可大幅提升。

2.4. Level File

每个 level file 中的数据均按照 StockID 与 Timestamp 排序。每个 StockID 的记录会存储为多个一定大小(16KB)的数据块,并将每个数据块在该文件中存储的起始位置记录下来。后续写入的数据会写入另一个 level file,依此类推。

level file 最多可有 4 个 level。Level 0 的文件由 cache engine 写入到磁盘而产生,而更高层次的 level file 是由更低层次的 level file 合并生成的。每个 level 0 文件最大为 32MB。每次从 cache engine 写入同一分区的数据量若大于 32MB,则产生多个 level 0 文件。从 level 0 到 level 1 的过程如下:当一个分区内的 level 0 文件数量超过 10 个或 level 0 总数据量大于某个阙值(256MB)时,所有 level 0 文件会被合并压缩为一个 level 1 的文件,以此类推。合并文件时会将数据按照 StockID 与 Timestamp 进行排序。通过不断进行数据文件的合并压缩,可以有效控制 level file 的数量,以免 level file 数量过多导致性能降低。

3. TSDB 使用技巧

本文在这一部分简单介绍 TSDB 的一些配置项和使用技巧。

3.1. 如何处理重复数据

一个分区内 sortColumns 重复的数据应当如何处理,由建表时 createPartitionedTable 函数的 keepDuplicates 参数指定。在上面这个例子中,即股票代码与时间列都一致的数据。默认的值为 ALL,即保留所有数据。若设置为 FIRST,则仅会保留重复数据中的第一条;若设置为 LAST,则仅会保留重复数据中的最新一条。

3.2. 控制 sort key 数量

为保证性能最优,建议每个分区内 sort key 最好不超过 1000 个。若 sort key 过多,每个 sort key 对应的数据量少,造成部分数据块内的数据量可能不足 TSDBMaxBlockSize,数据块数量也很多。当用户查询数据时,由于读取大量数据块,造成读文件耗时增加。此时,用户可通过建表函数指定 sortKeyMappingFunction,对 sort key 降维。请注意,降维操作一定程度上将影响写入性能,建议用户优先合理规划 sortColumns。

3.3. 设置 cache engine 容量

使用配置参数 TSDBCacheEngineSize 设定 cache engine 容量(以 GB 为单位)。默认值为 1GB,建议设置 1GB 或以上。

3.4. 触发 level file 合并

level file 的数量越多,则查询的效率会越低。对不再有数据写入的分区,可使用 triggerTSDBCompaction 命令以手动触发 level 0 file 的合并,既可以提升查询性能,亦可提升压缩率。