数据分析之时序数据库

2020-05-22 00:00:00 索引 查询 数据 时间 聚合

1 海量数据分析

海量数据分析类系统的设计主要面临2个大问题:

  • 1 海量数据如何存储?
    • a 借助于于Hadoop生态体系中的存储系统或者其他存储系统来存储海量数据,自身提供对上述数据的分布式查询分析功能,如Impala、Hive、SparkSQL、Presto、Drill、Kylin、OpenTSDB等

优势和劣势:
加入了Hadoop体系的生态圈,更加容易被接受,同时省去了研发分布式存储系统的麻烦,更多的是在分布式查询上做优化。但无法在存储上做更加深度的优化,比如没有倒排索引支持,过滤查找速度可能相对弱些,后面会重点分析下OpenTSDB的困局。

    • b 自身提供分布式存储,如Elasticsearch、Druid、ClickHouse、Palo

优势和劣势:
可以在存储上进行深度的优化,为自己特定数据模型进行定制。代价就是自己要实现分布式存储

  • 2 如何对海量数据进行快速分析?
    • a 对Hadoop上的原始数据集进行大规模并行的分析处理,如SQL on Hadoop之类的Hive、SparkSQL、Impala等等。通过并行的内存计算分析来提高查询速度。
    • b 预聚合之类的系统,如Kylin、Druid等。有效减少了查询的数据量,提高了查询速度,但是丢失了原始数据。Druid中的预聚合只是在时间维度进行预聚合,其他维度上的聚合在查询时计算得到,而Kylin会根据用户配置计算出所有要聚合的维度,这个聚合量就大了很多。终占用的磁盘空间也相对比较大,查询速度相对来说快。
    • c 含有列式存储、倒排索引等特性之类的系统,如Elasticsearch、Druid、InfluxDB。如倒排索引可以高效的进行数据过滤,进而可以提高查询的速度。还有很多特性如CBO和Vectorization、查询的流式处理等查询方面的优化由于篇幅暂不在本文的讨论范围内,本文注重于存储层面对查询的影响。


2 时序数据库

时序数据库也属于海量数据分析的范畴内,典型的系统如InfluxDB、Druid、OpenTSDB、Prometheus,也有人拿Elasticsearch来做时序数据库(比如腾讯)。具体有以下几个显著的场景特点:

  • 1 目前的几个时序数据库的数据模型基本统一
    Metric:指标名称
    Timestamp:时间戳
    Tags:维度组合
    Fields:指标值
    相对关系型数据库的数据模型来说,区分就是
    • a 必定含有时间字段
    • b 区分列的类型,需要用户将列划分为维度列和指标列。

划分后的好处就是:可以做一些存储上的优化,比如自动对维度列建立倒排索引,不需要用户使用关系型数据库那样针对某一列或者多列手动建立索引。总的来说:虽然给用户带来了分区维度列和指标列的麻烦,但是带来的收益确实非常大的。

  • 2 需要支持海量数据的实时写入与查询
    时序数据库使用多的场景就是监控领域,订单量的实时监控、机器的CPU、内存、网络等实时监控
    要做到这点目前来说基本上需要实现一个LSM树存储模型,上述几个时序数据库基本都有。
  • 3 在查询数据时都有时间范围
    在存储的设计上都会按照时间进行分片存储,按照时间范围查询时可以快速过滤掉无关的数据。

下面就简述下目前的几个时序数据库如何去解决前面提到海量数据分析的2大问题。

2.1 OpenTSDB

  • 1 海量数据如何存储?
    OpenTSDB自身并不实现分布式存储,而是借助于HBase或者Cassandra来存储海量的数据。
    对于实时写入也不是问题。
  • 2 如何对海量数据进行快速分析?
    • a OpenTSDB在数据过滤能力上还是比较弱的,主要是因为它所依赖的底层存储HBase自身目前暂时不支持二级索引(实际上有办法支持但是这其实就不在OpenTSDB考虑的范围内了)或者倒排索引。
    • b 在预聚合方面,OpenTSDB也是比较弱的。OpenTSDB提的2个概念,Rollup和Pre-Aggregates。Rollup是将同一个维度组合Series不同时间点上的数据聚合起来,解决查询时间范围大的问题,Pre-Aggregates是将不同的Series在同一时间点上的数据聚合起来,解决维度过多的问题。
      OpenTSDB要实现这2大功能,如果底层存储支持聚合操作,那么OpenTSDB中的TSD就可以将部分聚合的数据发往底层存储,由底层存储完成终的聚合,这里显然底层存储HBase并不支持聚合并且可能不会为了OpenTSDB来实现聚合功能。所以OpenTSDB中的TSD就只能先聚合好终的数据然后再发往底层存储。TSD原本是无状态设计,每个TSD只能见到部分的数据,这是无法完成上述任务的。如果将相同Metric的数据路由到同一台TSD,又会带来很多问题,同时会有时间窗口的引入,当数据在该时间窗口内未达到就会被丢弃。OpenTSDB又只能寄希望外部的流式处理来完成聚合任务。上述的诸多问题根本的原因就是底层存储不支持聚合。这一问题详见OpenTSDB的Rollup和Pre-Aggregates
    • 在时间范围过滤上也还是能定位到startKey和endKey,也能做到快速过滤。


OpenTSDB依赖了其他存储系统,在数据规模小的时候还能忍受,数据规模大了,查询速度就慢很多,想改变却又因底层存储不支持自己特定需求的原因而困难重重。

不过OpenTSDB将metric、tagk、tagv转换成id的方式确实可以省去很多的存储容量,这部分值的借鉴。

2.2 InfluxDB

  • 1 海量数据如何存储?
    InfluxDB是自研存储,通过hash分片的方式来存储海量数据。
    对于实时写入,也是通过LSM方式来实现数据的快速写入。
  • 2 如何对海量数据进行快速分析?
    • a InfluxDB实现了倒排索引,因此可以实现快速的数据过滤功能。倒排索引的代价就是降低了写入速度,再加上倒排索引的占用量可能会很大,并不能完全放内存,为了解决上述问题,倒排索引的写入也引入了LSM模型,即InfluxDB的TSI。其实InfluxDB的倒排索引的设计并不突出,倒排索引针对数据过滤场景非常有优势,但是对于没有数据过滤的场景如果仍然沿用倒排索引的查询方式通过一系列series id去随机查找数据会很慢很慢,在这方面InfluxDB做的也不好。综上2方面的因素,InfluxDB在对比Druid、我们自研的LinDB会都会弱一些。后面会详细分析下3者的倒排索引实现。
    • b 在预聚合方面,InfluxDB引入了Continuous Query和Retention Policy。2者配合使用可以提高数据的查询速度。相比于OpenTSDB中的Rollup和Pre-Aggregates,InfluxDB全部融合到了Continuous Query,既可以对时间维度进行Rollup,又可以将多个Series进行Pre-Aggregates。其实现原理很简单:就是查询一遍聚合后的数据写入到新的指标名下或者新的Retention Policy下。这种通过查询数据来实现预聚合的方式都有一个缺点:每次查询都是查询近一段时间范围的数据,对于之前已经查询过的时间范围若来了新的数据,并不会再次查询一次更新下结果。
      这种方式的预聚合的确能提高速度,但是对用户来说干预很大,用户需要理解Continuous Query,并且根据自己的查询需求来编写对应的Continuous Query,在查询时又要手动去选择合适的Retention Policy去进行查询,即还不能够做到自动化,InfluxDB自己又不能对所有metric都自动执行Continuous Query, 因为它不知道该如何聚合,不同用户写入的metric指标可能有不同的聚合需求。
    • c 列式存储,对于不需要查询的列可以显著降低IO,也是借鉴了Facebook的gorilla论文中的压缩算法进行压缩,压缩比高
    • d 在时间范围过滤上,由于InfluxDB存储本身就是按照时间范围划分的,所以也可以高效过滤


2.3 Druid

  • 1 海量数据如何存储?
    Druid也是自研存储,但是这个分布式存储的架构设计相当复杂。
    对于实时写入,也是通过类似LSM方式来实现数据的快速写入。
  • 2 如何对海量数据进行快速分析?
    • a 实现了倒排索引,在数据过滤方面也是非常高效的,详见后面的InfluxDB、Druid、我们自研的LinDB实现对比。
    • b 在时间范围过滤上,Druid的存储也是按照时间范围划分的,也能达到快速过滤。
    • c 在预聚合上,Druid支持在时间维度上的聚合,解决了部分问题,并没有解决维度基数很大时的预聚合问题,并且Druid不支持同一份数据聚合出不同粒度的数据(比如segmentGranularity为10s,对于查询几个月的数据量来说10s粒度还是很慢的)。这就需要用户自己同一份数据多次输入不同的datasource(每个datasource不同的segmentGranularity)来解决,查询时又要根据查询时间范围手动来选择对应粒度的datasource。


2.4 Elasticsearch

这里只是重点来说Elasticsearch在海量数据的聚合分析领域的应用,这里并不涉及到全文搜索(这是Elasticsearch立足的主战场,鲜有对手,但是在海量数据的聚合分析领域Elasticsearch对手就很多)

  • 1 海量数据如何存储?
    Elasticsearch也是借助于Lucene拥有自己的存储,同时也实现了分布式存储相关功能。
    对于实时写入,也是通过LSM方式来实现数据的快速写入的。
  • 2 如何对海量数据进行快速分析?
    • a 借助于Lucene实现了倒排索引,在数据过滤方面也是非常高效的。
    • b 在时间范围过滤上,Elasticsearch由于其通用性并没有针对时间进行特殊优化,导致在这方面相对InfluxDB、Druid逊色一些。虽然可以通过每天建立一个Index来缓解这个问题,但是仍然有避不开的麻烦,即在设计聚合分析时需要能够支持多个Index,这无疑增加了复杂度,不是一个理想方案。
    • c 在预聚合方面,Elasticsearch在Elastic{ON} 2018实现了这个功能。目前来看实现上和InfluxDB应该是类似的,都是通过定时任务查询原始数据来实现Rollup,并且支持多时间粒度聚合,来适应不同的查询时间范围。只是目前该功能还不能自动化,需要用户参与。


2.5 Kylin

在一定程度上Kylin也会被用做时序方面的数据分析。

  • 1 海量数据如何存储?
    Kylin依托于HBase(也可以换成别的存储)来实现海量数据的存储。
    对于实时写入,由于Kylin的重点是在数据摄入时做了大量的预聚合,那么就会导致实时性相比其他的几个系统还是慢很多的。
  • 2 如何对海量数据进行快速分析?
    • a 在预聚合方面,Kylin做了很多工作,基本上把所有维度组合都进行预聚合了一遍,大大减少了在查询时的聚合量,速度相比前面几个还是非常快的。拿占用更多的空间和降低了实时性的代价换取更短的查询时间。Kylin通过各种方式来减少预聚合的量来降低上述代价,但是这都需要用户理解并参与优化,增加了用户的使用负担。
    • b 在时间范围过滤上,时间也属于预聚合中的一个维度,所以时间范围过滤也是很高效的。
    • c kylin虽然说它所依赖的存储并不支持倒排索引,但是由于大量的预聚合,在一定程度上已经减少了查询时要聚合的数据量,所以即使HBase的过滤能力弱终的速度也还是ok的。


3 LinDB时序数据库

在调研了上述诸多系统之后,来看看LinDB的设计

  • 1 海量数据如何存储?
    LinDB通过借鉴Kafka的集群功能来实现海量数据的存储,目前只依赖ZooKeeper,并且在部署方面完全可以任意台部署,并不要求至少3台。
    对于实时写入,LinDB内部也是采用LSM方式来实现快速写入。
  • 2 如何对海量数据进行快速分析?
    • a 实现了倒排索引,在数据过滤方面也是非常高效的,先总结下其他系统的倒排索引:
      • 整体实现方式
        InfluxDB:基本实现方式是tagKey-tagValue-[seriesKey offset list],作为全局索引,优点:在时序场景下,相对文件级别索引,大部分时间每个文件索引基本上都差不多的,所以在查询上只需要1次索引查询即可,不像文件级别索引每个文件都要进行索引查询。
        Druid:基本实现方式是tagKey-tagValaue-[row id list],每个文件包含自己的索引。缺点:每个文件都要进行索引查询。优点:在数据迁移方面非常有利,只需要将整个文件复制即可,而InfluxDB就相对麻烦很多。
        LinDB:基本实现方式是tagKey-tagValue-[series Id list],也是作为全局索引,优缺点和InfluxDB一样的。LinDB这样设计主要还是基于时序场景下大部分情况下文件索引都是重复的这一情况考虑的。
      • 倒排索引大小
        这里指的是上述一个list的大小
        InfluxDB、LinDB:倒排索引大小就是组合数
        Druid:倒排索引大小就是行数,在时序数据场景下,一个文件中的数据基本是所有组合数多个时间点的数据,数据行数相比组合数大了很多,因此相对InfluxDB和LinDB大了很多
      • 数据存储大小
        InfluxDB:数据文件中的key是seriesKey+FieldKey,这一部分也是相当耗费存储的,并没有像OpenTSDB那样将他们转换成id来存储。对于时间的压缩采用差值的方式进行压缩,对于数据的压缩采用facebook的gorilla论文中的方式。
        Druid:倒排索引list中存储的是行号,每个文件中都将tags转换成对应的id来存储,相比InfluxDB也没有重复存储大量的tags。对于时间的压缩和数据都采用LZ4方式进行压缩
        LinDB:倒排索引list中存储的是series id,数据文件中是按照id来存储的,相比InfluxDB没有重复存储大量的tags,相比Druid没有重复存储tags到id的映射,以及每个tags的bitmap。因此从整体设计上来看LinDB是节省存储的。对于时间的压缩采用一个bit的方式来代表该数据槽位是否有数据(这种设计几乎在大部分情况下是非常有利的,除非是只有开始和结束槽位有数据,中间槽位都没来数据,这种特别的场景极少),因此相比InfluxDB和Druid做了,对于数据的压缩采用facebook的gorilla论文中的方式。
      • 查找数据方面
        InfluxDB:先根据过滤条件和倒排索引找到符合条件的所有seriesKey offset,然后再根据offset找到对应seriesKey,再将seriesKey和field拼成数据文件中的key,到符合查找时间范围的数据文件中查找对应的数据,所有的key分片随机查找,每个key查找是二分查找。
        Druid:在每一个符合查找时间范围的文件中,先根据过滤条件和倒排索引找到符合条件的所有row id,再按照所有的row id顺序性查找field的数据。比InfluxDB的优势在于:
        1 InfluxDB通过倒排索引找到的是seriesKey offset,还不能根据这个offset直接到数据文件中查找,还必须多一步offset到seriesKey的查找,假如符合条件的series有300万,那么这一步就已经相当耗时了,慢也就是必然的了
        2 InfluxDB对所有的seriesKey+field拼接成的key的查找是随机查找,每个key的查找是二分查找的方式,假如符合条件的series有300万,那么300万次的二分查找也是相当耗时(并且key的长度可加剧了耗时)。InfluxDB可以对所有的key进行分片多线程查找,相对来说快了一点,但是并不改变查询效率差的本质。
        LinDB:先根据过滤条件找到符合条件的所有series id,我们就可以直接拿着series id到数据文件中查找,相比InfluxDB省了offset到seriesKey的查找过程。Druid是文件内索引,过滤条件出来的结果是row id,天然顺序性,并且可以直接定位到数据位置。LinDB和InfluxDB是全局索引,过滤出来的结果需要经过一个查找过程才能找到对应文件中的offset。InfluxDB是原始的二分查找,效率并不高。LinDB通过计算要查找的series id是文件中的第几个series id,就可以根据这个序号找到对应的offset。我们通过RoaringBitMap的分桶策略,顺序性分片,只需要算出在当前分片的第几个位置再加上初始位置就可以得到总的位置,计算当前分片的第几个位置是二分查找。
        总的来说:
        InfluxDB: 多线程查找、二分查找、查找是长字符串之间的比较(还多一步series offset到seriesKey的查找)。这就是InfluxDB慢的一部分原因。
        Druid:单线程查找、O(1)查找、查找是数字之间的比较
        LinDB: 多线程查找、局部二分查找、查找是数字之间的比较


    • b 在时间范围过滤上,LinDB底层存储就是按照时间范围进行物理划分的,所以可以快速过滤
    • c 在预聚合上,先总结下其他系统的预聚合:
      • OpenTSDB的预聚合是它的痛点
      • InfluxDB的预聚合通过查询来实现有缺陷,以及用户需要理解Continuous Query和Retention Policy
      • Druid底层存储支持时间维度的预聚合,并且只能有1种聚合粒度
      • Elasticsearch近发布支持预聚合跟InfluxDB实现有点类似
      • Kylin预聚合比较全面,但是需要用户深度参与优化

从目前公司内部的实际使用情况来看,很多用户就前面所说的时序数据模型都没有理解,更别指望他们去理解Continuous Query、Kylin Cube等概念了,对于他们来说这些概念他们也不想了解,只管打点即可,剩下的性能问题都是系统维护者的事。所以我们LinDB面向用户的首要目标是简单易用,用户只需理解时序数据模型即可。
要实现这个目标并不简单,查询的数据量大小主要有2方面的因素:组合数的大小*每个组合数的点数,比如100个host,每个host 32个核,每个host的每个核 1s一个点,那么查询所有host 7天内的所涉及的数据量大小为 (100*32) * (7*24*3600)。为了减少这个数据量的大小,就需要从下面2个方向入手:

      • 对时间维度进行预聚合
        提前将每个组合1s一个点的数据聚合成10s一个点、10分钟一个点、1天一个点。这样就可以将数据量降低至1/10、1/(10*60)、1/(24*3600)。查询近几个小时,可以用10s粒度的数据区查,查询几天的数据可以用10分钟粒度的数据区查,查询几个月甚至几年的数据可以用1天粒度的数据去查,这已经大大降低了要查询的数据量,意味着查询几年的数据的响应时间都可以是毫秒级别的。
        用户需要做的:用户需要给出每个数据即Field的聚合方式即可
        我们如何实现:10s、10分钟、1天粒度,这3种粒度可以自定义,完全可以满足从近几小时到几年范围的数据查询,用户在查询指标时会根据用户的查询时间范围自动选择合适粒度的数据,占用的存储基本是原始数据的1/10、1/(10*60)、1/(24*3600)。这一切都是用户无感知的。在实现上,我们不是建了多个库,1份数据写到每个库中(每个库都包含复制、LSM模型),我们不是像InfluxDB那样通过查询来实现,我们是每个库支持多粒度存储,在写入时写入到小粒度中,flush出多个文件后,将多个文件一起读取聚合到下一个粒度中。
      • 对组合数进行预聚合
        提前将每个host的所有核聚合起来,但是这是跟用户的查询需求密不可分的,还是需要用户参与。由于时间维度预聚合已经基本满足了我们的需求,所以这个目前我们还暂时没做,之后我们可能会在时间维度预聚合的结果上再来实现这一功能,将彻底解决大时间范围大维度查询的问题。

我们可以看到InfluxDB、Kylin等Rollup的实现是将时间维度预聚合和组合数预聚合合并在一个功能中,虽然简化了开发,但是却麻烦了用户。我们则必须要把他们分开,我们将时间维度预聚合完全自动化,使得几乎所有的用户不用陷入如何优化的烦恼中,针对极个别的用户我们之后再通过组合数预聚合让用户参与优化。总之,LinDB在功能设计上都以简化用户使用为目标。


特别有意思的是很多系统在自己特定领域站稳脚跟后都会进行扩张到其他相关领域,比如OLTP的数据库向OLAP扩张,再比如Elasticsearch在全文搜索领域站稳脚跟后扩张到时序数据库领域,如果在设计之初就能考虑到其领域的关键点的话,那么扩张可能会顺利很多,比如时序数据库中非常重要的倒排索引和预聚合

未来对数据的实时写入和实时查询要求会越来越高,因此时序数据库相比依赖于HDFS的Hive、SparkSQL、Impala等优势很大,但是目前时序数据库的查询丰富性方面相比它们还差很多,通常都是对单指标的filter和group by查询,比如对多指标的join暂时暂时都不支持。在监控场景下,join的需求不是很强烈,但是时序数据库要想走向其他场景下的数据分析领域,瓜分他们的地盘,join还是必不可少的功能,这时对时序数据库的分布式SQL查询要求也就变高了,如果能做到的话才更容易走出时序数据分析领域,向其他数据分析领域进军

参考文档:

1 OpenTSDB文档

2 InfluxDB文档

3 InfluxDB系列解析

4 Druid文档

5 Druid Storage 原理

6 Elasticsearch Rollups

7 Elasticsearch技术研讨知乎专栏

8 Roaring Bitmaps

相关文章