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官方架构图[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[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中就不复存在的必要了。这会给一些业务场景的处理带来很大的便利性。
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来对时序数据建模。为了追求极致性能和极致成本,大家都在针对海量数据和使用场景,持续改进和优化数据的存储结构设计、各种高效索引机制、和查询效率。从单点技术或者关键技术上来讲,有趋同性和同质化的大趋势。
除了开源社区推陈出新外,国内外众多云服务厂商也陆续发布了相关的时序数据库产品或者服务。
华为云的GaussDB for Influx[13]云服务,基于InfluxDB进行深度优化改造,在架构、性能和数据压缩等方面进行了技术创新,取得了很好的效果。实现了存储与计算分离架构,其中采用了华为云自研的高性能分布式存储系统,显著提升了时序数据库的可靠性;同时方便计算节点分钟级扩容和存储空间的秒级扩容,同时大幅降低存储成本。支持亿级时间线(开源的能力在千万时间线级别),写入性能基本保持稳定;能够支持更高的高散列聚合查询性能;在压缩算法上,相比原生的InfluxDB,重点针对Float、String、Timestamp这三种数据类型进行了优化和改进。。
华为云MRS云服务包含了IoTDB[14],其中 IoTDB定位为面向工业设备、工业现场的时序数据库库。IoTDB优化后性能更好,千万级数据点秒级写入,TB级数据毫秒级查询;优化后的数据压缩比可达百倍,进一步节省存储空间和成本;通过采用对等分布式架构,双层多Raft协议,边云节点同步双活,做到7*24小时高可用。在工业化场景,真正做到一份时序数据兼容全场景、一套时序引擎打通云边端和一套框架集成云边端。
据公开资料[15],阿里云时序时空数据库TSDB的发展演变经历了三个阶段。在v1.0阶段基于OpenTSDB,底层实现了双引擎——HBase和HiStore。在v2.0阶段中,把OpenTSDB引擎换成了自研的TSDB引擎,弥补了OpenTSDB不支持的倒排索引、面向时序场景的特殊编码、分布式流计算聚合函数等特性。随后实现了云和边缘计算一体化,TSQL兼容Prometheus生态。其中TSDB for Spatial Temporal支持时空数据,它基于自研的S3时空索引和高性能电子围栏。最新TSDB同样基于 Gorilla, 将单个数据点的平均使用存储空间降为1~2个字节,可以降低90%存储使用空间,同时加快数据写入的速度。对于百万数据点的读取,响应时间小于 5 秒,且最高可以支撑每秒千万数据点的写入。相较于开源的 OpenTSDB 和 InfluxDB,读写效率提升了数倍,同时兼容 OpenTSDB 数据访问协议。
腾讯云也推出了TencentDB for CTSDB[16]云服务,它是一款分布式、可扩展、支持近实时数据搜索与分析的时序数据库,兼容 Elasticsearch 常用的 API 接口和生态。它支撑了腾讯内部20多个核心业务。性能方面可以做到每秒千万级数据点写入,亿级数据秒级分析。CTSDB也是采用LSM机制,先写内存再周期性刷写到存储;然后通过倒排索引加速任意维度数据查询,能实现数据秒级可查。也支持像histogram、percentile、cardinality这样的通用聚合计算函数;也通过配置 Rollup 任务定时聚合历史数据保存至新的数据表,实现降精度(Downsampling)特性。在集群中节点数量超过30个时,需要新购集群或者将通用集群架构优化升级为混合节点集群架构,以保证多节点超大集群的性能稳定。从这些特性推断,CTSDB内核应该是借鉴了ElasticSearch内核深度优化经验的基础上构建的时序数据库能力。
除了云服务厂商提供的开箱即用的云服务外,还有一些创新型产品涌现出来,比较有名的包括TDengine[17]、字节跳动的TerarkDB[18]、DolphinDB[19]等等。他们也在快速演进发展中,值得大家持续跟踪关注,尤其是国内孵化出来的一些TSDB产品。
InfluxDB、IoTDB和OpenTSDB等除了社区版本外,也有云厂商提供原生InfluxDB(阿里云TSDB for InfluxDB)、IoTDB(华为云MRS IoTDB)或OpenTSDB(华为云MRS OpenTSDB)云化服务,方便使用。更主流的做法是各云厂商根据自身的技术沉淀和研发实力,借鉴、优化甚至重新研发了时序数据库内核,能提供更强的集群能力,更高性能写入,更快的查询和聚合分析能力。国外厂商AWS有Timestream[20],一种Serverless时序数据库服务,国内华为云的GaussDB for Influx,汇聚顶级数据库专家团队打造的新一代时空分析数据库;腾讯云的TencentDB for CTSDB,兼容ES生态;阿里云的HiTSDB等等。这些开箱即用的、可扩展的、高可用的时序数据库,为云原生应用的开发与部署带来了福音,无需管理底层基础设施,只需专注于业务构建。
Promeheus发展过程中,其需要长期保存的历史数据(long-term)存储是其短板之一。业界有一些折中的集成方案。比如采用Cassandra作为Prometheus的持久化存储;还有采用InfluxDB作为Prometheus的持久化存储,一方面充分利用Prometheus监控相关能力和社区生态(包括支持分布式集群的Cortex);另一方面利用好InfluxDB时序数据库优点,尤其是超PB级的分布式数据库能力,以弥补Prometheus在海量历史数据存储上的短板。
Apache Druid在OLAP即时分析领域有着很强的竞争力,也为众多大厂所采用。业界最大的集群拥有4000多个节点。不管是时序指标,还是业务数据,应用日志等,都可以利用Druid的Kafka Indexing Service和其强大的数据预处理能力,转换为时序数据进入Druid。目前Druid SQL特性发展也很快,包括跨表join,subquery和众多函数、算子的持续丰富。
不管是正统的时序数据库,还是适合时序数据的OLAP分析系统;不管是开源社区的热门项目,还是云厂商提供更强大的云原生时序数据库,都为各种时序数据(包括指标、业务数据)的存储、检索和分析提供多样化的选择。用户结合自己的业务场景,一定能找到相对适合的工具或服务,满足业务诉求。