时序数据处理流行工具分析。

主流TSDB分析

InfluxDB

InfluxDB[9]是一个一站式的时序工具箱,包括了时序平台所需的一切:多租户时序数据库、UI和仪表板工具、后台处理和监控数据采集器。如下图9所示。

图9

InfluxDB支持动态shcema,也就是在写入数据之前,不需要做schema定义。因此,用户可以随意添加measurement、tag和field,可以是任意数量的列。

InfluxDB底层的存储引擎经历了从LevelDB到BlotDB,再到自研TSM的历程。从v1.3起,采用的是基于自研的WAL + TSMFile + TSIFile方案,即所谓的TSM(Time-Structured Merge Tree)引擎。其思想类似LSM,针对时序数据的特性做了一些特殊优化。TSM的设计目标一是解决LevelDB的文件句柄过多问题,二是解决BoltDB的写入性能问题。

Cache: TSM的Cache与LSM的MemoryTable类似,其内部的数据为WAL中未持久化到TSM File的数据。若进程故障failover,则缓存中的数据会根据WAL中的数据进行重建。

WAL (Write Ahead Log) : 时序数据写入内存之后按照SeriesKey进行组织。数据会先写入WAL,再写入memory-index和cache,最后刷盘,以保证数据完整性和可用性。基本流程包括:根据Measurement和TagKV拼接出乎series key;检查该series key是否存在;如果存在,就直接将时序数据写入WAL、时序数据写缓存;如果不存在,就会在Index WAL中写入一组entry;依据series key所包含的要素在内存中构建倒排索引、接着把时序数据写入WAL和缓存。

TSM Files: TSM File与LSM的SSTable类似。在文件系统层面,每一个TSMFile对应了一个 Shard,单个最大2GB。一个TSM File中,有存放时序数据(i.e Timestamp + Field value)的数据区,有存放Serieskey和Field Name信息的索引区。基于 Serieskey + Fieldkey构建的形似B+tree的文件内索引,通过它就能快速定位到时序数据所在的数据块。在TSMFile中,索引块是按照 Serieskey + Fieldkey 排序 后组织在一起的。

TSI Files:用户如果没有按预期根据Series key来指定查询条件,比如指定了更加复杂的查询条件,技术手段上通常是采用倒排索引来确保它的查询性能的。由于用户的时间线规模会变得很大,会造成倒排索引消耗过多的内存。对此InfluxDB引入了TSIfiles。TSIFile的整体存储机制与TSMFile相似,也是以 Shard 为单位生成一个TSIFile。

在InfluxDB中有一个对标传统RDB的Database概念。逻辑上每个Database下面可以有多个measurement。在单机版中每个Database实际对应了一个文件系统的目录。

InfluxDB作为时序数据库,必然包括时序数据存储和一个用于度量、标记和字段元数据的倒排索引,以提供更快速的多维查询。InfluxDB在TSMFile上按照下面的步骤扫描数据,以获得高性能查询效果:

• 根据用户指定的时间线(Serieskey)和FieldKey在索引区找到Serieskey+FieldKey所在的 索引数据块。

• 根据用户指定的时间戳范围在索引数据块中查找数据对应在哪个或哪几个索引条目

• 把检索出的索引条目所对应的时序数据块加载到内存中进一步扫描获得结果

和 Prometheus 一样,InfluxDB 数据模型有键值对作为标签,称为标签。InfluxDB 支持分辨率高达纳秒的时间戳,以及 float64、int64、bool 和 string 数据类型。相比之下,Prometheus 支持 float64 数据类型,但对字符串和毫秒分辨率时间戳的支持有限。InfluxDB 使用日志结构合并树的变体来存储带有预写日志,按时间分片。这比 Prometheus 的每个时间序列的仅附加文件方法更适合事件记录。

Prometheus

下图是Prometheus官方架构图[10],包括了一部分生态组件,它们大多是可选项。其中最核心的是Prometheus 服务器,它负责抓取和存储时间序列数据,并对这些数据应用规则,从而聚合出新的时间序列或生成警报,并记录存储它们。

图10

TSDB是Prometheus的关键内核引擎。最新的V3引擎[7]相当于面向TSDB场景优化后的LSM(Log Structured Merge Tree,结构化合并树)。LSM树核心思想的核心就是放弃部分读能力,换取写入的最大化能力;其假设前提就是内存足够大。通常LSM-tree适用于索引插入比检索更频繁的应用系统。V3存储引擎也采用了Gorilla论文思想。它包括了下面几个TSDB组件:块头(Head Block)、预写日志WAL及检查点、磁盘上Chunk头的内存映射、持久化block及其索引和查询模块。下图11是Prometheus的磁盘目录结构。

图11

在V3引擎中一个chunk内会包含很多的时间线(Time series)。Chunk目录下的样本数据分组为一个或者多个段(segment),每个缺省不超过512MB。index是这个block下的chunk目录中的时间线按照指标名称和标签进行索引,从而支持根据某个label快速定位到时间线以及数据所在的chunk。meta.json是一个简单的关于block数据和状态的一个描述文件。Chunks_head负责对chunk索引,uint64索引值由文件内偏移量(下4个字节)和段序列号(上4个字节)组成。

Prometheus将数据按时间维度切分为多个block。只有最近的一个block允许接收新数据。最新的block要写入数据,会先写到一个内存结构中,为了保证数据不丢失,会先写一份预写日志文件WAL,按段(大小128MB )存储在目录中。它们是未压缩的原始数据,因此文件大小明显大于常规块文件。Prometheus 将保留三个或者多个WAL文件,以便保留至少两个小时的原始数据。

V3引擎将2个小时作为块(block)的默认块持续时间;也就是块按2h跨度来分割(这是个经验值)。V3也是采用了LSM一样的compaction策略来做查询优化,把小的block合并为大的block。对于最新的还在写数据的block,V3引擎则会把所有的索引全部hold在内存,维护一个内存结构,等到该block关闭再持久化到文件。针对内存热数据查询,效率非常高。

Prometheus官方再三强调了它的本地存储并不是为了持久的长期存储;外部解决方案提供延长的保留时间和数据持久性。社区有多种集成方式尝试解决这个问题。比如Cassandra、DynamoDB等。

通过指标实现应用的可观察性(observability)是IT监控运维系统的第一步。指标提供汇总视图,再结合日志提供的有关每个请求或事件的明细信息。这样更容易帮助问题的发现与诊断。

Prometheus 服务器彼此独立运行,仅依赖其本地存储来实现其核心功能:抓取、规则处理和警报。也就是说,它不是面向分布式集群的;或者说当前它的分布式集群能力是不够强大的。社区的Cortex、Thanos等开源项目就是针对Prometheus的不足而涌现出来的成功解决方案。

Druid

Druid[11]是有名的实时OLAP分析引擎。Druid的架构设计比较简洁(如下图12)。集群中节点分3类:Master节点、Query节点和Data节点。

图12

Druid数据存储在datasource中,类似于传统RDBMS中的表(table)。每个datasource都按时间(其他属性也可以)分区(partition)。每个时间范围称为一个“块(Chunk)”(例如,一天,如果您的数据源按天分区)。在一个Chunk内,数据被分成一个或多个 “段(Segment)”。每个段都是一个文件,通常包含多达几百万行数据。如下图13所示。

图13

Segment的目的在于生成紧凑且支持快速查询的数据文件。这些数据在实时节点MiddleManager上产生,而且可变的且未提交的。在这个阶段,主要包括了列式存储、bitmap索引、各种算法进行压缩等。这些Segment(热数据)会被定期提交和发布;然后被写入到DeepStorage(可以是本地磁盘、AWS的S3,华为云的OBS等)中。Druid与HBase类似也采用了LSM结构,数据先写入内存再flush到数据文件。Druid编码是局部编码,是文件级别的。这样可以有效减小大数据集合对内存的巨大压力。这些Segment数据一方面被MiddleManager节点删除,一方面被历史节点(Historical)加载。与此同时,这些Segment的条目也被写入元数据(Metadata)存储。Segment的自描述元数据包括了段的架构、其大小及其在深度存储中的位置等内容。这些元数据被协调器(Coordinator)用来进行查询路由的。

Druid 将其索引存储在按时间分区的Segment文件中。Segment文件大小推荐在 300MB-700MB 范围内。Segment文件的内部结构,它本质上是列存的:每一列的数据被布置在单独的数据结构中。通过单独存储每一列,Druid 可以通过仅扫描查询实际需要的那些列来减少查询延迟。共有三种基本列类型:时间戳列、维度列和指标列,如下图14所示:

图14

维度(Dimension)列需要支持过滤和分组操作,所以每个维度都需要以下三种数据结构:

1) 将值(始终被视为字符串)映射到整数 ID 的字典,

2) 列值的列表,使用 1 中的字典编码。 【服务于group by和TopN查询】

3) 对于列中的每个不同值,一个指示哪些行包含该值的位图(本质就是倒排索引,inverted index)。【服务于快速过滤,方便AND和OR运算】

Druid中每列存储包括两部分:Jackson 序列化的 ColumnDescriptor和该列的其余二进制文件。 Druid强烈推荐默认使用LZ4压缩字符串、long、float 和 double 列的值块,使用Roaring压缩字符串列和数字空值的位图。尤其在高基数列场景中匹配大量值的过滤器上Roaring 压缩算法要快得多(对比CONCISE 压缩算法)。

值得一提的是Druid支持Kafka Indexing Service插件(extension),实现实时摄入(ingestion)任务,那么此时可以立即查询该segment,尽管该segment并没有发布(publish)。这更能满足对数据的产生到可查询可聚合分析的实时性要求。

Druid另外一个重要特性就是在数据写入的时候,可以开启rollup功能,将选定的所有dimensions 按照你指定的最小时间间隔粒度(比如1分钟,或者5分钟等)进行聚合。这样可以极大的减少需要存储的数据大小,缺点是原始的每条数据就被丢弃了,不能进行明细查询了。

Druid为了让查询更高效,有如下设计考虑。

• Broker修剪每个查询访问哪些Segment:它是 Druid 限制每个查询必须扫描的数据量的重要方式。首先查询先进入Broker,Broker 将识别哪些段具有可能与该查询有关的数据。然后,Broker 将识别哪些Historian和 MiddleManager正在为这些段提供服务,并向这些进程中的每一个发送重写的子查询。Historical/MiddleManager 进程将接收查询、处理它们并返回结果。Broker 接收结果并将它们合并在一起以获得最终答案,并将其返回给原始调用者。

• Segment内利用索引过滤:每个Segment内的索引结构允许 Druid 在查看任何数据行之前确定哪些(如果有)行与过滤器集匹配。

• Segment内只读取特定关联的行和列:一旦 Druid 知道哪些行与特定查询匹配,它只会访问该查询所需的特定列。在这些列中,Druid可以从一行跳到另一行,避免读取与查询过滤器不匹配的数据。

时间戳也是Druid数据模型的必备项。尽管Druid不是时序数据库,但它也是存储时序数据的自然选择。Druid数据模型可以支持在同一个datasource中,可以同时存放时序数据和非时序数据。因此,Druid不认为数据点是“时间序列”的一部分,而是将每个点单独处理以进行摄入和聚合。比如正统的TSDB支持的时序数据插值计算,在Druid中就不复存在的必要了。这会给一些业务场景的处理带来很大的便利性。

IoTDB

Apache IoTDB[12] 始于清华大学软件学院,2020年9月为 Apache 孵化器项目。IoTDB 是一个用于管理大量时间序列数据的数据库,它采用了列式存储、数据编码、预计算和索引技术,具有类 SQL 的接口,可支持每秒每节点写入数百万数据点,可以秒级获得超过数万亿个数据点的查询结果。主要面向工业界的IoT场景。

IoTDB 套件由若干个组件构成,共同形成数据收集、数据摄入、数据存储、数据查询、数据可视化、数据分析等一系列功能。如下图15所示:

图15

IoTDB 特指其中的时间序列数据库引擎;其设计以设备、传感器为核心,为了方便管理和使用时序数据,增加了存储组(storage group的概念)。

存储组(Storage Group): IoTDB提出的概念,类似于关系数据库中的Database的概念。一个存储组中的所有实体的数据会存储在同一个文件夹下,不同存储组的实体数据会存储在磁盘的不同文件夹下,从而实现物理隔离。对IoTDB内部实现而言,存储组是一个并发控制和磁盘隔离的单位,多个存储组可以并行读写。对用户而言,方便了对设备数据的分组管理和方便使用。

设备 (Device):对应现实世界中的具体物理设备,比如飞机发动机等。在IoTDB中, device是时序数据一次写入的单位,一次写入请求局限在一个设备中。

传感器(Sensor): 对应现实世界中的具体物理设备自身携带的传感器,例如:风力发电机设备上的风速、转向角、发电量等信息采集的传感器。在IoTDB中,Sensor也称为测点(Measurement)。

测点/物理量(Measurement,也称工况、字段 field):一元或多元物理量,是在实际场景中传感器采集的某时刻的测量数值,在IoTDB内部采用<time, value>的形式进行列式存储。 IoTDB存储的所有数据及路径,都是以测点为单位进行组织。测量还可以包含多个分量(SubMeasurement),比如GPS 是一个多元物理量,包含 3 个分量:经度、维度、海拔。多元测点通常被同时采集,共享时间列。

IoTDB的存储由不同的存储组构成。每个存储组是一个并发控制和资源隔离单位。每个存储组里面包括了多个Time Partition。其中,每个存储组对应一个WAL预写日志文件和TsFile时序数据存储文件。每个Time Partition中的时序数据先写入Memtable,同时记入WAL,定时异步刷盘到TsFile。这就是所谓的tLSM时序处理算法。

摄入性能方面:IoTDB 具有最小的写入延迟。批处理大小越大,IoTDB 的写入吞吐量就越高。这表明 IoTDB 最适合批处理数据写入方案。在高并发方案中,IoTDB 也可以保持吞吐量的稳定增长(受网卡、网络带宽约束)。

聚合查询性能方面:在原始数据查询中,随着查询范围的扩大,IoTDB 的优势开始显现。因为数据块的粒度更大,列式存储的优势体现出来,所以基于列的压缩和列迭代器都将加速查询。在聚合查询中,IoTDB使用文件层的统计信息并缓存统计信息。多个查询只需要执行内存计算,聚合性能优势明显。

数据存储对比

基于前面的分析,我们尝试用下面的表格对比来说明这些时序数据处理系统的特点。

表3

对于时序数据的处理,关键能力主要包括数据模型定义、存储引擎、与存储紧密协作的查询引擎和支持分区扩展的架构设计。主流的TSDB基本都是基于LSM或者结合时序数据场景专门优化的LSM tree来实现(包括InfluxDB号称的TSM,IoTDB的tLSM,本质上都还是LSM机制)。其中只有IoTDB独创采用了tree schema来对时序数据建模。为了追求极致性能和极致成本,大家都在针对海量数据和使用场景,持续改进和优化数据的存储结构设计、各种高效索引机制、和查询效率。从单点技术或者关键技术上来讲,有趋同性和同质化的大趋势。

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java