整理自:https://sq.163yun.com/blog/article/169866295296581632
InfluxDB的数据模型和其他时序数据库有些许不同,下图是InfluxDB中的一张示意表:
1. Measurement:从原理上讲更像SQL中表的概念。这和其他很多时序数据库有些不同,其他时序数据库中Measurement可能与Metric等同,而在influxDB中field的概念更像是其他数据库的Metric,这点需要注意。
2. Tags:维度列
(1)上图中location和scientist分别是表中的两个Tag Key,其中location对应的维度值Tag Values为{1, 2},scientist对应的维度值Tag Values为{langstroth, perpetual},两者的组合TagSet有四种:
location = 1 , scientist = langstroth
location = 1 , scientist = perpetual
location = 2 , scientist = langstroth
location = 2 , scientist = perpetual
(2)在InfluxDB中,表中Tags组合会被作为记录的主键,因此主键并不唯一,比如上表中第一行和第三行记录的主键都为’location=1,scientist=langstroth’。所有时序查询最终都会基于主键查询之后再经过时间戳过滤完成。
3. Fields:数值列。数值列存放用户的时序数据。
4. Point:类似SQL中一行记录,而并不是一个点。
时序数据的时间线就是一个数据源采集的一个指标随着时间的流逝而源源不断地吐出数据,这样形成的一条数据线称之为时间线。如下图所示:
上图中有两个数据源,每个数据源会采集两种指标:butterflier和honeybees。InfluxDB中使用Series表示数据源,Series由Measurement和Tags组合而成,Tags组合用来唯一标识Measurement。Series是InfluxDB中最重要的概念,在接下来的内核分析中会经常用到。
在 InfluxDB 中,我们可以粗略的将要存入的一条数据看作一个虚拟的 key 和其对应的 value(field value),格式如下:
cpu_usage,host=server01,region=us-west value=0.64 1434055562000000000
虚拟的 key 包括以下几个部分: database, retention policy, measurement, tag sets, field name, timestamp。
database 和 retention policy 在上面的数据中并没有体现,通常在插入数据时在 http 请求的相应字段中指定。
host=server01,region=us-west
和 host=server02,region=us-west
就是两个不同的 tag set。value
就是 fieldName,InfluxDB 中支持一条数据中插入多个 fieldName,这其实是一个语法上的优化,在实际的底层存储中,是当作多条数据来存储。Point
InfluxDB 中单条插入语句的数据结构,series + timestamp
可以用于区别一个 point,也就是说一个 point 可以有多个 field name 和 field value。
Series
series 相当于是 InfluxDB 中一些数据的集合,在同一个 database 中,retention policy、measurement、tag sets 完全相同的数据同属于一个 series,同一个 series 的数据在物理上会按照时间顺序排列存储在一起。
series 的 key 为 measurement + 所有 tags 的序列化字符串
,这个 key 在之后会经常用到。
代码中的结构如下:
type Series struct {
mu sync.RWMutex
Key string // series key
Tags map[string]string // tags
id uint64 // id
measurement *Measurement // measurement
}
Shard
shard 在 InfluxDB 中是一个比较重要的概念,它和 retention policy 相关联。每一个存储策略下会存在许多 shard,每一个 shard 存储一个指定时间段内的数据,并且不重复,例如 7点-8点 的数据落入 shard0 中,8点-9点的数据则落入 shard1 中。每一个 shard 都对应一个底层的 tsm 存储引擎,有独立的 cache、wal、tsm file。
创建数据库时会自动创建一个默认存储策略,永久保存数据,对应的在此存储策略下的 shard 所保存的数据的时间段为 7 天,计算的函数如下:
func shardGroupDuration(d time.Duration) time.Duration {
if d >= 180*24*time.Hour || d == 0 { // 6 months or 0
return 7 * 24 * time.Hour
} else if d >= 2*24*time.Hour { // 2 days
return 1 * 24 * time.Hour
}
return 1 * time.Hour
}
如果创建一个新的 retention policy 设置数据的保留时间为 1 天,则单个 shard 所存储数据的时间间隔为 1 小时,超过1个小时的数据会被存放到下一个 shard 中。
InfluxDB对数据的组织和其他数据库相比有很大的不同,为了更加清晰的说明,笔者按照自己的理解画了一张InfluxDB逻辑架构图:
DataBase
InfluxDB中有Database的概念,用户可以通过create database xxx来创建一个数据库。
Retention Policy(RP)
数据保留策略。很长一段时间笔者对RP的理解都不足够充分,以为RP只规定了数据的过期时间。其实不然,RP在InfluxDB中是一个非常重要的概念,核心作用有3个:指定数据的过期时间,指定数据副本数量以及指定ShardGroup Duration。RP创建语句如下:
CREATE RETENTION POLICY ON ON DURATION REPLICATION [SHARD DURATION ] [DEFAULT]
其中retention_policy_name表示RP的名称,database_name表示数据库名称,duration表示TTL,n表示数据副本数。SHARD DURATION下文再讲。举个简单的栗子:
CREATE RETENTION POLICY "one_day_only" ON "water_database" DURATION 1d REPLICATION 1 SHARD DURATION 1h DEFAULT
InfluxDB中Retention Policy有这么几个性质和用法:
1. RP是数据库级别而不是表级别的属性。这和很多数据库都不同。
2. 每个数据库可以有多个数据保留策略,但只能有一个默认策略。
3. 不同表可以根据保留策略规划在写入数据的时候指定RP进行写入,下面语句就指定six_mouth_rollup的rp进行写入:
curl -X POST 'http://localhost:8086/write?db=mydb&rp=six_month_rollup' -->'disk_free,hostname=server01 value=442221834240i 1435362189575692182'
如果没有指定任何RP,则使用默认的RP。
Shard Group
Shard Group是InfluxDB中一个重要的逻辑概念,从字面意思来看Shard Group会包含多个Shard,每个Shard Group只存储指定时间段的数据,不同Shard Group对应的时间段不会重合。比如2017年9月份的数据落在Shard Group0上,2017年10月份的数据落在Shard Group1上。
每个Shard Group对应多长时间是通过Retention Policy中字段”SHARD DURATION”指定的,如果没有指定,也可以通过Retention Duration(数据过期时间)计算出来,两者的对应关系为:
问题来了,为什么需要将数据按照时间分成一个一个Shard Group?个人认为有两个原因:
1. 将数据按照时间分割成小的粒度会使得数据过期实现非常简单,InfluxDB中数据过期删除的执行粒度就是Shard Group,系统会对每一个Shard Group判断是否过期,而不是一条一条记录判断。
2. 实现了将数据按照时间分区的特性。将时序数据按照时间分区是时序数据库一个非常重要的特性,基本上所有时序数据查询操作都会带有时间的过滤条件,比如查询最近一小时或最近一天,数据分区可以有效根据时间维度选择部分目标分区,淘汰部分分区。
Shard
Shard Group实现了数据分区,但是Shard Group只是一个逻辑概念,在它里面包含了大量Shard,Shard才是InfluxDB中真正存储数据以及提供读写服务的概念,类似于HBase中Region,Kudu中Tablet的概念。关于Shard,需要弄清楚两个方面:
1. Shard是InfluxDB的存储引擎实现,具体称之为TSM(Time Sort Merge Tree) Engine,负责数据的编码存储、读写服务等。TSM类似于LSM,因此Shard和HBase Region一样包含Cache、WAL以及Data File等各个组件,也会有flush、compaction等这类数据操作。
2. Shard Group对数据按时间进行了分区,那落在一个Shard Group中的数据又是如何映射到哪个Shard上呢?
InfluxDB采用了Hash分区的方法将落到同一个Shard Group中的数据再次进行了一次分区。这里特别需要注意的是,InfluxDB是根据hash(Series)将时序数据映射到不同的Shard,而不是根据Measurement进行hash映射,这样会使得相同Series的数据肯定会存在同一个Shard中,但这样的映射策略会使得一个Shard中包含多个Measurement的数据,不像HBase中一个Region的数据肯定都属于同一张表。
上文已经对InfluxDB的Sharding策略进行了介绍,这里简单地做下总结。我们知道通常分布式数据库一般有两种Sharding策略:Range Sharding和Hash Sharding,前者对于基于主键的范围扫描比较高效,HBase以及TiDB都采用的这种Sharding策略;后者对于离散大规模写入以及随即读取相对比较友好,通常最简单的Hash策略是采用取模法,但取模法有个很大的弊病就是取模基础需要固定,一旦变化就需要数据重分布,当然可以采用更加复杂的一致性Hash策略来缓解数据重分布影响。
InfluxDB的Sharding策略是典型的两层Sharding,上层使用Range Sharding,下层使用Hash Sharding。对于时序数据库来说,基于时间的Range Sharding是最合理的考虑,但如果仅仅使用Time Range Sharding,会存在一个很严重的问题,即写入会存在热点,基于Time Range Sharding的时序数据库写入必然会落到最新的Shard上,其他老Shard不会接收写入请求。对写入性能要求很高的时序数据库来说,热点写入肯定不是最优的方案。解决这个问题最自然的思路就是再使用Hash进行一次分区,我们知道基于Key的Hash分区方案可以通过散列很好地解决热点写入的问题,但同时会引入两个新问题:
1. 导致Key Range Scan性能比较差。InfluxDB很优雅的解决了这个问题,上文笔者提到时序数据库基本上所有查询都是基于Series(数据源)来完成的,因此只要Hash分区是按照Series进行Hash就可以将相同Series的时序数据放在一起,这样Range Scan性能就可以得到保证。事实上InfluxDB正是这样实现的。
2. Hash分区的个数必须固定,如果要改变Hash分区数会导致大量数据重分布。除非使用一致性Hash算法。笔者看到InfluxDB源码中Hash分区的个数固定是1,对此还不是很理解,如果哪位看官对此比较熟悉可以指导一二。
本篇文章重点介绍InfluxDB中一些基本概念,为后面分析InfluxDB内核实现奠定一个基础。文章主要介绍了三个重要模块:
1. 首先介绍了InfluxDB中一些基本概念,包括Measurement、Tags、Fields以及Point。
2. 接着介绍了Series这个非常非常重要的概念。
3. 最后重点介绍了InfluxDB中数据的组织形式,总结起来就是:先按照RP划分,不同过期时间的数据划分到不同的RP,同一个RP下的数据再按照时间Range分区形成ShardGroup,同一个ShardGroup中的数据再按照Series进行Hash分区,将数据划分成更小粒度的管理单元。Shard是InfluxDB中实际工作者,是InfluxDB的存储引擎。
InfluxDB 的数据存储主要有三个目录。
默认情况下是 meta, wal 以及 data 三个目录。
meta 用于存储数据库的一些元数据,meta 目录下有一个 meta.db
文件。
wal 目录存放预写日志文件,以 .wal
结尾。data 目录存放实际存储的数据文件,以 .tsm
结尾。这两个目录下的结构是相似的,其基本结构如下:
# wal 目录结构
-- wal
-- mydb
-- autogen
-- 1
-- _00001.wal
-- 2
-- _00035.wal
-- 2hours
-- 1
-- _00001.wal
# data 目录结构
-- data
-- mydb
-- autogen
-- 1
-- 000000001-000000003.tsm
-- 2
-- 000000001-000000001.tsm
-- 2hours
-- 1
-- 000000002-000000002.tsm
其中 mydb 是数据库名称,autogen 和 2hours 是存储策略名称,再下一层目录中的以数字命名的目录是 shard 的 ID 值,比如 autogen 存储策略下有两个 shard,ID 分别为 1 和 2,shard 存储了某一个时间段范围内的数据。再下一级的目录则为具体的文件,分别是 .wal
和 .tsm
结尾的文件。
WAL 文件
wal 文件中的一条数据,对应的是一个 key(measument + tags + fieldName) 下的所有 value 数据,按照时间排序。
TSM 文件
单个 tsm 文件的主要格式如下:
主要分为四个部分: Header, Blocks, Index, Footer。
其中 Index 部分的内容会被缓存在内存中,下面详细说明一下每一个部分的数据结构。
Header
0x16D116D1
。1
。Blocks
Blocks 内部是一些连续的 Block,block 是 InfluxDB 中的最小读取对象,每次读取操作都会读取一个 block。每一个 Block 分为 CRC32 值和 Data 两部分,CRC32 值用于校验 Data 的内容是否有问题。Data 的长度记录在之后的 Index 部分中。
Data 中的内容根据数据类型的不同,在 InfluxDB 中会采用不同的压缩方式,float 值采用了 Gorilla float compression,而 timestamp 因为是一个递增的序列,所以实际上压缩时只需要记录时间的偏移量信息。string 类型的 value 采用了 snappy 算法进行压缩。
Data 的数据解压后的格式为 8 字节的时间戳以及紧跟着的 value,value 根据类型的不同,会占用不同大小的空间,其中 string 为不定长,会在数据开始处存放长度,这一点和 WAL 文件中的格式相同。
Index
Index 存放的是前面 Blocks 里内容的索引。索引条目的顺序是先按照 key 的字典序排序,再按照 time 排序。InfluxDB 在做查询操作时,可以根据 Index 的信息快速定位到 tsm file 中要查询的 block 的位置。
这张图只展示了其中一部分,用结构体来表示的话类似下面的代码:
type BlockIndex struct {
MinTime int64
MaxTime int64
Offset int64
Size uint32
}
type KeyIndex struct {
KeyLen uint16
Key string
Type byte
Count uint32
Blocks []*BlockIndex
}
type Index []*KeyIndex
后面四个部分是 block 的索引信息,根据 Count 中的个数会重复出现,每个 block 索引固定为 28 字节,按照时间排序。
间接索引
间接索引只存在于内存中,是为了可以快速定位到一个 key 在详细索引信息中的位置而创建的,可以被用于二分查找来实现快速检索。
offsets 是一个数组,其中存储的值为每一个 key 在 Index 表中的位置,由于 key 的长度固定为 2字节,所以根据这个位置就可以找到该位置上对应的 key 的内容。
当指定一个要查询的 key 时,就可以通过二分查找,定位到其在 Index 表中的位置,再根据要查询的数据的时间进行定位,由于 KeyIndex 中的 BlockIndex 结构是定长的,所以也可以进行一次二分查找,找到要查询的数据所在的 BlockIndex 的内容,之后根据偏移量以及 block 长度就可以从 tsm 文件中快速读取出一个 block 的内容。
Footer
tsm file 的最后8字节的内容存放了 Index 部分的起始位置在 tsm file 中的偏移量,方便将索引信息加载到内存中。