清华自研时间序列数据库 Apache IoTDB 原理解析
云智慧 AIOps 社区是由云智慧发起,针对运维业务场景,提供算法、算力、数据集整体的服务体系及智能运维业务场景的解决方案交流社区。该社区致力于传播 AIOps 技术,旨在与各行业客户、用户、研究者和开发者们共同解决智能运维行业技术难题,推动 AIOps 技术在企业中落地,建设健康共赢的 AIOps 开发者生态。
智能运维领域的数据特点
指标数据作为运维场景中的重要观测项,是服务可用性监控、系统健康度度量等场景的主要数据来源。从下面架构示意图中们可以看出,采集器采集服务器上各种指标数据,发往消息队列,通过实时流处理和离线计算最终存入到数据库。
在这个上述场景中,我们往往会遇到以下几种数据挑战:
我们日常需要监控的指标数量超百万,峰值时甚至会达到千万级,每天沉淀下来的指标数据量达到 GB 级别,甚至 TB 级别。
针对指标数据的日常分析行为通常涉及到近 1 小时、近 1 天、近 7 天、近 30 天、近 1 年等多种时间跨度。对范围查询的性能有一定要求。
3)在数据传输过程中,由于受到网络、设备资源等原因造成短时间内出现乱序到达、缺丢点、峰谷潮、重复数据等问题
4)由于服务器或设备本身原因,采集的指标数据时间往往不够精准,导致数据粒度不齐整的问题。例如对于秒级别的指标,上一个采集的数据点的时间戳是 2021-01-01 10:00:00:000 下一个数据点的有可能是 2021-01-01 10:00:01:015。而不同的指标相同时刻采集的数据点时间戳分别是 2021-01-01 10:00:00:000 和 2021-01-01 10:00:00:015。
至此整体的需求基本已经明确,在做数据库选型时需要满足以下需求:
1)支持数据长时间存储;
2)支持大时间跨度的快速检索;
3)高速的数据吞吐能力;
4)高效的数据压缩比;
5)能够有效的解决数据的乱序、缺失值、粒度不齐整以及重复数据等数据质量问题。
智能运维领域的时序数据该如何存储
对于上述的需求我们该如何选型?是传统的关系型数据库,还是通用的 NoSQL 数据库,亦或是专用的时序数据库?他们能否满足上述数据库选型的需求?
数据如何存储还要结合数据本身的特点。这里以一个真实场景中的案例,某运营商有约 3000 万的监控指标,并且采集的过程中存在空值数据、数据缺失、数据重复等情况,甚至会出现新的指标。如果一分钟采集一次指标在允许一定数据延迟的情况下,写入速率要超过 50w/s,一天需要存储 432 亿的数据,这对关系数据库来说无论是从写入速率是在查询时效都很难满足需求。
再看看通用的 NoSQL 数据库,首先先简单梳理一下这些指标数据的特点,我们发现这些指标数据除了有时间戳和指标值外还会有一些 tag 来标识数据来自那台机器,通过采集器实际的采集数据的样例如下图所示:
在通用的 NoSql 数据库虽然可以满足吞吐量性能以及查询性能,但是为了满足指标的动态变更我们只能按照一个设备一张表或者多个设备共享一张表的建模方式如下图所示。
无论是一个设备一张表还是多个设备共享一张表的存储方式,为了能够区分数据来哪个指标,我们只能把 tags 作为一列进行存储,不难发现这种这样建表方式会出现大量的 tag 数据冗余存储的问题。并且通用的 NoSql 数据库往往在解决数据重复的问题上并不友好,更多的是依靠一些排重策略来实现。排重策略通常有两种:一种是依靠外部的排重方式达到存储时数据已经排重,另一种是存储不排除查询时依靠 sql 来做排重。如果使用第一种数据排重无疑会增加系统的复杂度,如果使用第二种这会导致在处理过程的出现数据冗余存储的情况。而且通用的 NoSql 数据库还存在一个问题就是:没有原生操作支持粒度卡齐或者线性填充来解决数据质量差的问题。
然而我们上面的遇到的一些数据挑战实际是属于时序数据库要解决的典型问题,市面上也有许多优秀的时序数据库,例如 InfluxDB、Apache IoTDB 等,它们在高吞吐、低延时查询、数据去重、数据填充、数据降采样、高压缩比等功能方面皆能满足第一章中的数据存储需求。接下来我们来重点看下完全开源的 Apache IoTDB 它是如何设计的。
IoTDB 的设计
IoTDB 的架构
IoTDB 是基于 LSM-Tree(Log-Structured Merge Tree)的架构进行设计的列式存储数据库,LSMtree 的核心思想就是放弃部分读的能力来换取最大的写入能力。从下图中 IoTDB 的整体架构图中我们可以看出 IoTDB 主要有三部分构成:分别是数据库引擎、存储引擎和分析引擎。
数据库引擎主要是负责 sql 语句的解析、数据写入、数据查询、数据删除等功能。
存储引擎主要是由 TsFile 来组成也是 IoTDB 的最具特色的设计,它不仅可以为 IoTDB 存储引擎使用,而且还可以直接通过链接器供分析引擎使用,同时还对外开放了 TsFile 的 API,用户也可以自己直接通过 API 来获取里面的内容。
分析引擎主要是用于与开源的数据处理平台对接等。
IoTDB 的数据读写流程
上面提到了 IoTDB 是基于 LSM-Tree 的思想来实现的,从以下数据写入流程图中我们可以看出:数据经过 time detector 时会根据内存中维护的最大时间戳来判断是否数据有序,在内存缓冲区 memtable 中分为有序序列和乱序序列,同时为了保障在断电后数据不丢失,IoTDB 也会把数据写入到 WAL(Write-Ahead Logging)中,到此客户端的数据写入就已经完成。随着数据的不断写入,memtable 中的数据达到一定的程度后,IoTDB 通过 submit flush task 把 memtable 变成 Immutable 最终刷到磁盘变成 Sstable 即 TsFile 文件,同时当持久化的 TsFile 文件达到一定程度会触发合并。
上面介绍了 IoTDB 数据的写入流程后,我们再来看下 IoTDB 核心的查询流程。如下图所示,当客户端发送查询请求时,首先通过 Antlr4 进行 sql 解析,然后去内存中的 MemTable、ImmuTable 和硬盘中 TsFile 中进行查询。当然,IoTDB 这里会通过 BloomFilter 和索引来提高数据的查询效率。我们知道 BloomFilter 的原理是哈希结果不存在那么一定没有此数据,如果哈希结果存在,IoTDB 那么还需要继续借助索引进行进一步的查找。
TsFile 结构
上一节 IoTDB 的读写流程都离不开 TsFile,那我们看看 IoTDB 最核心 TsFile 是怎样一个结构。从下面的 TsFile 结构示意图中可以看出 TsFile 整体分为两部分:一部分是数据区,另一部分是索引区。
数据区主要包括 Page 数据页、Chunk 数据块和 ChunkGroup 数据组。其中 Page 由一个 PageHeader 和一段数据(time-value 编码的键值对)组成,Chunk 数据块由多个 Page 和一个 Chunk Header 组成,ChunkGroup 存储了一个实体(Entity) 一段时间的数据,它由若干个 Chunk, 一个字节的分隔符 0x00 和一个 ChunkFooter 组成。
索引区主要包括 TimeseriesIndex、IndexOfTimeseriesIndex 和 BloomFilter,其中 TimeseriesIndex 包含 1 个头信息和数据块索引(ChunkIndex)列表,头信息记录文件内某条时间序列的数据类型、统计信息(最大最小时间戳等);数据块索引列表记录该序列各 Chunk 在文件中的 offset,并记录相关统计信息(最大最小时间戳等);IndexOfTimeseriesIndex 用于索引各 TimeseriesIndex 在文件中的 offset;BloomFilter 针对实体(Entity)的布隆过滤器。下图中 TsFile 包括两个实体 d1、d2,每个实体分别包含三个物理量 s1、s2、s3,共 6 个时间序列,每个时间序列包含两个 Chunk。
TsFile 索引构建
TsFile 中所有的索引节点构成一棵类 B+树结构的多叉索引树,这棵树由两部分组成:实体索引部分和物理量索引部分。下面举一个例子来展示索引树的构成:假设我们设置树的度为 10,我们有 150 个设备每个设备有 150 个测点共计 22500 条时间序列,这时我们需构建一个深度为 6 的索引树即可,这时我们查询数据所在位置需做 6 次磁盘的 IO,具体如下图所示。
上面这种方式看似磁盘 IO 次数比较多,这是由于我们设置的树的度比较小从而导致整体树的深度比较大。如果我们把树的度增大到 300,在实体索引部分一个高度为 2 的子树即可实现 90000 个设备存储,同理一个高度为 2 个物理量索引部分也可存放 90000 个物理量,最终形成的整个索引树可存放 81 亿条时间序列。这时我们再读取数据只需要做 4 次磁盘 IO 即可定位到我们需要的数据位置。
TsFile 查询流程
在了解了 TsFile 的结构以及索引的构建,那么 IoTDB 是如何在 TsFile 内部完成一次查询的,下面用一个具体查询例如 select s1 from root.ln.d1 where time>100 and time<200,来演示在 TsFile 中是如何定位到所需数据。他的具体步骤以及示意图如下所示:
1)读取 TsFile MetadataSize 信息
2)根据 TsFile MetadataSize 和 offset 获取 TsFile MetaData 的位置
3)读取 Metadata IndexNode 中的数据,通过 MetadataIndexEntry 中的 name 定位到设备 root.ln.d1
4)读取到设备 root.ln.d1 中的 offset 偏移量,根据偏移量找到 TimeSeries Metadata 中的信息进而找到 s1
5)通过 ChunkMetadata 记录的统计信息 startTime 和 endTime 与我们查询的区间(100,200)对比获取到 root.ln.d1 设备下面测点 s1 的偏移量
6)根据 s1 中的偏移量可以直接获取到 ChunkGroup
7)依次通过 ChunkGroupHeader、ChunkHeader 定位到 Chunk 数据依次读取 Page 中的 PageHeader,如果时间区间在(100,200)中那么我们就直接读取 PageData 数据。
总结
本文抛砖引玉简单的介绍了 IoTDB 的读写以及 TsFile 的核心文件的设计,实际上 IoTDB 整体设计和实现还是比较杂,里面涉及了很多的细节这里就不再展开介绍了。
写在最后
近年来,在 AIOps 领域快速发展的背景下,IT 工具、平台能力、解决方案、AI 场景及可用数据集的迫切需求在各行业迸发。基于此,云智慧在 2021 年 8 月发布了 AIOps 社区, 旨在树起一面开源旗帜,为各行业客户、用户、研究者和开发者们构建活跃的用户及开发者社区,共同贡献及解决行业难题、促进该领域技术发展。
社区先后 开源 了数据可视化编排平台-FlyFish、运维管理平台 OMP 、云服务管理平台-摩尔平台、 Hours 算法等产品。
可视化编排平台-FlyFish:
项目介绍:https://www.cloudwise.ai/flyFish.html
Github 地址: https://github.com/CloudWise-OpenSource/FlyFish
Gitee 地址: https://gitee.com/CloudWise/fly-fish
行业案例:https://www.bilibili.com/video/BV1z44y1n77Y/
部分大屏案例:
请您通过上方链接了解我们,添加小助手(xiaoyuerwie)备注:飞鱼。加入开发者交流群,可与业内大咖进行 1V1 交流!
也可通过小助手获取云智慧 AIOps 资讯,了解云智慧 FlyFish 最新进展!
评论