时序数据TimeSeries是一连串随时间推移而发生变化的相关事件
常见的时序数据有:
此类数据的特点:
当面对时序数据时,传统的关系型数据就显得力不从心
关系型数据模型 | 时序数据库需求 |
---|---|
数据按住建索引组织、存储 | 数据按时间戳进行组织、存储,便于按时间维度查询 |
数据持久后永久存在 | 数据具有的生命周期,定期清理过期数据 |
支持复杂的OLTP功能(点查、改、删) | 支持的OLAP操作(基于时间窗口) |
并发修改加锁,提供事务保证 | Last Write Win解决写冲突,无需事务 |
除此之外,关系型数据库还存在以下天然短板:
需要通过分表分库sharding实现横向扩展
分库分表引入额外复杂度,需要维护映射关系。 此时 SQL 语言的查询优势不复存在,多数查询都会退化为 KV 查找
写时模式schema on write灵活度不足 关系数据库新增字段属于 DDL 操作,会导致锁表锁库。 频繁的库表变更影响系统稳定,无法适应快速的业务变更。
注意,修改表结构的操作,MySQL8.0之前的版本默认是会锁表的,所以,不要在业务高峰期做这些操作,而是选择在晚间进行操作,当然,我们也可以在晚间使用在线的DDL工具来处理这些事情,避免锁表对业务造成影响。
常用的在线DDL工具
存储时序数据库的另一个挑战就是其夸张的数据生成速度。以用户行为数据为例,如果一个接口的QPS是1万。就意味着一秒钟内会生成1万条用户行为记录。假设这样的接口有100个,那么每秒钟生成的记录数可达100万。
一种解决方式是将消息积压至 Kafka 这类中间件,然后进行异步处理。但对于某些业务场景来说,这一处理方式则显得不合时宜。以股票成交数据为例,为了保障行情的时效性,无法采用异步批处理的方式实现。为了实现极高的写入吞吐量,通常会考虑使用 Redis 实现这一功能。
然而这一方案也存在以下问题:
时序数据库是一类专门用于存储时序数据的数据管理系统,这类数据库的设计思想大致可以总结为下面几条:
类似于关系型数据库,时序数据库也有自己的数据模型,并且两者直接存在不少相似之处:
关系模型 | 时序模型 | 含义 |
---|---|---|
table | metric/measurement | 表——指标(时间序列) |
column | value/field | 列——值(无索引序列) |
index | tag | 索引——标签(索引列) |
row | point | 记录行——数据点(时间序列中某个时刻的数据) |
primary key | timestamp | 行主键——点时间戳(时间序列内唯一标识) |
其中 tag 的概念较为重要:
OpenTSDB 是一种基于 HBase 来构建的分布式、可扩展的时间序列数据库。OpenTSDB 被广泛应用于存储、索引和服务从大规模计算机系统(网络设备、操作系统、应用程序)采集来的监控指标数据,并且使这些数据易于访问和可视化。
OpenTSDB 由时间序列守护程序 (TSD) 以及一组命令行实用程序组成。每个 TSD 都是独立的。 没有主节点,没有共享状态。
优点
TSD 是无状态的,所有状态数据保存在 HBase 中,天然支持水平扩展
缺点
时序数据库 InfluxDB 是一款专门处理高写入和查询负载的时序数据库,基于 InfluxDB 能够快速构建具有海量时序数据处理能力的分析和监控软件。该项目的发起者是 influxdata 公司,该公司提供了一套用于处理时序数据的完整解决方案,InfluxDB 是该解决方案中的核心产品。
优点
缺点
InfluxDB 的数据模型已经很接近传统的关系模型:
database | 命名空间,相互隔离 |
---|---|
retention policy | 保存策略,定义数据生命周期 |
bucket | database+retention policy |
measurement | 相关时间序列集合 |
tag | 标签索引,索引列 |
field | 时序数据,无索引列 |
point | 数据点,时间序列中某个时刻的数据 |
time | 时间戳,时间序列内唯一标识 |
保留策略retention policy 用于管理数据生命周期,其中包含了:
持续时间duration:指定了数据保留时间,过期的数据将自动从数据库中删除
副本个数replication factor:指定了集群模式下,需要维护数据副本的个数(仅在集群模式下有效)
分片粒度hard duration):指定了 shard group 的时间跨度(影响数据分片大小)
保留策略与 database 存在以下关系:
这意味着:
同个 measurement 可能存在两个有着完全相同的 time 的 point。为了解决数据重复的问题,InfluxDB 2 引入了一个 bucket 的概念,用于避免这一情况。
时间序列 Series 在 InfluxDB 中也是个核心概念:
series key | measurement+tag+field key |
---|---|
series | 时间序列,serial key相同的数据集合 |
seriescardinality | 序列基数,series key的数量 |
为了唯一标识一个时间序列,InfluxDB 引入了 Serieskey 的概念:
每个数据点都有 Serieskey,Serieskey 相同的数据点属于同个时间序列,会存储在同一个文件中,方便后续查询
Serieskey 的数量称为序列基数 series cardinality:
序列基数是一个重要的性能指标,InfluxDB 会为每个 Serieskey 在内存中维护一个索引记录,因此序列基数能够直观反映当前数据的内存压力
上图的 series cardinality 为 4,其中包含以下 series key:
measurement + tags + field |
---|
census, location=1, scientist=langstroth, butterflier |
census, location=1, scientist=langstroth, honeybees |
census, location=1, scientist=perpetual, butterflier |
census, location=1, scientist=perpetual, honeybees |
注意:即便两条记录的 measurement、time、tag、field 完全一致,但只要使用的是不同的 RP,那么它们就属于不同的 series,会被存储在不同的 bucket 中。
InfluxDB 提供了两种查询语言:
sql# 创建数据库
CREATE DATABASE "sample_data"
USE sample_data
# 插入样例数据,格式参考:https://docs.influxdata.com/influxdb/v1.8/write_protocols/line_protocol_tutorial
INSERT census,location=1,scientist=langstroth butterflies=12i,honeybees=23i 1439827200000000000
INSERT census,location=1,scientist=perpetua butterflies=1i,honeybees=30i 1439827200000000000
INSERT census,location=1,scientist=langstroth butterflies=11i,honeybees=28i 1439827560000000000
INSERT census,location=1,scientist=perpetua butterflies=3i,honeybees=28i 1439827560000000000
INSERT census,location=2,scientist=langstroth butterflies=2i,honeybees=11i 1439848440000000000
INSERT census,location=2,scientist=langstroth butterflies=1i,honeybees=10i 1439848800000000000
INSERT census,location=2,scientist=perpetua butterflies=8i,honeybees=23i 1439849160000000000
INSERT census,location=2,scientist=perpetua butterflies=7i,honeybees=22i 1439849520000000000
# 显示数据库中的表
# measurement 无需预先定义,由 InfluxDB 动态创建
SHOW MEASUREMENTS
# 显示数据库中的 field key
SHOW FIELD KEYS
# 显示数据库中的 tag key
SHOW TAG KEYS
# 显示数据库中的 tag value
SHOW TAG VALUES WITH KEY = scientist
# 查询所有数据
SELECT * FROM census;
# 对 location = 1 的数据求和
SELECT SUM(butterflies) AS butterflies, SUM(honeybees) AS honeybees FROM census WHERE location = '1';
# 删除 location = 1 的数据
DELETE FROM census WHERE location = '1';
SELECT * FROM census;
# 更新特定数据
SELECT * FROM census;
INSERT census,location=2,scientist=perpetua butterflies=10i,honeybees=50i 1439849520000000000
SELECT * FROM census;
# 更新数据时要保证数据类型一致,否则会报错
INSERT census,location=2,scientist=perpetua butterflies=abc,honeybees=efg 1439849520000000000
# 删除数据库
DROP DATABASE sample_data;
Flux 无命令行支持,只能通过 http 接口请求。有兴趣可以参考下面的脚本,动手尝试一下:
curl -XPOST 127.0.0.1:8086/api/v2/query -sS \ -H 'Accept:application/csv' \ -H 'Content-type:application/vnd.flux' \ -H 'Authorization: Token root:123456' \ -d ' from(bucket:"sample_data") |> range(start:time(v: 1439827200000), stop:time(v: 143984952000)) |> filter(fn:(r) => r._measurement == "census" and r.location == "1" and (r._field == "honeybees" or r._field == "butterflies")) |> limit(n: 100)'
在了解完查询语言之后,接下来看看 InfluxDB 的整体架构:
上图将 InfluxDB 分为了 4 层,上面 database 与 RP 这两层之前已经介绍过,我们重点关注下面两层:
shard | 存储的时序数据的磁盘文件 |
---|---|
shard group | shard 容器,责管理数据的生命周期,清除过期的数据 |
shard duration | shard duration |
由于时序数据的数据量通常十分巨大,因此 InfluxDB 的设计中引入了分片的策略。并且同时采用了两种分片策略: |
整个数据的写入流程简化为 3 个步骤:
预写日志Write-Ahead-Log是一种常见的提高数据库优化手段,能够在保证数据安全的同时,提升系统的写入性能。 InfluxDB WAL 由一组定长的 segement 文件构成,每个文件大小约为 10MB。这些 segment 文件只允许追加,不允许修改。
Cache 是 WAL 的一个内存快照,保证 WAL 中的数据对用户实时可见。 当 Cache 空闲或者过满时,对应的 WAL 将被压缩并转换为 TSM,最终释放内存空间。 每次重启时会根据 WAL 重新构造 Cache。
TSM 是一组存储在磁盘上的外存索引文件,细节将在后续进行介绍。
它们之间的关系可以简单描述为:
本文作者:Eric
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!