InfluxDB的存储引擎演化过程

简介: InfluxDB的存储引擎从LSM Tree,到mmap B+ Tree,再到TSM Tree。

时序数据的来源很多,比如DevOps,目前公司的的机房里面成百上千台的服务器和虚机已经很常见了,这些服务器的运行状态、内部服务的log这些都需要监控。还有IoT,5G技术逐渐成熟,IoT将直接受益,终端设备每时每刻对会将其探测到数据以及其自身的运行状态数据传回服务器。时序数据数据以下特点:

  • 数据量非常大
  • 高并发读/写,取决于产生数据的终端数目、频率、数据维度,以及监控分析系统的需求
  • 数据持续稳定生成,系统设计时就可以基本确定其IOPS
  • 与时间维度完全相关

    • 时间越久的数据其价值越来越低,这也会导致大量过期数据的删除操作
    • 单点的数据没有价值,价值在连续的数据分布,所以就算单点数据出错了也没有必要修正

目前有一些公司开始关注时序数据系统,比如Facebook的beringei数据库,阿里云上实现的HiTSDB服务。InfluxDB是目前最为活跃的开源时序数据库(TSDB: Time Series Database)。InfluxDB尝试了不同的数据存储引擎,最终还是设计了针对时间序列进行优化的TSM Tree(Time-Structured Merge Tree)。

数据组织形式

以下是批量写入InfluxDB的数据Line Protocol。数据中包含了服务器运行参数和空气质量监测数据。

Labs,location=SH,host=server1 CUP=73,Mem=16.067 1574179200s
Labs,location=SH,host=server1 CUP=74,Mem=15.123 1574179210s
Labs,location=SH,host=server1 CUP=71,Mem=15.450 1574179220s
Labs,location=SH,host=server2 CUP=31,Mem=32.087 1574179200s
Labs,location=SH,host=server2 CUP=20,Mem=32.167 1574179210s
Labs,location=SH,host=server2 CUP=25,Mem=32.257 1574179220s
Labs,location=SZ,host=server1 CUP=11,Mem=8.021 1574179200s
Labs,location=SZ,host=server1 CUP=17,Mem=7.530 1574179210s
Labs,location=SZ,host=server1 CUP=37,Mem=7.214 1574179220s
Labs,location=SZ,host=server2 CUP=43,Mem=16.779 1574179200s
Labs,location=SZ,host=server2 CUP=22,Mem=16.326 1574179210s
Labs,location=SZ,host=server2 CUP=21,Mem=16.117 1574179220s
Air,city=SH,sensor=e6b58be8 PM25=62,O3=16,CO=5 1574179200s
Air,city=SH,sensor=e6b58be8 PM25=62,O3=19,CO=6 1574179210s
Air,city=SH,sensor=e6b58be8 PM25=62,O3=23,CO=5 1574179220s

measurement:是逻辑上的数据容器,类似于MySQL里面的table,数据样本中的Labs和Air。

tag set:以第一行数据为例,location和host就是tag key,SH和server1就是对应的tag value。组合在一起location=SH,host=server1就是tag set,还有city=SH,sensor=e6b58be8等。

field set:第一行数据中的CUP=73,Mem=16.067

timestamp:第一行数据中的1574179200s

point:数据点,样本数据中每一行就是一个数据点,它包括了以上列出来的几个部分。

series:给定一组tag set,所包含的所有数据点组成的相关数据序列,基于timestamp有序的。比如在Labs这个measurement中,location=SH,host=server1这个tag set对应的就是一个series,这个series包含了3个point。这里measurement + tag set就可以唯一标识series,称为SeriesKey。这是对InfluxDB里面最基本的时序数据组织形式。

entry:每个series中包含了field set,每个field对应的一列数据以及相对的timestamp就组成一个entry,这里SeriesKey + FiledName就是EntryKey 。比如Labs + location=SH,host=server1 + CUP对应的entry[1574179200s|73, 1574179210s|74, 1574179220s|71]。所以entry就是一个值序列。

时序数据中,单个point是没有意义的,意义在series或者entry上面进行聚合计算,也就是给定tags、fields以及一个时间区间,进行查询计算。

LSM Tree

一直到0.8版本以前,InfluxDB使用LSM Tree(Log-Structured Merge Tree)作为存储引擎,底层直接使用LevelDB,用timestamp作为key进行排序存储。关于LSM Tree可以参考序列文章:

使用LSM Tree遇到了一些问题:

  1. LevelDB其实是一个库,不是服务,没有实现热备份。
  2. LevelDB的删除操作的实现方式是写入一条tombstone记录,由后台的compaction进行删除,所以删除等效于写入。但是如上文所说,时序数据会涉及到大量的过期数据需要删除,这会严重影响数据写入效率。为了解决这个问题,InfluxDB根据时间段将数据分为多个shard,每个shard起一个LevelDB存储。这样删除过期数据就可以直接根据shard的时间区间进行,没必要通过写入操作。
  3. 使用shard又造成了另一个问题,随着数据量的增大,开了越来越多的LevelDB,每个LevelDB都会产生很多SSTable文件,机器上的文件句柄不久就被用完了。

mmap B+Tree

0.9版本使用BoltDB来做底层存储,其存储引擎用的是 mmap B+Tree(memory mapped B+Tree)。这带来了3个好处:

  1. InfluxDB和BoltDB都是以Go编写的,BoltDB可以直接嵌入到InfluxDB中。
  2. BoltDB支持热备份。
  3. BoltDB中每个数据库存储为一个文件,节省文件句柄。

InfluxDB还在BoltDB前面加了一层WAL。但是BoltDB由于数据写入时需要更新索引,造成随机写,其性能满足不了高IOPS的场景。

TSM Tree

从0.9.5版本开始,到目前最新1.7版本,InfluxDB使用新设计实现的存储引擎TSM Tree,这次又回到了LSM Tree,针对时序数据进行优化。对于其实现细节,已经有很多很专业的文章,但是不太易于理解。

LSM Tree对于写入效率的优化已经达到了极致,所以TSM的优化最主要的是对于数据压缩、数据清除和查询效率。以上文给出的数据为例,我们主要的需求场景有2种:

  1. location=SH,host=server1这一台服务器最近24小时的运行状态,CUP和Mem使用情况。这是给定了SeriesKey查找对应的series。这个其实是比较直接的,因为每一个entry都可以通过其EntryKey定位出来。
  2. location=SH这个机房所有机子最近24小时的总体运行状态,比如最大CPU,平均Mem。这里没有给定完全的SeriesKey,而是通过某些tag需要查询出所有相关的series。这个情况稍微麻烦一下,我们放到下面说。

下面我们来看InfluxDB数据存储的实现。

Shard

Shard是InfluxDB中真正完成数据存储的部件。延用了前面版本基于LevelDB存储的思想,每个shard等价于一个LevelDB实例,包含独立的wal,cache(LevelDB中的MemTable),分层级的tsm文件(LevelDB中的SSTable)以及compaction。新数据进来会根据timestamp写入对应的shard,如果是分布式环境,还可以通过hash放到不同的机器上面。

Cache

放在内存中,设计相对简单,就是一个字典,key是EntryKey,value是Entry。新数据写入时,找到相对的Entry追加进去。当大小达到阈值,再写到TSM文件。

TSM文件

TSM文件的设计类似于SSTable,cache中的一个Entry会持久化到tsm文件中一个或者多个block,并为每一个block建立index,在index中记录该block的EntryKey和timestamp区间,用于查询。上面的数据写入tsm文件会生成以下key-value条目:

Labs,location=SH,host=server1___CUP ===> (1574179200s,1574179210s,1574179220s), (73,74,71)
Labs,location=SH,host=server1___Mem ===> (1574179200s,1574179210s,1574179220s), (16.067,15.123,15.450)
Labs,location=SH,host=server2___CUP ===> (1574179200s,1574179210s,1574179220s), (31,20,25)
Labs,location=SH,host=server2___Mem ===> (1574179200s,1574179210s,1574179220s), (32.087,32.167,32.257)
Labs,location=SZ,host=server1___CUP ===> (1574179200s,1574179210s,1574179220s), (11,17,37)
Labs,location=SZ,host=server1___Mem ===> (1574179200s,1574179210s,1574179220s), (8.021,7.530,7.214)
......

到这里,解决上面提出来的第一个问题就比较清楚了,因为对EntryKey和timestamp都已经建了索引。还有一点,tsm文件中的每一个block只会Entry的数据,这个设计其实是列式存储,有两个好处:

  1. 根据timestamp进行区间检查找时,只需要拼接相关Entry,可以提高查询效率
  2. 列式存储,相同数据类型,更好的压缩率

TSI(Time Series Index)

这里就来看一下第二个问题。首先对数据中的series进行编号,并维护一个字典从series id映射到SeriesKey

{
    0: "location=SH,host=server1",
    1: "location=SH,host=server2",
    2: "location=SZ,host=server1",
    3: "location=SZ,host=server2"
}

然后对所有tag建立反向索引(就是看跟每个tag相关的所有series):

{
    "location": {
        "SH": [0, 1],
        "SZ": [2, 3]
    },
    "host": {
        "server1": [0, 2],
        "server2": [1, 3],
    }
}

现在第二个问题的解决方法就比较清楚了,只要在反向索引中找出所有跟给定的tag相关的series,通过series的SeriesKey就可以回到第一个问题,所以最终还是基于SeriesKey来定位到相应的数据。另外一点,InfluxDB对tag是加了索引的,给定某一个tag进行查询的效率很高,但是field没有,也就是说类似于SELECT * FROM Labs WHERE CUP=73这样对field进行检索是比较慢的,因为会对tsm文件中的数据进行遍历。

这又带来另外一个问题,如果series太多,反向索引在内存中放不了怎么办?这是在1.3版本中解决的问题,也是使用LSM Tree持久化到磁盘,同理也是每一个shard都会有一个独立的LSM Tree,磁盘文件名为fields.idx。

数据库文件结构

<root>/
├── data/
|    └── <db name>/
|        └── autogen/
|            └── 123/ # shard name
|                ├── 000000009-0000000001.tsm
|                └── fields.idx
├── meta/
|    └── meta.db
└── wal/
    └── <db name>/
        └── autogen/
            └── 123/ # shard name
                └── _00085.wal
目录
相关文章
|
1月前
|
存储 监控 物联网
【Clickhouse 探秘】Clickhouse 投影技术到底能做什么?怎么实现的?
ClickHouse 投影是一种数据结构,用于提高特定查询模式下的性能。通过预处理数据,投影可以显著减少查询的执行时间,特别是在复杂的聚合和排序查询中。投影自动与基础表数据保持同步,支持多投影,适用于实时分析、用户行为分析、日志分析等场景。虽然投影能显著提升查询性能,但也会增加存储开销和写入性能的影响。
73 0
|
7月前
|
存储 运维 监控
面经:Cassandra分布式NoSQL数据库深度解读
【4月更文挑战第10天】本文是关于Apache Cassandra的面试准备指南,涵盖了数据模型、一致性模型、架构性能优化和故障恢复等核心知识点。作者强调理解Cassandra的列族、Tunable Consistency、Gossip协议及运维策略的重要性,并通过代码示例辅助解释。掌握这些内容不仅能帮助在面试中表现出色,也有助于实际工作中解决大规模数据处理问题。
117 1
|
7月前
|
存储 监控 负载均衡
InfluxDB最佳实践:数据模型设计与查询优化
【4月更文挑战第30天】本文探讨了InfluxDB的最佳实践,重点在于数据模型设计和查询优化。合理选择字段类型,根据业务逻辑划分Measurement,利用Tags进行索引优化,以及适时数据归档和清理,能有效提升性能。查询优化包括使用索引、精简查询语句、应用聚合函数及限制返回结果。分布式查询和分片适用于大规模数据集,以实现并行查询和负载均衡。这些策略旨在帮助用户优化InfluxDB的性能,进行高效时序数据分析。
|
7月前
|
存储 NoSQL 分布式数据库
分布式NoSQL列存储数据库Hbase_高级思想(八)
分布式NoSQL列存储数据库Hbase_高级思想(八)
70 0
|
存储 缓存 NoSQL
H2存储内核分析一
现在做数据库一般都才有 C/C++ 获取其它编译型的语言,为什么会选择 h2 这种基于 java 的语言?会不会影响效率?其实回答这个问题很简单,无论是用什么语言来实现数据库,其实都是在调用操作系统 IO 的函数。因此仅仅是作为存储的话差别其实是不大的。 现在大多数,涉及到存储内核的文章或者讲义,要么是一堆原理,要么就是玩具版本例子,根本无法应用到实际的工程上面去,就像马保国的闪电五连鞭一样。我们选择 h2 的一个重要原因就是,学习完后,可以直接应用到工程上。行不行直接在擂台上比一下就知道了。
H2存储内核分析一
|
存储 传感器 分布式计算
「时序数据库」时序数据库和MongoDB第二部分-模式设计最佳实践
「时序数据库」时序数据库和MongoDB第二部分-模式设计最佳实践
|
存储 NoSQL Oracle
「时序数据库」使用cassandra进行时间序列数据扫描
「时序数据库」使用cassandra进行时间序列数据扫描
|
存储 分布式计算 druid
一篇文章搞懂数据仓库:数据应用--OLAP
一篇文章搞懂数据仓库:数据应用--OLAP
一篇文章搞懂数据仓库:数据应用--OLAP
|
SQL 存储 缓存
CockroachDB之本地以及分布式查询处理
译者注:本文详细介绍了CockroachDB的两种查询处理方式:本地及分布式,其中详细描述了设计分布式引擎的目的,为了达到分布式,还存在哪些遗留问题。以下为译文。 当CockroachDB节点接收到查询SQL时,大概会发生什么事情呢: pgwire模块负责与客户端应用通信,从客户端接收查询请求。将SQL文本分析并转换为抽象语法树(Abstract Syntax Tree,简称AST)。然后进一步分析并将其转换为逻辑查询计划,该计划是关系运算符的树,如过滤器,渲染(项目),连接。 顺便说一下,逻辑计划树是由EXPLAIN语句报告的数据。 然后将逻辑计划交给负责执行查询的back-end层
332 0