时序数据的来源很多,比如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遇到了一些问题:
- LevelDB其实是一个库,不是服务,没有实现热备份。
- LevelDB的删除操作的实现方式是写入一条tombstone记录,由后台的compaction进行删除,所以删除等效于写入。但是如上文所说,时序数据会涉及到大量的过期数据需要删除,这会严重影响数据写入效率。为了解决这个问题,InfluxDB根据时间段将数据分为多个shard,每个shard起一个LevelDB存储。这样删除过期数据就可以直接根据shard的时间区间进行,没必要通过写入操作。
- 使用shard又造成了另一个问题,随着数据量的增大,开了越来越多的LevelDB,每个LevelDB都会产生很多SSTable文件,机器上的文件句柄不久就被用完了。
mmap B+Tree
0.9版本使用BoltDB来做底层存储,其存储引擎用的是 mmap B+Tree(memory mapped B+Tree)。这带来了3个好处:
- InfluxDB和BoltDB都是以Go编写的,BoltDB可以直接嵌入到InfluxDB中。
- BoltDB支持热备份。
- BoltDB中每个数据库存储为一个文件,节省文件句柄。
InfluxDB还在BoltDB前面加了一层WAL。但是BoltDB由于数据写入时需要更新索引,造成随机写,其性能满足不了高IOPS的场景。
TSM Tree
从0.9.5版本开始,到目前最新1.7版本,InfluxDB使用新设计实现的存储引擎TSM Tree,这次又回到了LSM Tree,针对时序数据进行优化。对于其实现细节,已经有很多很专业的文章,但是不太易于理解。
- InfluxData Doc
- InfluxDB详解之TSM存储引擎解析(一)
- InfluxDB详解之TSM存储引擎解析(二)
- 时序数据库技术体系 – InfluxDB TSM存储引擎之TSMFile
- 时序数据库技术体系 – InfluxDB 多维查询之倒排索引
LSM Tree对于写入效率的优化已经达到了极致,所以TSM的优化最主要的是对于数据压缩、数据清除和查询效率。以上文给出的数据为例,我们主要的需求场景有2种:
-
location=SH,host=server1
这一台服务器最近24小时的运行状态,CUP和Mem使用情况。这是给定了SeriesKey
查找对应的series。这个其实是比较直接的,因为每一个entry都可以通过其EntryKey定位出来。 -
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
的数据,这个设计其实是列式存储,有两个好处:
- 根据timestamp进行区间检查找时,只需要拼接相关
Entry
,可以提高查询效率 - 列式存储,相同数据类型,更好的压缩率
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