InfluxDB 是什么
InfluxDB 是用Go语言编写的一个开源分布式时序、事件和指标数据库,无需外部依赖。
InfluxDB在DB-Engines的时序数据库类别里排名第一。
重要特性极简架构:单机版的InfluxDB只需要安装一个binary,即可运行使用,完全没有任何的外部依赖。
极强的写入能力:底层采用自研的TSM存储引擎,TSM也是基于LSM的思想,提供极强的写能力以及高压缩率。
高效查询:对Tags会进行索引,提供高效的检索。
InfluxQL:提供SQL-Like的查询语言,极大的方便了使用,数据库在易用性上演进的终极目标都是提供Query Language。
Continuous Queries: 通过CQ能够支持auto-rollup和pre-aggregation,对常见的查询操作可以通过CQ来预计算加速查询。
存储引擎: 从LSM 到 TSM
InfluxDB 采用自研的TSM (Time-Structured Merge Tree) 作为存储引擎, 其核心思想是通过牺牲掉一些功能来对性能达到极致优化,其官方文档上有项目存储引擎经历了从LevelDB到BlotDB,再到选择自研TSM的过程,整个选择转变的思考。
时序数据库的需求数十亿个单独的数据点
高写入吞吐量
高读取吞吐量
大型删除(数据过期)
主要是插入/追加工作负载,很少更新
LSM 的局限性
在官方文档上有写, 为了解决高写入吞吐量的问题, Influxdb 一开始选择了LevelDB 作为其存储引擎。 然而,随着更多地了解人们对时间序列数据的需求,influxdb遇到了一些无法克服的挑战。
LSM (日志结构合并树)为 LevelDB的引擎原理, 具体细节可以参考。 LSM 树原理详解levelDB 不支持热备份。 对数据库进行安全备份必须关闭后才能复制。LevelDB的RocksDB和HyperLevelDB变体可以解决此问题。
时序数据库需要提供一种自动管理数据保存的方式。 即删除过期数据, 而在levelDB 中,删除的代价过高。(通过添加墓碑的方式, 段结构合并的时候才会真正物理性的删除)。
InfluxDB 的解决方案 - TSM
按不同的时间范围划分为不同的分区(Shard),因为时序数据写入都是按时间线性产生的,所以分区的产生也是按时间线性增长的,写入通常是在最新的分区,而不会散列到多个分区。分区的优点是数据回收的物理删除非常简单,直接把整个分区删除即可。在最开始的时候, influxdb 采用的方案每个shard都是一个独立的数据库实例,底层都是一套独立的LevelDB存储引擎。 这时带来的问题是,LevelDB底层采用level compaction策略,每个存储引擎都会打开比较多的文件,随着shard的增多,最终进程打开的文件句柄会很快触及到上限。
由于遇到大量的客户反馈文件句柄过多的问题,InfluxDB在新版本的存储引擎选型中选择了BoltDB替换LevelDB。BoltDB底层数据结构是mmap B+树。 但由于B+ 树会产生大量的随机写。 所以写入性能较差。
之后Influxdb 最终决定仿照LSM 的思想自研TSM ,主要改进点是基于时序数据库的特性作出一些优化,包含Cache、WAL以及Data File等各个组件,也会有flush、compaction等这类数据操作。
InfluxDB 系统架构
DataBase: 用户可以通过 create database xxx 来创建一个database。
Retention Policy(RP):数据保留策略, 可用来规定数据的的过期时间。
CREATE RETENTION POLICY "a_year" ON "food_data" DURATION 52w REPLICATION 1 SHARD DURATION 1h
上述语句可以创建一个保存周期为52周的RP。REPLICATION 1 表示副本数量为1。 ”SHARD DURATION”指定每个Shard Group对应多长时间。
一个数据库可以用多个RP , 不同的表可以设置不同的RP。
Shard Group :实现了数据分区,但是Shard Group只是一个逻辑概念,在它里面包含了大量Shard,Shard才是InfluxDB中真正存储数据以及提供读写服务的概念。
Share:结构示意图如下:
Share 就是上面章节所介绍的TSM 引擎, 负责把数据写入文件。
具体的过程类似于LSM,数据来了先存到cashe, 等cashe 大小到一定程度就会异步flush 到TSM文件。 同时WAL(Write Ahead Log) 用来预防数据丢失。
TSM 原理
先来看一下Influxdb 数据模型:
重要概念:Measurement : 类似于mysql 的表名。 (census)
tag key:类似于mysql 加了索引的列名
Field key : 类似于mysql 没加索引的列名
Point:类似SQL中一行记录,而并不是一个点。
insert census,location=1,scientist=langstroth butterflies=12,honeybees=23 1435362189575692182
//向census 插入一条数据,location,scientist 为 tag key ,butterflies和honeybees 为filed key ,tag 和filed 之间空格间隔。 1435362189575692182为时间戳, 可省略。
show series from census
InfluxDB 核心概念 –Series
时序数据的时间线就是一个数据源采集的一个指标随着时间的流逝而源源不断地吐出数据,这样形成的一条数据线称之为时间线。
InfluxDB在时序数据模型设计方面提出了一个非常重要的概念:SeriesKey。SeriesKey实际上就是measurement+datasource(tags)。时序数据写入内存之后按照SeriesKey进行组织。
时序数据在内存中表示为一个Map:>, 其中Key = seriesKey + fieldKey。这个Map执行flush操作形成TSM文件。
TSM 文件结构
TSM文件最核心的由Series Data Section以及Series Index Section两个部分组成,其中前者表示存储时序数据的Block,而后者存储文件级别B+树索引Block,用于在文件中快速查询时间序列数据块。
Series Data Block
Map中一个Key对应一系列时序数据,因此能想到的最简单的flush策略是将这一系列时序数据在内存中构建成一个Block并持久化到文件。然而,有可能一个Key对应的时序数据非常之多,导致一个Block非常之大,超过Block大小阈值,因此在实际实现中有可能会将同一个Key对应的时序数据构建成多个连续的Block。但是,在任何时候,同一个Block中只会存储同一种Key的数据。
另一个需要关注的点在于,Map会按照Key顺序排列并执行flush,这是构建索引的需求。Series Data Block文件结构如下图所示:
Series Index Block
每个key 对应一个Index Block。
很多时候用户需要根据Key查询某段时间(比如最近一小时)的时序数据,如果没有索引,就会需要将整个TSM文件加载到内存中才能一个Data Block一个Data Block查找,这样一方面非常占用内存,另一方面查询效率非常之低。为了在不占用太多内存的前提下提高查询效率,TSM文件引入了索引,其实TSM文件索引和HFile文件索引基本相同。TSM文件索引数据由一系列索引Block组成,每个索引Block的结构如下图所示:
Series Index Block由Index Block Meta以及一系列Index Entry构成:Index Block Meta最核心的字段是Key,表示这个索引Block内所有IndexEntry所索引的时序数据块都是该Key对应的时序数据。
Index Entry表示一个索引字段,指向对应的Series Data Block。指向的Data Block由Offset唯一确定,Offset表示该Data Block在文件中的偏移量,Size表示指向的Data Block大小。Min Time和Max Time表示指向的Data Block中时序数据集合的最小时间以及最大时间,用户在根据时间范围查找时可以根据这两个字段进行过滤。
TSM引擎工作原理-时序数据读取
基于对TSM文件的了解,在一个文件内部根据Key查找一个某个时间范围的时序数据就会变得很简单,整个过程如下图所示:
上图中中间部分为索引层,TSM在启动之后就会将TSM文件的索引部分加载到内存,数据部分因为太大并不会直接加载到内存。用户查询可以分为三步:首先根据Key找到对应的SeriesIndex Block,因为Key是有序的,所以可以使用二分查找来具体实现。
找到SeriesIndex Block之后再根据查找的时间范围,使用[MinTime, MaxTime]索引定位到可能的Series Data Block列表。
将满足条件的Series Data Block加载到内存中解压进一步使用二分查找算法根据timestamp查找即可找到。
多维查询之倒排索引
上个章节讲的是如何根据key 查到数据。 然而,在实际场景中, 经常会有根据表名和 部分tag 来查询数据的场景。 比如最开始的那个表, 用户要查cenus 表 location=1 在一周内的所有butterflies 总数。 这种的话如果tag 和measurement 不做索引的话,则查询方式只能是遍历对比。
InfluxDB给出了倒排索引的实现,称之为TimeSeries Index,意为TimeSeries的索引,简称TSI。InfluxDB TSI在1.3版本之前仅支持Memory-Based Index实现,1.3之后又实现了Disk-Based Index实现。
Memory-Based Index
Memory-Based Index方案将所有TimeSeries索引加载到内存提供服务,核心数据结构主要有:seriesByID:通过SeriesID查询Series的一个Map。
seriesByTagKeyValue:双层Map,第一层是TagKey对应其所有的TagValue,第二层是TagValue对应的所有Series的ID。可以看到,当TimeSeries的基数变得很大,这个map所占的内存会相当多。
sortedSeriesIDs:一个排序的SeriesID列表。
Q:考虑下如何查询多个tag 纬度下的series ID?A:答案是拉出多个 seriesID List 求交集。
Disk-Based Index
Disk-baesd index 的实现思路和TSM file 的实现原理一样。 先写入WAL 预写日志,然后数据存入cashe , 当cashe 积攒到一定大小后, flush 到文件。
Q: 新插入一条数据, 如何判断series key 是否已经存在? A:Bloom Filter
常用操作
下载 :
wget https://dl.influxdata.com/influxdb/releases/influxdb_1.7.9_amd64.deb
sudo dpkg -i influxdb_1.7.9_amd64.deb
启动:
./influxd 启动influxdb 服务器。 ./influx 启动shell 客户端。
远程连接服务端 ./influx -host XXX -port xx
创建数据库:
create database monit
use monit
插入数据:
insert census,location=1,scientist=langstroth butterflies=12,honeybees=23
//无需先创建measurement
查询数据:
select * from census
http 接口访问:
curl -G 'http://localhost:8086/query?pretty=true' --data-urlencode "db=mydb" --data-urlencode "q=SELECT \"value\" FROM \"cpu_load_short\" WHERE \"region\"='us-west'"
q= 除了可以接查询语句, 还可以接建RP, 建CQ , 插入等等操作
支持同时多个点写入和同时多个查询
curl -i -XPOST 'http://localhost:8086/write?db=mydb' --data-binary 'cpu_load_short,host=server02 value=0.67cpu_load_short,host=server02,region=us-west value=0.55 1422568543702900257
cpu_load_short,direction=in,host=server01,region=us-west value=2.0 1422568543702900257'
支持连续查询(Continuous Queries简称CQ) 来处理数据采样。
假设我有一个数据库monitor , 里面有一张表monit.biz.douyin 记录抖音的数据打点,tag(metric,province,carrier)。 我之前创建了一个名叫3_month的RP ,我想按照metric 和province 两个tag聚合每分钟抖音的打点数量 。建立CQ 如下
CREATE CONTINUOUS QUERY "douyin_1min_sum" ON "monitor" BEGIN SELECT sum("count") INTO monitor."3_month"."monit.biz.douyin.1min" FROM monitor."default"."monit.biz.douyin" GROUP BY time(1m),metric,province END
monit.biz.douyin.1min 就是聚合完的新表。
参考文章: