数据库的性能往往很大程度上由存储引擎决定。如何针对不同的应用场景设计高性能的存储引擎一直是数据库开发的难题。本文为大家介绍 DolphinDB 在2.0版本中新推出的存储引擎 TSDB。
时序数据分析可以帮助决策者了解到生产和业务中的细微变化,从而对资源整合、跟踪、预测、商业智能等进行优化。
随着互联网和通信技术的发展,网络中产生的时间序列数据量有了爆炸式的增长。面对万亿级海量数据,现代业务对数据价值挖掘的需求也越来越趋向精细、复杂的计算分析。
一款强大的存储引擎,是一切数据查询、计算、分析、生产活动的基础和起点,是高性能处理大规模数据的基本保障,也是一个数据库最核心、最复杂的组件之一。究竟该怎么设计好一个存储引擎,至今仍是一个没有标准答案的开放性问题。
DolphinDB 时序数据库在 2.0 版本中正式推出了基于 LSM 树自研的新存储引擎 TSDB,实现了更高效的查询、排序和去重,充分扩展了数据存储的功能,在部分场景下极大提升了数据库性能。
TSDB 还是 OLAP ?
DolphinDB 在2.0版本之前只有唯一的 OLAP 存储引擎。这种引擎下数据表中每个分区的每一列存为一个文件。数据在表中的存储顺序与数据写入的顺序一致,数据写入有非常高的效率。但是 OLAP 引擎也存在以下一些局限:
- 分区内不支持索引,即使只是检索几行数据,也必须加载整个分区(的相关列);
- 写入过程设计简单,不支持去重;
- 不适合几百几千列的宽表存储;
- 即使修改一条记录,也必须重写整个分区;
针对这些局限,DolphinDB 基于 LSM 树自研出了 TSDB 存储引擎,并在2.0版本中正式发布。
TSDB 存储引擎很好地克服了上述局限,功能更加全面。每一个分区的数据写在一个或多个 level file 中。每一个 level file 内部的数据按照指定的列进行排序且创建块索引。相比之下,TSDB 引擎在以下几个方面有显著优势:
- 过滤条件使用分区列以及分区内排序字段的点查询非常高效;
- 可以在写入时对数据进行排序和去重;
- 适合存储几百几千列的宽表,也适合存储 array vector 和 BLOB 类型的数据;
- 若指定去重时保留最后一条记录(设置 keepDuplicates=LAST),则修改数据时重写数据所在 level file 即可,不需要重写整个分区。
但与 OLAP 引擎相比,TSDB 引擎的不足之处在于:
- 写入吞吐量低。TSDB 引擎中,数据需在 cache engine 中进行排序;level file 会进行合并与压缩。
- 读取整个分区数据或整个分区的某几列数据时,效率低于 OLAP。
查询场景示例
下面通过一个例子来解释 OLAP 与 TSDB 引擎所适合的查询场景。
假设数据表 t 按照股票代码与交易日进行组合分区,以下是该数据表中的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 脚本在数据库初始化的时候需要指定存储引擎为 "TSDB"(engine="TSDB"),并可以在创建数据表时指定对列进行排序,而 OLAP 引擎则不具备这一特性(sortColumns=`StockID`Timestamp)。
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 引擎所创建的数据库中,每个分区内部无索引,一个查询语句从数据库中读取数据的最小单位是一个分区的一列。即使要查找一行数据,也必须读取该行数据所在分区的所有行,耗时一般在100毫秒以上。
而TSDB 引擎则在每个分区内部设置了索引,查找少量数据时无需读取分区内所有行,耗时可以低至几毫秒。TSDB 引擎在海量数据查询与分析方面的性能仅仅略慢于 OLAP 引擎。因此,若对查询少量数据有极致性能要求,推荐使用 TSDB 引擎。
TSDB 原理简介
本节将结合上一节中的例子为大家简要介绍 TSDB 存储引擎的原理。
redo log
redo log 类似于 Write-Ahead Log。数据加载到内存之后,会先持久化到 redo log 中。即便数据库在写入时宕机了,重启时,系统仍能够从 redo log 中恢复数据。
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 的空间。
Sort Columns
类似上例中 StockID 与 Timestamp 这样用于将数据进行排序的列,在 TSDB 中被称为 sort columns。sort columns 的最后一列必须为时间类型。sortColumns 除了最后一列的其他列通常为在点查中过滤条件会用到的列,其唯一值组合,称为 sort key。使用 TSDB 引擎,查询时可利用 sort key 直接定位到过滤条件所指定的数据块,然后仅读取这些数据块的数据,点查性能可大幅提升。
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 数量过多导致性能降低。
TSDB 使用技巧
接下来为大家简单介绍 TSDB 的一些配置项和使用技巧。
如何处理重复数据
一个分区内 sortColumns 重复的数据应当如何处理,由建表时 createPartitionedTable 函数的 keepDuplicates 参数指定。在上面这个例子中,即股票代码与时间列都一致的数据。默认的值为 ALL,即保留所有数据。若设置为 FIRST,则仅会保留重复数据中的第一条;若设置为 LAST,则仅会保留重复数据中的最新一条。
控制 sort key 数量
为保证性能最优,建议每个分区内 sort key 最好不超过1000个。若 sort key 过多,每个 sort key 对应的数据量少,造成部分数据块内的数据量可能不足 TSDBMaxBlockSize,数据块数量也很多。当用户查询数据时,由于读取大量数据块,造成读文件耗时增加。此时,用户可通过建表函数指定 sortKeyMappingFunction,对 sort key 降维。请注意,降维操作一定程度上将影响写入性能,建议用户优先合理规划 sortColumns。
设置 cache engine 容量
使用配置参数 TSDBCacheEngineSize 设定 cache engine 容量(以 GB 为单位)。默认值为 1GB,建议设置 1GB 或以上。
设置 TSDB 数据块大小
使用配置参数 TSDBMaxBlockSize 设置存储时拆分的数据块(block)在压缩前的大小(以 bytes 为单位),默认值为16,384。这个参数越大,则数据的压缩率越高,但点查的性能也会有所下降。
触发 level file 合并
level file 的数量越多,则查询的效率会越低。对不再有数据写入的分区,可使用 triggerTSDBCompaction 命令以手动触发 level 0 file 的合并,既可以提升查询性能,亦可提升压缩率