当前位置: 首页 > 科技观察

时序数据库技术体系——时序数据存储模型设计

时间:2023-03-17 16:02:52 科技观察

时序数据库技术体系中一个很重要的技术点就是时序数据模型设计。不同的时序系统有不同的设计模式,不同的设计模式可以读写时序数据。性能、数据压缩效率等方面都有不同程度的影响。在本文中,笔者将分别介绍OpenTSDB、Druid、InfluxDB和Beringei这四个时序系统中的时序数据模型设计。在详细介绍时序数据模型之前,还是有必要简单回顾一下时序数据的几个基本概念,如下图所示:上图是一个典型的时序数据示意图,从如图,时间序列数据由两个维度组成,横坐标代表时间轴。随着时间的推移,数据会不断吐出;与横坐标不同,纵坐标由两个元素组成,即数据源和指标。数据源由一个系列的标签(tag,也叫维度)唯一表示。图中数据源为广告数据源。该数据源由发布者、广告商、性别和国家四个维度值唯一表示。Metric表示要收集的数据源索引。数据源通常收集许多指标。上图中的广告数据源收集了三个指标:impressions、clicks和revenue,分别代表广告浏览量、广告点击率和广告收入。看到这里,相信大家对时序数据有了初步的了解,可以简单概括为:一个时序数据点(point)是由datasource(tags)+metric+timestamp这三部分唯一确定的。然而,这只是一种逻辑概念上的理解。一个具体的时序数据库是如何存储这样一系列时序数据点的呢?下面,笔者介绍四个系统:OpenTSDB、Druid、InfluxDB、Beringei。OpenTSDB(HBase)时序数据存储模型OpenTSDB基于HBase存储时序数据,RowKey规则在HBase层面设计为:metric+timestamp+datasource(tags)。HBase是一个KV数据库。如果一个时序数据(点)用KV的形式表示,那么V一定是点的具体值,而K自然是唯一决定点值的datasource+metric+timestamp。这个规则不仅适用于HBase,也适用于其他KV数据库,比如Kudu。由于HBase中的K是由datasource、metric和timestamp组成的,现在我们可以简单的认为rowkey就是这三者的组合,那么问题来了:这三者的组合顺序是怎样的?让我们先看看哪一个应该排在列表的顶部。因为HBase中一张表的数据组织方式是按照rowkey的字典顺序排列的,为了把同一个指标的所有数据放在一起,HBase把metric放在了rowkey的最前面。如果把时间戳放在最前面,那么同一时间的数据必然会写入同一个数据段,达不到散列的效果;而如果将datasource(也就是tags)放在最上面,还有一个更大的问题,就是datasource本身是由多个tag组成的。如果用户指定了一些标签进行搜索,而且不是前缀标签,在HBase中就会变成大规模的扫描过滤查询,查询效率很低。举上面的例子,如果datasource放在最前面,那么rowkey可以表示为publisher=ultrarimfast.com&advertiser:google.com&gender:Male&country:USA_impressions_20110101000000,此时用户想查找在USA发布的所有广告在20110101000000时的浏览量,即根据country=USA等维度信息在指定时间点搜索某个指标,且该维度不是前缀维度,大量记录会被扫描以进行过滤。确认metric放在最上面之后,我们再看看是datasource应该放在中间还是timestamp应该放在中间?把metric放在前面已经可以解决请求均匀分布(hash)的需求,所以HBase会把timestamp放在中间,datasource放在最后。试想一下,如果把datasource放在中间,也会遇到上面说的找后缀维度的问题。所以OpenTSDB中rowkey的设计是:metric+timestamp+datasource,好吧,那么HBase只能设置一个columnfamily和一个column。那么问题来了,OpenTSDB的这种设计有什么问题呢?在理解设计问题之前,我们需要先简单了解一下HBase在文件中存储KV的方式,即一系列时序数据在文件和内存中的存储方式,如下图所示:上图是一个HBase中存储KeyValue(KV)数据的数据块结构。一个数据块由多个KeyValue数据组成。在我们的例子中,KeyValue是一个时间序列数据点(point)。Value结构很简单,就是一个值。Key比较复杂,由rowkey+columnfamily+column+timestamp+keytype组成,其中rowkey等于metric+timestamp+datasource。问题一:有很多无用的字段。只有KeyValue中的rowkey才有用。其他columnfamily、column、timestamp、keytype等字段理论上没有实际意义,但在HBase存储系统中必须存在,消耗大量存储成本。问题二:数据来源和采集指标冗余。KeyValue中的rowkey等于metric+timestamp+datasource。试想同一个数据源的同一个集合索引,随着时间的推移不断吐出集合数据。这些数据理论上共享相同的数据源(datasource)和集合指标(metric)。但是在HBase这种存储体系下,无法体现共享,所以存在大量的数据冗余,主要是数据源冗余和集合索引冗余。问题三:无法有效压缩。HBase提供了块级的压缩算法——snappy、gzip等,这些通用的压缩算法并不是针对时序数据设置的,压缩效率比较低。HBase也提供了一些编码算法,比如FastDiff等,可以达到一定的压缩效果,但是效果不好。效果不佳的主要原因是HBase没有数据类型的概念,没有schema的概念,不能对特定的数据类型进行特定的编码,只能选择通用的编码,效果可想而知。问题四:多维查询能力不能完全保证。HBase本身没有schema,目前也没有实现倒排索引机制。所有查询都必须指定指标、时间戳和完整的标签或前缀标签才能查询。后缀维度查询也比较困难。虽然存在各种问题,但OpenTSDB还是从两个方面对存储模型进行了优化:优化一:时间戳没有想象中的秒级、毫秒级那么细,而是精确到小时级,然后每小时每小时一秒设置为列。这样的行将有3600列,每列代表一小时中的一秒。据说此设置能够有效检索整整一个小时的数据。优化2:所有的metrics和所有的标签信息(tags)都使用全局编码,将标签值编码成更短的bits,减少rowkey存储的数据量。上面分析的HBase存储方式的缺点是会存在大量的数据源(tags)冗余和指标(metric)冗余。如果有冗余,那我就做一个编码,把字符串编码成比特。尽量减少冗余。虽然这样的全局编码可以有效减少数据存储量,但是由于全局编码字典需要存储在内存中,在很多情况下(海量标签值),字典需要的内存会非常大。以上两个优化可以参考OpenTSDB的经典示意图:Druid时序数据存储模型设计不同于HBase、Kudu等KV数据库,Druid是另一种玩法。Druid是一个不折不扣的没有HBase主键的列式存储系统。以上时序数据在Druid中的表示如下:Druid是一个列式数据库,所以每一列都会独立存储。比如Timestamp列会存储在一起形成一个文件,publish列会存储在一起形成一个文件。比喻。细心的童鞋会说,这样存储还是会有很多数据源(标签)的冗余。对于冗余问题,Druid和HBase的处理方式是一样的。它们都是使用编码字典对标签值进行编码,将字符串类型的标签值编码为int值。但是,与HBase不同的是,Druid编码是部分编码。Druid和HBase都采用了LSM结构。数据先写入内存,然后刷新到数据文件。Druid编码是文件级别的,本地编码可以有效减少巨大的内存占用。压力。此外,Druid的列式存储模式还有以下优势:数据存储压缩率高。每列独立存储,可以针对每一列进行压缩,可以为每一列设置相应的压缩策略,比如time列,int,fload,double,string可以单独压缩,压缩效果更好.支持多维查找。Druid为数据源的每一列设置一个Bitmap索引。使用Bitmap索引可以有效实现多维搜索。例如,如果用户想查找在20110101T00:00:00时间点在美国发布的所有广告的浏览量,则可以使用country=USA在Bitmap索引中找到您要查找的行号,并且然后根据行号定位到要检查的指标。但是,这样的存储模型也存在一些问题:数据仍然是冗余的。和OpenTSDB一样,标签有很多冗余。特定数据源的范围查找不如OpenTSDB高效。这是因为Druid会将数据源拆分成多个标签,每个标签使用Bitmap索引,最后使用和操作找到符合条件的行号。这个过程需要一定的开销。在OpenTSDB中,可以直接根据数据源拼装rowkeys,查找B+树索引,效率必然更高。对比OpenTSDB和Druid,InfluxDB时序数据存储模型设计,可能很多童鞋对InfluxDB不是特别熟悉,但是InfluxDB在时序数据库榜单中遥遥领先。InfluxDB是专业的时序数据库,只存储时序数据,所以对于时序数据,可以在数据模型的存储上做很多优化工作。为了保证高效的写入,InfluxDB也采用了LSM结构。数据先写入内存,当内存容量达到一定阈值时,再刷入文件。InfluxDB在时序数据模型的设计中提出了一个非常重要的概念:seriesKey,seriesKey其实就是datasource(tags)+metric,时序数据写入内存后,按照seriesKey进行组织:内存其实就是一个Map:>、Map一个SeriesKey对应一个List,时间线数据存放在List中。数据进来后,根据datasource(tags)+metric组合成一个SeriesKey,然后将Timestamp|Value的组合值写入到时间线数据List中。内存中的数据flush后,同一个SeriesKey中的timeline数据也会写入到同一个block中,即一个block中的数据属于同一个数据源下的一个metric。我们认为这个设计是根据时间线把时间序列数据挑出来。先来看看这种设计的好处:好处一:来自同一个数据源的标签不再冗余存储。一个block中的数据都共享一个SeriesKey,只需要将SeriesKey写入Block的Trailer部分即可。时序数据的存储容量大大降低。好处二:时间序列和数值可以分开独立存储在同一个区块中,独立存储可以将时间列和数值列分开压缩。InfluxDB对时间列的存储借鉴了Beringei的压缩方式,采用delta-delta压缩方式,大大提高了压缩效率。Value的压缩可以针对不同的数据类型采用相同的压缩效率。好处三:对于给定数据源和时间范围的数据搜索,可以非常高效的进行搜索。这与OpenTSDB相同。细心的同学可能会问,将datasource(tags)和metric组合成SeriesKey不是也不能实现多维搜索。确实是这样,但是InfluxDB内部实现了倒排索引机制,即tag和SeriesKey的映射关系。如果用户想根据某个标签进行查找,首先根据标签在倒排索引中找到对应的SeriesKey,然后根据SeriesKey定位具体的时间线数据。InfluxDB的这个存储引擎叫做TSM,全称是Timestamp-StructureMergeTree,基本原理和LSM类似。后面笔者会专门介绍InfluxDB的数据写入、文件格式、倒排索引和数据读取。Beringei时序数据存储模型设计Beringei是Facebook今年开源的一个时序数据库系统。InfluxDB时序数据模型设计很好的根据数据源和metric选择时序,解决了维度列值冗余存储和时间列压缩无效的问题。但是InfluxDB并没有很好的解决writecache压缩的问题:InfluxDB在写入内存的时候不进行压缩,而是在数据写入文件的时候进行相应的压缩。我们知道时序数据最大的特点之一就是最近写入的数据最热。将最近写入的数据全部放在内存中,可以大大提高读取效率。Beringei很好的解决了这个问题。流式压缩是指数据写入内存后进行压缩,这样可以让更多的时序数据缓存在内存中,对近期的数据查询有很大的帮助。Beringei的时序数据模型设计与InfluxDB基本相同。它还提出了一个类似于SeriesKey的概念来挑选时间线。但是与InfluxDB有两个很大的区别:文件组织不同。Beringei的文件存储格式是按照时间窗口来组织的。比如最近5分钟的所有数据都写入同一个文件。这个文件分为很多块,每个块中的所有时序数据共享一个SeriesKey。Beringei文件没有索引,InfluxDB有索引。Beringei目前没有倒排索引机制,所以对于多维查询效率不高。后续笔者还会介绍Beringei的数据写入、流式压缩、文件格式。在我看来,如果将Beringei和InfluxDB有效的结合起来,时间序列数据可以高效的存储在内存中。另外,数据按照维度进行组织,可以非常有效地提高数据在文件中的存储效率和查询效率。最后,结合InfluxDB的倒排索引功能,可以有效提升多维查询能力。本文为时序数据库技术体系的第一篇文章。作者结合四种时序数据库:OpenTSDB、Druid、InfluxDB和Beringei,主要介绍了时序数据在这种数据形式下的存储模型。每个数据库都有自己的一套存储方式,每种存储方式都有自己的优缺点。正是这些优缺点,直接决定了对应时序数据库的压缩性能和读写性能。