InfluxDB(一)初探时序数据库

最近公司有个需求需要借助InfluxDB实现(或者更准确的说,使用该数据库可以更容易的实现),因此稍微看了下这个数据库,把比较重要的一些东西先简单记录一下,日后如果踩坑,也会继续在下面补充。

零、下载&安装

官方地址:https://portal.influxdata.com/downloads/

一、什么是时序数据库,它可以用来做什么?

简单来说时序数据库就是存储带有时间戳且包含随时间发生变化的数据,InfluxDB属于一种时序数据库。这类数据具体指什么数据呢?这里举几个例子:

  1. 监控,比如某一时段CPU占用率、服务QPS、服务耗时等监控数据
  2. 某飞机在运行过程中各时刻的高度、速度、机舱内温度、湿度
  3. 某地区每时每刻的室外温度记录

上述的数据都满足某一个具体的事物(监控指标、飞机、地区)随着时间的变化而需要记录的一些数据(监控数据、速度、温度等),这些数据被称为时序性数据,这些数据可以被用来做大数据分析、人工智能等。

Q:通过上述的例子,这些无非是一些指标数据嘛,为什么非得用时序性数据库记录?不可以用Mysql这种关系型数据库来做吗?

A:不可以,首先对于这种数据,量级上是巨大的,比如每时每刻都要记录某飞机的坐标变化,类似这种指标,量级很庞大,关系型数据库设计之初并不是为了处理这种大数据量级的,即便是后来的分表分库,也是为了优化数据量和访问量大了以后查询和写入性能问题的,所以不适合用来存储数量如此庞大的数据,所以这并不是能不能实现的问题,而是适不适合的问题,而时序型数据库的写入速度非常快,其次内部还会对存储数据进行压缩,同时提供了更高的可用性,比如数据保留策略、数据聚合函数、连续查询等。数据保留策略可以保证超过指定期限的数据被回收,释放磁盘空间。

二、InfluxDB基本概念

2.1:建表

不需要专门建表,一般insert执行过去时,如果发现表不存在,就自动创建。

2.2:基本概念与mysql中概念的对应关系

概念名称 概念解释 MySQL类比
database 数据库 类似mysql里的库
measurement 类似mysql里的表
point 一行数据记录 类似mysql里的一行数据
表1

2.3:point详解

point的概念就类似于mysql一行数据(data point,直译为数据点,可以理解成一行数据),point的构成部分有三个:

point属性 属性解释 备注
time 时间戳 influxdb自带字段,单位:纳秒
tags 有各种索引的属性 可设置多个,逗号隔开
fields 没有索引的属性 可设置多个,逗号隔开
表2

tagsfileds的区别:

1
2
1. field无索引,一般表示会随着时间戳而发生改变的属性,比如温度、经纬度等,类型可以是 string, float, int, bool
2. tag有索引,一般表示不会随着时间戳而发生改变的属性,比如城市、编号等,类型只可为 string

比较值得注意的是:在InfluxDB中,tags决定了series的数量,而series的数量越大,对于系统CPU和InfluxDB的性能影响越大,series估算方法下面会说。

2.4:sql语句示范

建库:

1
create database "库名"

一般来说,新建完库,还要对该库设置自己的时效策略RP),下面会详细说明RP的设置方法,一般建好库后都会有一个默认的RP策略

插入数据:

1
2
3
4
5
insert results,hostname=index2,name=index2333  value=2,value2=4

-------- ---------------------------- -------------------------

表名 tags的设置 fields的设置

上面这句sql的意思就是说在名为result的表里,插入两个分别命名为hostnamename取值分别为index2index2333tags,以及两个分别命名为valuevalue2取值分别为2和4的fields的数据。

⚠️ 注意:这里的指令直接输入整数,InfluxDB是按照浮点型处理的,如果一定要让上面的value=2value2=4中的24是整型数据,那么需要在后面加上修饰词:value=2ii就代表整型。

检索数据:

1
select * from results

打印结果为:

1
2
3
4
name: results2
time hostname name value value2
---- -------- ---- ----- ------
1560928150794920700 index2 index2333 2 4

分页检索:

1
SELECT * FROMWHERE 条件 LIMIT rows OFFSET (page - 1)*rows

rows代表每页展示行数,page表示页码

删除表:

1
drop measurement "表名"

三、InfluxDB的时效设置

很遗憾,InfluxDB是不支持删除和修改的,删除有专门的操作,但是性能很低,不建议使用。

InfluxDB支持定期清除数据策略。

查看当前库对应策略配置的命令为:

1
show retention policies on "库名"

结果:

1
2
3
name       duration      shardGroupDuration       replicaN     default
------- --------- ------------------ --------- -------
autogen 0s 168h0m0s 1 true

name:过期策略名

duration:保留多长时间的数据,比如3w,意思就是把三周前的数据清除掉,只保留三周内的,支持h(小时),d(天),w(星期)这几种配法。

shardGroupDuration:存储数据的分组结构,比如设置为1d,表示的是每组存储1d的数据量,也就是说数据将按照天为单位划分存储分组,然后根据每条数据的时间戳决定把它放到哪个分组里,因此这个概念还会影响到过期策略,因为InfluxDB在清除过期数据时不可能逐条清理,而是通过清除整个ShardGroup的方式进行,因为通过跟当前时间对比就可以知道哪些分组里的数据一定是过期的,从而进行整组清理,效率往往更高。

replicaN:副本数量,一般为1个(这个大概就是备份的意思?这个配置在单实例模式下不起作用)

default:是否是默认配置,设置为true表示默认的意思,主要用于查询时是否指定策略,如果不指定,则这个查询就是针对default=true的策略进行的,注意,本文章里的sql都没有指明保留策略的名字,如果有需要,那么请指定。一个库允许有多套策略配置(每套策略里都可以有自己的一份数据,比如同样一张表的数据在策略A和策略B的情况下是不同的,可以理解为一个库对应N个策略,每个策略里有自己的N多张表,相互独立),在不指定策略名称的情况下写的sql,默认使用default=true的策略。

当然,策略也支持修改,指令如下:

1
alter retention policy "autogen" on "test" duration 30d default

上面这条指令就会把上面策略明为autogen的策略有效保存时间改为30天

四、InfluxDB的存储结构

4.1:结构层级

了解完策略,结合上面提到的seriestagsfields等概念,画一下单个InfluxDB库的存储结构:

图1

图里没有体现出tagsfields这些数据层面的东西,它们最终被存放在了Shard里,之前说过tags会影响series的大小,其实series就相当于一个唯一化分类,eries估算方式为:

1
series的个数 = RP × measurement × tags(tags去重后的个数)

比如一个database中有一个measurement,叫test,有两个RP(7d, 30d),taghostserver。host的值有hostA、hostB,server为server1,server2。那么这个database的series值为2RP x 4tags = 8

4.2:LSM-Tree

回归图1前,先来了解下LSM-Tree,几乎所有的k-v存储的写密集型数据库都采用该数据结构实现,该数据结构的结构如下(注意下面说的层级并非树的层级,而是合并逻辑发生时的层级):

图2

图2,该结构写入流程如下:

写入的数据首先加到0层,0层的数据存储在内存中,短期内查询的数据一般在0层,由于是内存操作,因此效率会非常高,当0层的数据达到一定大小时,此时会把0层 和位于它下面的1层进行合并,然后合并出来的新的数据(0层+1层的数据)会顺序写磁盘(这里由于是顺序写入磁盘,因此写性能会非常好),然后替换掉原来老的1层数据,当1层达到一定大小的时候,将继续和它的下层合并,以此类推,一级一级的往下递,除此之外还可以将合并之后旧文件全部删掉,留下最新的。

LSM-Tree参考文章:LSM-tree 基本原理及应用

然后回归图1,最终的数据会被存储进ShardShard里存在几个区域,就是最终存放数据的地方,下面针对这几个区域说明下它们的作用:

4.3:Cache

相当于上面说的LSM-Tree0层,存放于内存中,写入的数据时首先被该模块接收并存储,该模块在内存中表现为一个map结构,k = seriesKey + 分隔符 + fieldsName,v = 具体的filed对应的值的数组,具体结构体如下(参考网上资料):

代码块1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Cache struct {
commit sync.Mutex
mu sync.RWMutex
store map[string]*entry
size uint64 // 当前使用内存的大小
maxSize uint64 // 缓存最大值

// snapshots are the cache objects that are currently being written to tsm files
// they're kept in memory while flushing so they can be queried along with the cache.
// they are read only and should never be modified
// memtable 快照,用于写入 tsm 文件,只读
snapshot *Cache
snapshotSize uint64
snapshotting bool

// This number is the number of pending or failed WriteSnaphot attempts since the last successful one.
snapshotAttempts int

stats *CacheStatistics
lastSnapshot time.Time
}

基于前面说的LSM-Tree,可以知道这里的cache不是持续增长的,而是达到一定值就会进行跟下层存储在磁盘上的数据(1~n层)进行合并,然后清空cache,在InfluxDB中,这一部分存储在磁盘上的数据,就是指TSM File模块,下面会介绍。

4.4:WAL

在上面对于Cache的描述中,Cache是基于内存做的写入数据接收方,那么如果中途机器宕掉,那么就会造成Cache里数据丢失的问题,为了解决这个问题,InfluxDB就设计了WAL模块,实际在写入一个数据时,不仅会先写进Cache,还会写入WAL,可以简单理解WAL就是对Cache里数据的备份,防止数据丢失,在Cache做完一次合并清除掉自身时,旧的WAL文件也会随之删除,然后新建一个WAL,迎接新一轮的写入。同样的,在InfluxDB启动时,也会先去读取WAL文件初始化Cache模块

4.5:TSM File

用于组成TSM-Tree结构的主要磁盘文件(可以对应图2的1~n层),内部做了很多存储以及压缩优化,单个TSM File的最大大小为2GB

4.6:Compactor

在后台持续运行的一个task(频率为1s),主要做以下事情:

  1. 在Cache达到阈值时,进行快照,然后将数据合并并保存在TSM File中
  2. 合并TSM File,将多个小型TSM File进行合并,使得每个文件的大小尽可能达到单个文件最大大小(也就是上面说到的2GB)
  3. 检查并删除一些已关闭的WAL文件

五、具体案例,以及实际的存储目录

结合图1的结构以及图2的数据结构,再加上对shard内部各组件的介绍,下面通过一个实际的例子来探索下InfluxDB实际的存储目录。

现在创建出来一个叫style_rank的库,且设置默认RP为7d,保存周期为7天,默认分组策略为24h分一次,如图:

图3

图中名为7drp策略被设置为默认策略,其中duration被设置为168hshardGroupDuration被设置为了24h,意味着该策略里的表数据将以7天为一个周期过期,期间以1天为单位分组。

5.1:InfluxDB配置文件

下载下来一个InfluxDB后,通过配置文件influxdb.conf可以配置各个文件存放的目录:

图4

这几个配置决定了你将产生的数据存放在哪里,建议自定义这个,默认的目录很迷=_=

5.2:具体的数据目录

配置完上面的文件,启动,然后录入数据,然后再去具体的目录下查找具体的数据文件:

元数据层:用来存放当前库的属性,比如RP默认RP分组策略等。

图5

wal层:用来存放刚写入数据的信息,会先写入内存,然后异步写入wal文件,wal文件用来重启InfluxDB时恢复内存数据用,当wal文件达到一定大小时,会压入data层wal层的结构和data层几乎一致

图6

上面进入wal目录后,需要选择正确的库,这里选择style_rank,进入库后,会显示出该库的所有RP,这里选择7d,然后对应图1,你现在来到了ShardGroup层,图6中的编号,就是生成的ShardGroup,随便进入一个,则可以看到最终的wal文件(事实上只有当天建的group内的wal才有数据)。

data层:用来最终存放数据的目录,所有wal文件内的数据达到一定大小后,均会被压缩进data层,现在进入data层,然后进入style_rank

图7

可以看到下方除了有两个RP策略外,还有_series目录,这个目录就是存放索引tags字段所产生的索引数据)的地方,用于服务重启时恢复索引数据用。

进入7d,可以看到跟wal层差不多的ShardGroup分层,你现在又来到了图1ShardGroup层:

图8

然后再次进入一个分组内:

图9

最终的tsm文件就存在该目录下,fields.idx存放的是表里的fields元数据,用于写入数据时做效验用。

六、执行计划

然后继续由上面的style_rank库,在7d这个RP下,新建两张表:

1
2
insert season_views,date="201907" season_id=666,views=56789
insert season_views_v2,season_id=666 views=56789

目的很简单,只是为了记录下season_id=xxx的数据在某一时刻对应的views值,俩表的区别在于,season_view仅仅以日期为tag(作为索引字段,可以忽略不计),season_views_v2则以season_id作为tag,某一时刻,两张表的数据量均达到了5kw数据一致,下面,让我们以最简单的方式,查询下这两张表:

ℹ️ ps:sql前加explain关键字可以查看其执行计划,加explain analyze可以看到具体每一步执行的耗时。

图10

同样的查询,season_id加了索引和不加索引的区别:

NUMBER OF SERIES(扫描系列数,加了索引后根据series的哈希算法,同一个season_id都被分在了同一个Shard里,这里之所以为8,是因为ShardGroup不同)由不加索引的64个,减少到了8个

NUMBER OF FILES(扫描文件数)由不加索引的54个减少到11个

NUMBER OF BLOCKS(扫描的数据块)由不加索引的152058减少到了184

七、总结

  1. 在使用时,尽可能按照某具体时间段进行过滤,如果过滤条件筛选出的数据量过大,则会严重影响查询效率。
  2. 适度使用函数,若使用,请保证筛选条件筛选出的数据量,如果过大,效率会极低,比如在season_views_v2中启用TOP函数,查出前三名,查询耗时超过10s,加上season_id条件后,则迅速返回。
  3. 加索引时需要注意,尽量避免大数据量的属性做tag,否则会产生大量series,生成大量索引数据,占用过多内存,比如用户级别的tag(这里的用户级别是指大型网站用户数量级,一般1亿以上的情况)。
  4. 免费版的InfluxDB不支持集群,主从需要借助influx-proxy实现,也可以自己通过监听wal文件写入,通过消息队列的方式实现主从同步,wal类似mysql里的binlog
  5. 建议使用的时候,先把自己的库建好,然后再把RP建好,如果嫌麻烦,可以直接把自己新建的RP定义成默认RP,如果RP设置为默认,只会对查询产生影响(当然写的时候也需要指定),在sql中不指定RP的情况下执行,则认为就是走的默认RP。
  6. 分库,建议按照日期分。