从一到无穷大 #16 ByteSeries,思考内存时序数据库的必要性

在这里插入图片描述本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。

引言

在[3]中我基于Gorilla讨论了时序数据库设置cache的可行性,最后得出结论:RAM去换实时查询的高效,显著增加成本的同时增加系统复杂性。

但是如果换个思路,缓存即为内存时序数据库,长久存储为为其他(分布式文件系统,对象存储,基于磁盘的时序数据库),这个选择又变得合理起来,因为不少研究对于使用内存加速数据写入和实时查询做了广泛的研究[4][5][6],这意味着内存时序数据库相比基于磁盘的时序数据库(写入过程可能存在多次IO操作,查询会触发多次IO操作)有着得天独厚的优势。

此时的权衡点回到了上篇文章中讨论的重点,是否存在Gorilla中提到的“26H”呢,如果线上负载存在的话此类假设,cache的存在是一个杀器,不但加速写入(批量写入后端磁盘存储),实时查询的速率也极其优秀(可以达到10ms级别)。

另外如果使用基于磁盘的时序数据库作为远端存储,对于历史数据的查询也会很快。但是缺陷就是宕机恢复流程会相对复杂,所以如果存在一个副本和主一样应用状态机问题就不是很大,缺陷是如果多副本宕机启动会非常慢。但是问题在于基于磁盘的时序数据库一般对于内存要求较高,无论是compaction,shard的forward index/inverted index的缓存,TSM的缓存,查询期间的内存消耗都不小,这导致如果要在基于磁盘的时序数据库内存中做cache的话要么提升机器内存规格(问题是请求进入引擎以及apply了raft log,在引擎内部做cache意味着需要在raft log外再维护一个log,用于宕机时内存数据恢复,好处是实时写入和实时查询性能提升),要么再加一层(类似于ByteSeries,Gorilla)。

结论:线上负载要求写入和实时查询高性能,时序数据库+缓存(内存时序数据库)可行。

ByteSeries的文章只是阐述了内存存储引擎,对分布式实现(数据如何分片,分布式查询聚合如何实现,难道真的只有一个节点?)和如何沉降冷数据没有提及。

内存布局

Active Segment

从一到无穷大 #16 ByteSeries,思考内存时序数据库的必要性_第1张图片

数据首先写入Active Segment,且这个关键路径不做复杂的数据转换和压缩,写入根据SeriesKey的哈希值决定写入哪个Segment ShardSegment Shard由一个追加写入的数组和动态的倒排索引组成。

分片的原因有两个:

  1. 并行数据写入
  2. 将数据从Active Segment转入Static Segment需要暂停写入,并重新分配新的segment,在分片的情况下转换可以是segment级别的,不需要执行整个缓冲区级别的内存分配。

Static Segment

线上100亿时间线的数据在元数据上可以看做10万个tag以及每个tag下数万个tag值。

Static Segment分为两个部分,Static SegmentCompressed Segment,SS数据累计到一定量时转换为CS,这里的目的主要是为了分批进行压缩,均摊昂贵的压缩开销。

从一到无穷大 #16 ByteSeries,思考内存时序数据库的必要性_第2张图片

新添加的数据直接添加到数组中,倒排索引中存储数组的偏移量,然后多个Temporary Static Segment合并成一个Static Segment,显然此时属于一个series key的数据还分布在数组的各处,并没有存储在连续空间内,也会导致索引中存储大量的下标值,而且数据点本身也没有办法压缩。

所以Static Segment会进一步转换为Compressed Segment,CS中属于一个Series key的数据都属连续存储的,这样可以减小数据的大小,索引中也只需要存储一个偏移。注意,这里其实存储结构其实是:

  1. 使用tire存储series key,指向compressed list的下标,compressed list内部指向此series key的Compressed data string
  2. Compressed data string指向压缩的数据点

文章最精髓的地方其实在于倒排索引的压缩:
从一到无穷大 #16 ByteSeries,思考内存时序数据库的必要性_第3张图片

构建倒排的步骤如下:

  1. 对所有的tagkv构建倒排索颖。即tagkv -> series keys 的偏移
  2. tagkey:使用前缀树压缩,这个思路没什么问题,不但可以极致的压缩tagkey,还允许范围查询。cedar[1]实现,个人认为redis的Redix tree也很适合这里的存储。
  3. tagvalue:把所有的series key list按顺序写入total inverted index array数组,这其中可以存在重复,因为不同的tagkey可以指向同样的serieskey,inverted index offset存储每个tagkv在total inverted index array数组的起始值,这意味这可以迅速找到tagkv对应的serieskey list。使用p4nzenc64来压缩这两个数组

其实可以看到,本质上和基于磁盘的invert index+TSM没有区别,无非去除了forward index,改为数组的下标。这带来的劣势时必须看作这个Compressed Segment已经封闭,且无法修改,不然整个结构全部都要变化。

Data Conversion Scheduler(DCS)

有三个上面提到的功能:

  1. AS转换为TSS
  2. 多个TSS与SS合并
  3. SS转换转换为CSS

值得关注的是这里需要考虑AS的写入吞吐和AS转换为TSS的速率是否匹配,否则可能导致压缩为TSS成为瓶颈,进而导致写入受限。

具体细节没什么好说的,DCS运行在一个单独的线程,执行一系列提前预设好的规则执行调度,其实就是基于维度,大小等条件判断。

评估

文章中把ByteSeries与 tsdc, Prometheus and Gorilla三个数据库从三个维度做了评估,分别为:

  1. 每个维度使用的字节数
  2. 写入速率
  3. single group bydouble group by查询速率

可以预想的,在高维度的场景下ByteSeries原数据所使用的内存非常少,这也很好理解,因为tagkey本身被极致压缩,元数据还只需要指向一个index。写入速率受转换的原因不如Gorilla。

总结

很优秀的内存压缩元数据思路,事实上可以认为这种方法是解决维度爆炸场景下写入和实时查询性能的一种可行方式。

引用:

  1. cedar - C++ implementation of efficiently-updatable double-array trie
  2. TurboPFor-Integer-Compression
  3. 从一到无穷大 #15 Gorilla,论黄金26H与时序数据库缓存系统的可行性
  4. Btrdb: Optimizing storage system design for timeseries processing FAST 2016
  5. Akumuli Numeric B+tree
  6. RTSI: An Index Structure for Multi-Modal Real-Time Search on Live Audio Streaming Services ICDE 2018

你可能感兴趣的:(从一到无穷大,时序数据库,数据库)