influxdb 倒排索引介绍

背景 - 时序数据库为什么需要倒排索引?

时序数据库对监控的时间线存在多维度查询,以及聚合查询的需求。

打比方:

我们监控所有服务器的cpu信息,我们会存在几种需求:

  • 指定服务器的机房来获取对应服务器的cpu利用率。
  • 指定服务器的机型来获取对应服务器的cpu利用率。

这种不同维度的查询需求在基本上所有的时序数据库中都会通过倒排索引来解决。

Influxdb自己实现了一套倒排索引,在1.5之前的版本实现比较粗糙,在1.5版本官方文档正式release了tsi1的倒排索引实现,而且出了一份"吹牛逼"的报告(主要是1.5针对1.3的优化结果)。

本文分别对1.5版本的tsi1倒排实现和1.3版本的inmem倒排实现做些介绍。水平有限,一通"胡侃",欢迎指正。

名词解释:

mesurement: 等同于关系型数据库中的 table
tag_key: 数据源的标签key
tag_value: 数据源的标签value, 表明这个数据源的一些属性。
series(seriesKey): 也可称之为数据源/时间线,在InfluxDB series由measurement name + tag_key + tag_value 组成。

inmem inverted Index实现

inmem既是将倒排索引数据全部放在内存中,带来问题显而易见:

  • 随着监控时间线的不断增加,既我们监控物件规模不断增长,内存爆炸。
  • 当server启动时需要从所有的tsm数据文件中读取索引信息并load到内存中,这样导致启动时间非常长。

inmem inverted index关键数据结构:

{measurement -> tagKey -> tagValue -> [seriesIDs]} {seriesID -> seriesString}

源码中结构比较复杂,这里省略。实际上inverted index就是按照hash结构存储 series 到 SeriesIDs的映射关系。

tsi1 inverted Index实现

tsi1是Influxdb 1.5版本对inmem的重大改进,并且出了一份测试报告做了和release 1.3.x的性能对比,所以我理解1.5版本是tsi1的正式release版本。

tsi1为了解决上述inmem的两个问题,又要保证倒排索引的吞吐满足性能需求,总结设计思路主要如下:

数据落盘,只有部分热数据缓存在内存中,依旧使用lsm tree思想实现快速写入需求
使用Hash数据结构,满足快速查询的需求,设计上不考虑 <,>等范围查询场景。
将数据做partition,减少冲突粒度锁粒度,能够在单机层面上纵向扩展性能。
tsi1中将inmem中的两份数据结构划分到两个模块中:

  1. {measurement -> tagKey -> tagValue -> [seriesIDs]} => tsi1 index
  2. {seriesID -> seriesKey} => SeriesFile
    SeriesFile
    首先介绍下SeriesFile模块,作用是记录{seriesID -> seriesKey},SeriesFIle设计:

将原先inmem index中的{seriesID -> seriesKey} 的结构落地到磁盘中,为了达到快速获取Series的要求,SeriesFile核心的数据结构hashtable。
SeriesFile跨Shard,所有Shard共享一个Series file
SeriesFile partition设计,提升单Influxdb对Series的写入效率,减少写入冲突。
SeriesFile设计细节:

SeriesFile默认分为8个partition,每个partition 由两部分组成
index file
segment file
index file则是记录seriesID到offset的映射,offset是对应series在segment file中的位点。
segment file会记录所有seriesKey,具体数据结构可见下方图片

index file结构
index file由三部分组成,HDR, KeyIDMap, IdOffsetMap.

HDR主要记录了一些元信息,存储了index file中的KeyIDMap和IdOffsetMap在index file中的offset。

IdOffsetMap 记录着ID到segment file offset 的映射关系,这个映射关系在disk按照Robin Hood HashTable存储。

img

index 文件中记录的offset为8byte, 如何映射到segment file ?

SegmentOffset 会将8 byte切分成3部分:

| 2 byte | 2 byte | 4 byte |

第一部分目前没有意义的。 第二部分映射到segment 的id, 这样计算segment最大数量为2的16次方。 第三部分映射到segment 文件中的offset。

传入Series ID,读取seriesKey流程:
  • get IDOffsetMap offset in Index file from HDR block .

  • get SegmentOffset from IDOffsetMap block.

  • get seriesKey from Segment file by SegmentOffset. 

seriesKey写入流程:
  • 首先新的seriesKey数据新增时,会将seriesKey写入到segment file中,但是不会马上写入到index file中,index file的记录存放在内存中,既index file实际上在内存中和磁盘中各有部分数据。如果server重启,直接通过重新load segment file,当前的index file会记录上次落盘时对应的segment file position, 只需要将新增segment file的index 记录load到内存中即可。
  • index file作为一个hashtable, 随着规模的增长,hashtable必然要面临着rehash的动作。那么这里玩法就是compact,当内存中的index达到一定阈值后,将内存中的记录+原有的index file的hashtable聚合到一个新的文件中。
segment file结构

segment files 存储了seriesKey,seriesKey的写入分为两种: insert和delete. 上层模块可能调用插入seriesKey和delete seriesKey接口,所有的记录顺序写入到segment files中。

这里设计上segment files是不会做compact的,可能在设计上认为时序场景基本不会出现delete series场景。

img

tsi1 index设计

总体上是将inmem中的 {measurement -> tagKey -> tagValue -> [seriesIDs]} 落盘,虽然落盘后用了一些技巧能够快速定位到seriesIDs记录,但是整体的数据结构和inmem一样,使用hashtable,能够O(1)的复杂度读取到记录。

tsi1总体设计:

  • tsi存储引擎设计类似LSM Tree

  • 每个Shard有独立的tsi1存储,这样的设计可以保证老的Shard中存在一些时间线能够随着Shard的过期自动过期删除。同时这样会带来Shard rollOver时会一些性能损耗。

  • 和SeriesFile一样,每个Shard的tsi分为多个partition写入,提升单Influxdb对tsi的写入效率,减少写入冲突,增加吞吐量和减少延时。

  • 和其他的所有tsm, seriesFile等模块类似,在工程实现上通过mmap 系统调用将磁盘文件映射到内存地址上,让os来帮助我们做LRU(Least Recently Used),热点数据cache在内存中。

三个主要模块:

Log file, Index File, Manifest File.

Log file模块

和所有的LSM 引擎类似,Log File模块实际在运行中生效的是Log File对应在内存中的Cache

cache结构
  • 内存中数据结构类似于inmem inverted index结构,

  • [SeriesIDs] 在内存中按照BitMap结构存储,节约内存消耗。

  • cache 中记录 add series, 同样也记录 delete series。并且delete series记录会随着dump到磁盘中持久化。

  • 同tsm存储时序数据一样,当上层调用Log File接口来获取 series时,会同时从cache和Index File读取合并,返回给上层。

Log File结构

Log File 记录着最近的series增加删除记录,数据写入Cache同时顺序写入到日志文件中。文件后缀为 .tsl 文件格式如下图:

  • LogEntry有多种type,根据Flag区分开,如最多的新增series flag, 在做redo行为时主要读取SeriesID和measurementName。
  • LogEntry中的SeriesID结构在存储时使用了简单的压缩编码,SeriesID原本是8 byte的long整型数据,实际在存储中使用了varint编码,可以有效减少空间使用。 虽然varint做了编码,而且字节不对齐对cpu不友好,但是在Influxdb场景下容量看起来更加受重视。

img

Index File

Index File可对照LSM Tree中的SSTable.

Index File的载体是一个文件,文件分为几个部分:trailer, measurement block, tag block

trailer主要记录measurement block, tag block的offset和size, 做一个路由作用。

measurement block

measurement block 作用是快速上层指定的measurement name 路由到tag block中,measurement block结构是一个hashtable, 保证O(1)复杂度。

tag block

tag block 主要存储了tag_key -> tag_value -> [seriesIDs]的映射关系,上层模块可以传入tag_key + tag_value, 然后可以快速获取到seriesIDs。
下图是通过measurement + tag_key + tag_value 获取对应的时间线(seriesIDs)的流程,图中仅仅展示了相关的部分的数据结构。 由于我们的数据复杂读很高,尽管我们使用了hashtable的数据结构,但是一次查询的IO次数还是比较多的,当然如果我们大多数情况下获取的都是热数据,大多数的IO请求都能够在cache层面命中。

img

Manifest File
Manifest File中记录当前所有index File 和 Log File。
Manifest File主要作用:

  • 按照时间顺序记录所有的Index File, 先读取新的index file,保证如果出现 series 写入然后删除的行为下,上层接口首先读取到的是新的index file, 那么会首先读到删除的结果。
  • 保证在下次compact时顺序,不会删除应该存在的数据。

tsi测试报告
[官方测试报告](http://get.influxdata.com/rs/972-GDU-533/images/Index and TSM overview in InfluxDB 1.5.pdf?spm=ata.21736010.0.0.5d8e758cfKJVL1&file=Index and TSM overview in InfluxDB 1.5.pdf)

总结

  • tsi1的实现其实非常复杂,本文并没有覆盖如tsi1中为了兼容 series删除场景,series数量的估计等细节,如有兴趣可以自行读源码阅读。
  • 需使用最新的release分支。tsi 存储引擎在1.5版本上还是不够成熟,本人在简单使用tsi inverted index运行influxdb过程中发现多个问题,当然influxdb社区的活跃度是毋庸置疑的,而且有商业化公司在支撑Influxdb发展,质量上会越来越好。
posted @ 2022-02-07 10:44  梧桐花落  阅读(1216)  评论(0编辑  收藏  举报