InfluxDB 的存储机制解析
本文介绍了 InfluxDB 对于时序数据的存储 / 索引的设计. 由于 InfluxDB 的集群版已在 0.12 版就不再开源, 因此如无特殊说明, 本文的介绍对象都是指 InfluxDB 单机版
1. InfluxDB 的存储引擎演进
尽管 InfluxDB 自发布以来历时三年多, 其存储引擎的技术架构已经做过几次重大的改动, 以下将简要介绍一下 InfluxDB 的存储引擎演进的过程.
1.1 演进简史
版本 0.9.0 之前
** 基于 LevelDB 的 LSMTree 方案 **
版本 0.9.0~0.9.4
** 基于 BoltDB 的 mmap COW B+tree 方案 **
版本 0.9.5~1.2
** 基于自研的 WAL + TSMFile 方案 **(TSMFile 方案是 0.9.6 版本正式启用, 0.9.5 只是提供了原型)
版本 1.3~至今
** 基于自研的 WAL + TSMFile + TSIFile 方案 **
1.2 演进的考量
InfluxDB 的存储引擎先后尝试过包括 LevelDB, BoltDB 在内的多种方案. 但是对于 InfluxDB 的下述诉求终不能完美地支持:
时序数据在降采样后会存在大批量的数据删除
=> *LevelDB 的 LSMTree 删除代价过高 *
单机环境存放大量数据时不能占用过多文件句柄
=> *LevelDB 会随着时间增长产生大量小文件 *
数据存储需要热备份
=> *LevelDB 只能冷备 *
大数据场景下写吞吐量要跟得上
=> *BoltDB 的 B+tree 写操作吞吐量成瓶颈 *
存储需具备良好的压缩性能
=> *BoltDB 不支持压缩 *
此外, 出于技术栈的一致性以及部署的简易性考虑(面向容器部署),InfluxDB 团队希望存储引擎 与 其上层的 TSDB 引擎一样都是用 GO 编写, 因此潜在的 RocksDB 选项被排除
基于上述痛点, InfluxDB 团队决定自己做一个存储引擎的实现.
2 InfluxDB 的数据模型
在解析 InfluxDB 的存储引擎之前, 先回顾一下 InfluxDB 中的数据模型.
在 InfluxDB 中, 时序数据支持多值模型, 它的一条典型的时间点数据如下所示:
图 2-1
measurement:
指标对象, 也即一个数据源对象. 每个 measurement 可以拥有一个或多个指标值, 也即下文所述的 **field**. 在实际运用中, 可以把一个现实中被检测的对象 (如:"cpu") 定义为一个 measurement
tags:
概念等同于大多数时序数据库中的 tags, 通常通过 tags 可以唯一标示数据源. 每个 tag 的 key 和 value 必须都是字符串.
field:
数据源记录的具体指标值. 每一种指标被称作一个 "field", 指标值就是 "field" 对应的 "value"
timestamp:
数据的时间戳. 在 InfluxDB 中, 理论上时间戳可以精确到 ** 纳秒 **(ns)级别
此外, 在 InfluxDB 中, measurement 的概念之上还有一个对标传统 DBMS 的 Database 的概念, 逻辑上每个 Database 下面可以有多个 measurement. 在单机版的 InfluxDB 实现中, 每个 Database 实际对应了一个文件系统的 目录.
2.1 Serieskey 的概念
InfluxDB 中的 SeriesKey 的概念就是通常在时序数据库领域被称为 时间线 的概念, 一个 SeriesKey 在内存中的表示即为下述字符串 (逗号和空格被转义) 的 字节数组(GitHub.com/influxdata/influxdb/model#MakeKey())
{measurement 名}{tagK1}={tagV1},{tagK2}={tagV2},...
其中, SeriesKey 的长度不能超过 65535 字节
2.2 支持的 Field 类型
InfluxDB 的 Field 值支持以下数据类型:
Datatype | Size in Mem | Value Range |
---|---|---|
Float | 8 bytes | 1.797693134862315708145274237317043567981e+308 ~ 4.940656458412465441765687928682213723651e-324 |
Integer | 8 bytes | -9223372036854775808 ~ 9223372036854775807 |
String | 0~64KB | String with length less than 64KB |
Boolean | 1 byte | true 或 false |
在 InfluxDB 中, Field 的数据类型在以下范围内必须保持不变, 否则写数据时会报错 类型冲突.
同一 Serieskey + 同一 field + 同一 shard
2.3 Shard 的概念
在 InfluxDB 中, 能且只能 对一个 Database 指定一个 Retention Policy (简称: RP). 通过 RP 可以对指定的 Database 中保存的时序数据的留存时间 (duration) 进行设置. 而 Shard 的概念就是由 duration 衍生而来. 一旦一个 Database 的 duration 确定后, 那么在该 Database 的时序数据将会在这个 duration 范围内进一步按时间进行分片从而时数据分成以一个一个的 shard 为单位进行保存.
shard 分片的时间 与 duration 之间的关系如下
Duration of RP | Shard Duration |
---|---|
< 2 Hours | 1 Hour |
>= 2 Hours 且 <= 6 Months | 1 Day |
> 6 Months | 7 Days |
新建的 Database 在未显式指定 RC 的情况下, 默认的 RC 为 数据的 Duration 为永久, Shard 分片时间为 7 天
注: 在闭源的集群版 Influxdb 中, 用户可以通过 RC 规则指定数据在基于时间分片的基础上再按 SeriesKey 为单位进行进一步分片
3. InfluxDB 的存储引擎分析
时序数据库的存储引擎主要需满足以下三个主要场景的性能需求
大批量的时序数据写入的高性能
直接根据时间线 (即 Influxdb 中的 Serieskey ) 在指定时间戳范围内扫描数据的高性能
间接通过 measurement 和部分 tag 查询指定时间戳范围内所有满足条件的时序数据的高性能
InfluxDB 在结合了 1.2 所述考量的基础上推出了他们的解决方案, 即下面要介绍的 WAL + TSMFile + TSIFile 的方案
3.1 WAL 解析
InfluxDB 写入时序数据时为了确保数据完整性和可用性, 与大部分数据库产品一样, 都是会先写 WAL, 再写入缓存, 最后刷盘. 对于 InfluxDB 而言, 写入时序数据的主要流程如同下图所示:
图 3-5
InfluxDB 对于时间线数据和时序数据本身分开, 分别写入不同的 WAL 中, 其结构如下所示:
索引数据的 WAL
由于 InfluxDB 支持对 Measurement,TagKey,TagValue 的删除操作, 当然随着时序数据的不断写入, 自然也包括 增加新的时间线, 因此索引数据的 WAL 会区分当前所做的操作具体是什么, 它的 WAL 的结构如下图所示
图 3-6
时序数据的 WAL
由于 InfluxDB 对于时序数据的写操作永远只有单纯写入, 因此它的 Entry 不需要区分操作种类, 直接记录写入的数据即可
图 3-7
3.2 TSMFile 解析
TSMFile 是 InfluxDB 对于时序数据的存储方案. 在文件系统层面, 每一个 TSMFile 对应了一个 Shard.
TSMFile 的存储结构如下图所示:
图 3-1
其特点是在一个 TSMFile 中将 时序数据 (i.e Timestamp + Field value) 保存在数据区; 将 Serieskey 和 Field Name 的信息保存在索引区, 通过一个基于 Serieskey + Fieldkey 构建的形似 B+tree 的文件内索引快速定位时序数据所在的 数据块
注: 在当前版本中, 单个 TSMFile 的最大长度为 2GB, 超过时即使是同一个 Shard, 也会继续新开一个 TSMFile 保存数据. 本文的介绍出于简单化考虑, 以下内容不考虑同一个 Shard 的 TSMFile 分裂的场景
索引块的构成
上文的索引块的构成, 如下所示:
图 3-2
其中 索引条目 在 InfluxDB 的源码中被称为 directIndex. 在 TSMFile 中, 索引块是按照 Serieskey + Fieldkey 排序 后组织在一起的.
明白了 TSMFile 的索引区的构成, 就可以很自然地理解 InfluxDB 如何高性能地在 TSMFile 扫描时序数据了:
根据用户指定的时间线 (Serieskey) 以及 Field 名 在 索引区 利用二分查找找到指定的 Serieskey+FieldKey 所处的 索引数据块
根据用户指定的时间戳范围在 索引数据块 中查找数据落在哪个 (或哪几个) 索引条目
将找到的 索引条目 对应的 时序数据块 加载到内存中进行进一步的 Scan
注: 上述的 1,2,3 只是简单化地介绍了查询机制, 实际的实现中还有类似扫描的时间范围跨索引块等一系列复杂场景
时序数据的存储
在图 3-1 中介绍了时序数据块的结构: 即同一个 Serieskey + Fieldkey 的 所有时间戳 - Field 值对被拆分开, 分成两个区: Timestamps 区和 Value 区分别进行存储. 它的目的是: 实际存储时可以分别对时间戳和 Field 值按不同的压缩算法进行存储以减少时序数据块的大小
采用的压缩算法如下所示:
Timestamp: Delta-of-delta encoding http://www.vldb.org/pvldb/vol8/p1816-teller.pdf
Field Value: 由于单个数据块的 Field Value 必然数据类型相同, 因此可以集中按数据类型采用不同的压缩算法
Float 类: Gorrila's Float Commpression http://www.vldb.org/pvldb/vol8/p1816-teller.pdf
Integer 类型: Delta Encoding + Zigzag Conversion + RLE / Simple8b / None
String 类型: Snappy Compression https://github.com/golang/snappy
Boolean 类型: Bit packing
做查询时, 当利用 TSMFile 的索引找到文件中的时序数据块时, 将数据块载入内存并对 Timestamp 以及 Field Value 进行解压缩后以便继续后续的查询操作.
3.3 TSIFile 解析
有了 TSMFile, 第 3 章开头所说的三个主要场景中的场景 1 和场景 2 都可以得到很好的解决. 但是如果查询时用户并没有按预期按照 Serieskey 来指定查询条件, 而是指定了更加复杂的条件, 该如何确保它的查询性能? 通常情况下, 这个问题的解决方案是依赖倒排索引(Inverted Index).
InfluxDB 的倒排索引依赖于下述两个数据结构
- map<SeriesID, SeriesKey>
- map<tagkey, map<tagvalue, List<SeriesID>>>
它们在内存中展现如下:
图 3-3
图 3-4
但是在实际生产环境中, 由于用户的时间线规模会变得很大, 因此会造成倒排索引使用的内存过多, 所以后来 InfluxDB 又引入了 TSIFile
TSIFile 的整体存储机制与 TSMFile 相似, 也是以 Shard 为单位生成一个 TSIFile. 具体的存储格式就在此不赘述了.
4. 总结
以上就是对 InfluxDB 的存储机制的粗浅解析, 由于目前所见的只有单机版的 InfluxDB, 所以尚不知道集群版的 InfluxDB 在存储方面有哪些不同. 但是, 即便是这单机版的存储机制, 也对我们设计时序数据库有着重要的参考意义.
来源: https://yq.aliyun.com/articles/690672