-->

No.1-Apache IoTDB 随笔 - Time Series DBMS 综述

时间:2022-03-25

文章转自:孙金城 Apache IoTDB 随笔,版权归原作者所有,若有侵权请联系:contact@timecho.com

“这是一篇无法一口气读完的、文字过万[正文字数14390]长文,这是一个无法中途不上厕所就看完的、关于时序数据库的视频[时长111分钟]分享...




大家好,很开心能够和大家一起交流时序数据库的相关的内容



       首先还是简单自我介绍一下,我是 孙金城,花名 金竹。我是2011年加入阿里,在2016年之前一直做公司内部的研发工作,包括阿里郎,Blink等平台。

       从2016年到现在我一直重心在开源建设上面,包括ApacheFlink/ApacheBeam/ApacheIoTDB,在这个过程中也得到了开源的一些肯定,目前是BeamCommitterApacheFlinkApacheIoTDBPMC,也是Apache Member,目前全球华人大概有30+ApacheMember,当然,随着开源的越来越热,国内每年参与开源建设的同学也在逐渐的在增加。

       那么2020之后会有怎样的规划呢?本着但做好事,莫问前程的心态,会多多在订阅号中记录我在流计算和IoT方面的认知。最终努力做到走进阿里/踏入开源,成为最好的自己。

       那么为什么一直做流计算会慢慢选择了解IoT相关领域呢?因为在马老师看来“5G时代,加速的不仅仅是通讯行业,而是更多的促进物联网(IOT)领域的发展。IoT将是一个新的浪潮。那么我参与IOT领域的切入点是什么呢,就是从了解时序数据库 进行着陆的...


      如图,这不是一篇经验分享,而是一个学习过程分享

     了解时序数据库最先想到的是了解一下时序数据库的现状及发展趋势,那么这个数据的权威性,大家应该有所共识,在db-engines网站的排名应该很客观的。如图,从2018年开始,TimeSeriesDBMS的关注度就持续迅猛的增长。其实不仅仅db-engines网站有这样的数据,Gartner也给出了预见性的判断。我们一起看一下...

       在2019"赋权的边缘就成为了十大战略性技术趋势之一,边缘由物联网驱动,需要使处理计算更接近端而不是集中式的云服务器。当然早在2018 "云向边缘计算挺进的趋势就已经非常明显。边缘设备的信息的收集/存储和计算需求与日俱增,一直到目前2020年,更多的计算和存储能力都逐渐下沉到设备端,云边端一体将是今后几年持续的焦点。设备数据具备时序性,而作为物联网领域具备存储和计算能力的产品就是时序数据库。那么目前业界时序数据库的阵营是怎样的状况呢?我们继续看一下...


       同样出自DB-Engines的排名信息,目前有几十种(大概34种)时序数据库,大家熟知的InfluxDB2016年以来稳稳的位居榜首,随着2018IoT领域的崛起,InfluxDB的热度也持续飙升,稳稳地龙头位置,那么InfluxDB为啥如此受到时序数据存储技术的青睐呢?我们接下来就会和大家细致进行分析...


       其实时序数据库早在1999年就已经有RRDtool,全称RoundRobin Database。新数据会自动覆盖老数据,也就是考虑了时序数据的时效性,在2008年又出现了GraphiteGraphite是一个用于采集网站实时信息,并进行统计的开源项目,可用于采集多种网站服务运行状态信息。随后就是大家熟知的OpenTSDBInfluxDB,还有feacebook的内存版时序数据库,再有就是非常著名的用于监控的Prometueus2017年,2018年的时候时序数据库已经发展的非常迅速来,这个在刚才的DB-engines网站数据和Gartner给出的战略技术趋势信息是非常吻合的。那么在2020年,Apache开源社区又出现了时序数据库的黑马 ApacheIoTDB,将来还会有哪些时序数据库产品呢?我们让时间来揭晓。



       那么在这里,抛出一个问题,就是:“时序数据库难道不能直接存储到关系数据库吗?”为啥要造出很多时序数据库,时序数据库和关系数据库又有怎样的本质区别呢?


       其实数据存到哪里合适,还是要看数据本身的特点,以及数据处理的需求。面对IoT领域,时序数据有很多的数据来源,比如汽车,火车,飞机等交通工具,以及我们越来越被大家认可的智能家居产品等。

       当然最大的数据量来源还是工业领域的各种设备传感器数据,这些设备的工况数据收集和处理将给存储和计算带来巨大的挑战。

       我们以一个具体的案例来说,这是GoldWind发电数据采集,GoldWind有超过2w个风机,一个风机有120-510个传感器,采集频率高达50Hz,就是每个传感器150个数据点采集峰值。这要算下来就是每秒5亿个时序指标点的数据。

       这个数据量让数据采集/存储/计算面临很大的挑战。同时还有我们业务中的一些非常常见的采集/查询需求,20/妙的吞肚需求是非常常见的,但是这样的需求单机关系数据库也是很难搞定的,我们接下来会分析一下具体原因。



       好的,那么还是简单梳理一下时序数据的特点,首先时序数据有特定的一些概念。如图:Metric,就是我们要采集的指标,类似一张表,还有Tag,就是metric的属性特点,比如指标属于哪个设备,哪个区域等等维度信息。再有Field就是真实要采集的指标名称。那么非常重要的时序数据一定有数据产生的时间,就是Timestamp时间戳信息,指标数据的时效性也是非常重要的,所以要有时间信息。那么Point就是类似表的一行数据。我们以风力 指标 为例 简单图示一下这些概念。

       Wind-force就是Metric,其中 cityregion就是Tagspeeddirection就是Field。当然Timestamp不用多说就是指标数据产生的时间了。

       那么这种关系设计的方式大家不难发现,tag的数据存储会有很多的冗余。这是关系数据库存储时序数据的一个涉及到存储成本的问题。当然问题不仅如此,我们继续往下看。


       现在,我们简单总结一下时序数据场景的特点或者说是需求核心会涉及到写入/读取/存储成本三个维度。写入要支撑上千万甚至上亿的吞吐能力;读取就需要根据不同的业务有不同的读取需求,后面我们会慢慢分析到。当然面对成本问题,海量数据的存储成本无疑是时序数据库设计的重中之重。所以,面对这样的时序数据场景的特点,时序数据存储到关系数据库就会有很多弊端,比如刚才说的数据冗余造成的成本问题,还有写入的吞吐问题等,那么我们接下来就要讨论这些问题存在的本质原因...


      好,回过头来我们在来看看目前的时序数据库从架构的角度有哪几种


      第一种,就是在关系数据库基础上进行改进的时序数据库,比如基于PG开发的Timescale


      第二种,就是在KV数据库的基础之上进行改进的时序数据库,比如,基于HBase开发的OpenTSDB


      第三种,就是为时序数据量身定制的时序数据库,那么目前的领头羊就是InfluxDB当然,我前面说过,我也很看好2020新晋的Apache顶级项目ApacheIoTDB


      这三类时序数据库又怎样的特点呢,我们接下来逐一进行讨论分析...



       我们先来看看基于关系数据库的时序数据库,既然基于关系数据库,那么我就要聊聊和关系数据库密切相关的存储结构。


       首先我们看一个简单的二叉树,我们知道二叉树是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,而且二叉树的存储结构及其算法都较为简单,二叉树特点是每个结点最多只能有两棵子树,且有左右之分。那么我们看看怎样向二叉树里面插入一个数据点呢?


       如图,我们插入节点10,插入的过程是二分法,即使从整个节点的数值的中间开始,如果要插入的值比这个数小就从左子树继续二分查找,如果要插入的数据比当前数大,就从右子树继续查找,那么按照这个算法,二叉树的写入和查询的复杂度都是logN


       我们再举一个小例子,可能目前我们讲解的内容很简单,大家都很熟悉,不过为了为后面的内容做铺垫,大家还是稍微耐心听我多啰嗦几句。这个例子,就是假设我们有如图8个数据,我们如果想要查找数据2,那么我们先看中间数据(第4个)点,13,比较一下132大,所以要查询的2一定在13的左边,我们再从左边的数据进行二分查找,左边的中间数据5,发现5还是比2大,那么就需要查找左子树,目前只剩下了2,当然也就是我们要找的数据了。那么logN其实就是log2N对吧,集合8个数据,查找一条数据就是log823次方等于8,所以log8就是3次,这是二叉树的查询复杂度的一个简单示例说明。OK,这个二叉树介绍呢有点浪费大家时间,废话有点多,大家见谅,那么,接下来我们就介绍关系数据库中使用的存储数据结构。



       在实际的关系数据库中有2种数据存储结构,一个是BTree一个是B+Tree那么,这两种数据结构格局特色,都有使用的场景。那么这两种数据结构的查询复杂度是怎样的呢?有什么本质区别。


       BTree的写入成本是LogBNB就是树的阶数,如图就是3BTree,每个节点的黄色方框表示的指针个数就是树的阶数,蓝色部分就是具体构建BTree节点的数值,如果是索引树的话,蓝色部分就是构建索引的key的值,比如订单号之类的关系数据库的索引字段。


       B+Tree的写入成本是LogBN,如果都是LogBN那么BtreeT+Tree有啥本质区别呢?这要看看BTreeB+Tree从数据的结构上有啥区别?


       最本质的区别是BTree在每个树节点是有数据存放的,B+Tree只有叶子节点才会存放数据,我们说的数据就是关系数据库中一行数据(包括Key和普通字段),比如订单号是key,订单号以及订单对应的产品名称,数量,金额等就是数据。同时还有一个很大的区别就是B+Tree结构在树叶节点是有指针指向相邻的叶子节点的,是一个链表。这特点,大家想想有什么利好?对,这个特点是有助于区间查询的。大家花几秒钟时间思考一下,相对于BTree是不是有这样的优势?:)


       这里大家可能发现一个问题,就是这个算法复杂度应该是LogN,为啥是LogBN。本质上在存储角度我们考虑的是DAM模型,也就是计算磁盘访问次数的成本。


       我们继续思考另外一个有意思的问题?Btree的成本和B+Tree都一样,B+Tree又有友好支持区间查询的优势,那么两个共存的意义是什么呢?好,我们还需要继续搞清楚这件事情...其实对于不太了解这些数据结构的同学是有点烧脑的,对于熟悉的同学可以放松一下,我们继续探究B+Tree存在的意义,抛开算法复杂度之外,还有哪些其他存储领域的实际因素,导致B+Tree更有优势。



       上面是BTree从算法角度的复杂度推导,下面是B+Tree的...


       这里有一个地方要说明一下,这里推理的复杂度是logN,但是前面我们说的是logBN,这个差别是什么?其实本质前面是磁盘访问复杂度,不包括数据块内的key值查找。这个大家如过感兴趣,可以思考一下,如果有问题欢迎留言J


      OK,放松3秒钟,然后继续分析 B+ Tree ?我们继续和这个问题死磕...



       要想清楚上面的问题,我们还需要一些算法之外的辅助内容,虽然是辅助但却是和存储系统密切相关的部分。那就是磁盘。在存储领域选择BTree还是B+Tree和磁盘IO有着直接的关系。那么什么是磁盘IO呢?


       我们知道计算机的CPU就如同人的大脑,大脑根据外部输入进行思考,并为我们下达行为命令,那么CUP同样要依托于外部输入进行计算,然后将计算的结果再向外输出。那么我们存储里面的磁盘IO可以简单理解成从磁盘读取数据和向磁盘写数据的过程。



       这个图应该是我们上学课本或者数据库理论方面书籍里的图片,处理器,数据总线,主存,磁盘等等,我们再熟悉不过的名词,当然内部也是非常复杂的,我们今天挑与我们要讨论的问题相关的内容进行分析。


       那就聊聊磁盘,首先磁盘内有很多的盘片,每个盘片又由若干扇区组成,扇区是磁盘读写的最小单元,每个扇区多大呢?一般是512个字节。好,那么这个和数据库又有啥关系呢?


       数据库的数据就存储在磁盘上,但是从数据库系统角度如果每次读取一个扇区的数据就太慢了,所以数据库一般会有数据块的抽象设计,数据都是一个数据块读取一次磁盘,一般数据块大小是464KB,那么重点来了,通常读取一个数据块的磁盘IO需要消耗大概10ms左右的时间,这是非常耗时的操作了,所以我们在数据库设计的时候一定要尽量减少磁盘的访问次数。那么MySQLInnoDB默认数据块的大小是16KB,当然这是可以配置的。不同数据库默认值不一样,比如HBase存储的数据块大小应该默认64M。好的,说到这里,不知道大家是不是知道我接下来要说什么了?有这方面经验的估计秒懂,但是第一次思考这个问题的,还是需要些解释的,我们往下继续进行...



       OK,我们还是举一个例子来讨论,假设以16K为块大小,也就是每16K数据需要访问一次磁盘,我们的目标是减少磁盘I/0,那么数据相同的情况下,B-TreeB+Tree哪个磁盘I/O更少呢?假设我们有一个查询:select* from device where deviceId=38deviceID是一个bigint8字节类型,作为索引key。我们分别分析一下,BtreeB+Tree哪个访问磁盘更少?


       主键deviceIdbigint类型,长度为8字节,而指针大小在InnoDB源码中设置为6字节,这样一共14字节,也即是如图一个蓝色框数据和一个橙色框指针需要14个字节存储。那么,我们一个数据块中能存放多少这样的单元,就代表一次可以在内存加载多少数据keydeviceID)和对应的地址指针,即16384(16K)/14bytes=1170。也即是,每1170个设备id读取一次磁盘,那么也就是内存构建一个1170阶的B+Tree


       那么大家想,如果是BTree结构,一行数据除了设备Id还有他设备信息,那么Btree在没有节点不但存储key和指针,还要存储数据,所以同样的数据块大小,是不是一次加载到内存的Key数量比B+tree要少很多,那么在查询时候必然比B+Tree访问磁盘IO要多,大家思考2秒钟,是不是这个道理?:)


      那么如果是BTree,相同的块大小,树的阶数就比较小,相对于B+Tree来说,相同条件B+Tree的阶数更大,也就是复杂度上的logBN,的B比较较大,B越大,相同的查询开销越小。



       那么,既然B+tree很优秀,我们看看一个B+Tree能支持怎样的数据规模呢?好我们想想B+Tree只有叶子节点要数据,比如我们一行数据是1K,一个数块是16K,那么也即是一个数据块可以存放16行数据。那么一个3层的B+Tree,叶子节点有多少行记录呢?三层的树的话,第二层有多少数据指针指向第三层叶子节点就有多少数据块数据的数据,每个叶子是一个数据块,每块有16行数据就能计算数据规模了。那么底层有多少指针呢?还是按照刚才MySQLinnoDB的例子算,会构建一个1170阶的B+Tree,大家还记得“阶”的概念吧,就是一个节点指针的数量,那么1170阶的B+tree,第二层首先有1170个节点,每个节点又有1170个指针(刚才我们计算过),那么我们可以计算第三层的记录行数了,那就是:1170*1170*16=2000w+。也即是3层的1170B+tree可以完美的支持2000w的数据量,那么这2000w数据量查询一条数据的成本是多少呢?通常磁盘IO的代价是最大的,刚才我们说的了一次磁盘大概10ms,那么3层,我们最多要3次磁盘IO就可以查询到了。也就是毫秒级的查询延时。


       我们再看一个例子,分析为啥3次就够了,假设这棵树,我们要查找key为30的数据,那么过程如下。


  • 根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存。[1次磁盘IO] 此时内存中有三个key(5,28,65)和三个存储其他磁盘块的地址的数据。我们发现28<30<65,因此我们找到指针p2。

  • 根据p2指针,我们定位到磁盘块3,并将其中的信息导入内存。 [2次磁盘IO] 此时内存中有3个key(28,35,56)和三个存储其他磁盘块的地址的数据。我们发现28<30<35,因此我们找到指针p1。

  • 根据p1指针,我们定位到磁盘块8,并将其中的信息导入内存。 [3次磁盘IO] 此时内存中有3个key(28,30,33)和对应的数据,完成查找。


      OK,查询的逻辑和成本消耗先介绍到这里。下面更重要的内容来了,也就是内容会涉及到了为什么关系数据库不适合作为IoT场景的存储,大家要多花精力理解,我给大家演示一下BTree的写入的过程。



       好,接下来,假设我们有数据按照如图的顺序到来,如何一步,一步的构建一颗BTree索引树呢?



       好,首先来的数据是33590我们构建一个树的形态如图,35root,左右各有一个孩子。


       然后又来了1726,二分的方式,1726都小于35,所以都在35的左子树,但是这里有个问题?


       就是我们是一个3Btree,指针有3个,节点只能有2个,所以31726已经超了,我们要进行节点的分裂,后面存储层面就涉及到磁盘块的分裂。


       好,我们进行节点分裂之后,树的样子变成,root节点有2key3个指针。我们继续看后面的变化。。。


       我们看看后面来了哪些数据。。。树的变化如图,这里提醒一点,中序遍历的不同的树形状可以得到相同的结果。我们这个例子核心是想说明数据的构建过程有节点的分裂,也意味着存储有磁盘块的分裂,会产生大量的随机磁盘IO。我们继续看变化。。。

       接下来再后面的数据。。好,3687296575到来后,树的形状如图。。。(这棵树是故意弄的很平衡哈:)


       最后,我们看看有哪些数据到来,最终树的形成。基于B-Tree/B+Tree的存储,数据写入造成很多的随机IO。我简单说一下,大家思考,如果一个节点已经写入磁盘了,后面树形发生变化之后,节点分裂,原先存储到一个磁盘块的数据,就会分开存储到2个新的磁盘块,那么原先的磁盘块就要删除。这样反复的操作,那就造成了Btree/B+tree很多的随机IO了。

       OK,聊到随机IO对存储系统的影响,我们也要顺便说一下相对于随机I/O就是顺序I/O。


  • 顺序IO - 是本次 I/O 给出的初始扇区地址和上一次 I/O 的结束扇区地址是完全连续或者相隔不多的。在顺序I/O访问中,磁盘所需的磁道搜索时间较少,读/写磁头可以以最小的移动访问下一个块。

  • 随机IO - 是指读写操作时间连续,但访问扇区地址不连续,随机分布在磁盘LUN的地址空间中。


       随机I/O可能是因为磁盘碎片导致磁盘空间不连续,或者当前block空间小于文件大小导致的。连续 I/O 比随机 I/O 效率高的原因是:在做连续 I/O 的时候,磁头几乎不用换道,或者换道的时间很短;而对于随机 I/O,如果I/O 很频繁的话,会导致磁头不停地换道,造成效率的极大降低。如图,我们看到不同磁盘 顺序写和随机写的性能差异,差不多都是数量级的差距。即便SSD的硬盘,希捷公司也有明确的说明,大家可以参考链接查看测试数据。


      所以随机IO和顺序IO对存储系统的写入性能有很大的影响。那么这些差异和时序数据场景有什么关系呢?我们继续聊。。


      首先我们还是要切回时序数据场景的特点IoT场景是写多/读少的场景,写入性能至关重要。所以基于关系数库,也就是Btree的存储结构的时序数据块在存储时序数据时候都存在IO效率低下,写入速度很慢的现象。那么需要如何解决呢?


       解决BTree的写入问题一般有两种方式,一种是是COLA(Cache-Oblivious Look ahead Array)- tokuDB。另一类就是我们今天要重点分析的,LSM tree(Log-structured merge Tree)结构。这个数据结构的插入复杂度是O(1),这个比B+Tree的logN要优秀很多了。


       目前基于LSM Tree数据结构的存储有很多,如著名的KV存储HBase还有LEVELDB,RocksDB等等。


       那么为了解决时序数据写入问题,时序数据库阵营出现了一些基于KV存储的时序数据库,比如大家熟知的,基于HBase的OpenTSDB。


       LSM Tree全称是 Log-structured merge  Tree,Merge就是合并,Log structured就是像写日志一样进行顺序写入,append only的方式。




       LSM树的核心思想就是放弃部分读能力,换取写入能力的最大化。核心思路其实非常简单,就是假定内存足够大,因此不需要每次有数据更新就必须将数据写入到磁盘中,而可以先将最新的数据驻留在内存中,等到积累到一定程度后,再使用归并排序的方式将内存中的数据合并追加到磁盘队尾。我们来看一下这个基于LSM Tree的写入过程:

      首先我们要解决如何做到顺序IO,就是一个数据写入到来之后,先进行WAL的落盘。那么如何解决乱序?那就是,写WAL是为了恢复,真正的有序写入要将数据写入内存,也就是Mem-Table,然后对Mem-Table进行排序,数据写入到内存之后,就表示写入成功了。那么写到内存之后会怎样操作呢?就是要解决落盘问题。当内存数据到达一定规模,就需要写入磁盘,LSM Tree的做法是将要刷磁盘的Mem-Table变成immutable,刷磁盘同时不影响写入请求,在创建一个新的Mem-table。这样的持久化在数据持续变化和查询的时候会有什么问题吗?当然有,比如,key更新/删除了怎么办?要查询的key在多个文件怎么办?所以,LSM Tree结构还需要有一个Compactions的阶段,以及对数据创建索引的过程。我们以HBase的HFile为例,存储结构如图,文件中除了数据还需要有索引和Boolmfilter的机制进行加速查询。


      上面我们了解了LSM Tree的写入逻辑,那么查询逻辑是怎么样的呢?查询逻辑最核心的是要查询索引,首先在内存Mem-table里面查询,然后在immutable Mem-table里面进行查找,然后是磁盘Flie里面进行查找。当然这里有Bloom filter辅助查询。Bloom filter本质就是一个bitmap,每个key数据用k个独立的hash就行计算,填充bitmap,数据查询时候Bloomfilter说没有一定没有,Bloomfilter说有,不一定有,还要继续索引查找。

       LSMTree虽然以牺牲查询为代价来解决Btree写入问题,但是会利用一些辅助手段,比如索引,Bloomfilter基数估计等手段加速查询,同时各种数据库还会有不同的Cache机制加速查询。这样听起来基于LSMTree结构的KV存储应该有很不错的读写表现。那么基于KV存储的时序数据库应该怎样设计呢?

       我们还是以大家熟知的基于HBase的OpenTSDB为例来进行分析,看看KV存储在设计时序数据存储时候的设计技巧和存在的问题。


       以HBase为例,我们聊聊KV存储对LSMTree的应用,如图大家应该很熟悉,HBaseClientHBaseRegionServer发起读写请求,当然HLog就是LSMTree中的WAL部分,MemStore就相当于LSM里面的Mem-table部分,HFile就是持久化文件。


      简化一下就是HBaseHRegionServer包括HLogHRegion,一个HRegion包括很多的HStore又分为MemStoreHFile两个核心部分。


      其中按照Rowkey进行region划分,每个region对应一个ColumnFamily,一个MemStore


      每个ColumnFamily里面又有若干Qualifiler。当然任何一个KV数据又会包含一个时间戳Timestamp


      那么按照这个架构,一条完整的数据是怎样的呢?




       如图这是一条完整的KV数据,开头KeyLengthValueLength两个固定的长度,分别代表KeyValue的长度。

  在Key部分:RowLength是固定长度的数值,表示RowKey的长度,Row 就是RowKey值。

       Column Family Length是固定长度的数值,表示ColumnFamily的长度 

      接着就是ColumnFamily再接着是Qualifier然后是两个固定长度的数值,表示TimeStampKeyTypePut/Delete) 

       Value部分没有什么复杂的结构,就是纯粹的二进制数据,这个也是很大的成本问题,后面我们会介绍。


       那么这里有两个常见问题,一是按Rowkey进行Region的划分,热点问题怎么处理?二是按ColumnFamily进行Store存储,如何设计ColumnFamily包含的Qualifier这个磁盘块,磁盘IO有什么关系?


       我们知道HBaseRowkey都是字典序的,一般Rowkey的典型组成会是:业务KEY+时间倒序(Long.MAX_VALUE -timestamp/加盐/Hash组成,那么时间倒序有利于最新的数据在最前面,加盐或者Hash可以缓解热点问题。关于ColumnFamilyQualifier的设计,简单讲一个原则就是经常一起查询的数据要作为同一个ColumFamilyQualifier


       除了这两个问题,我们还有2个问题需要考虑,那就是数据查询时候如何定位一条数据属于哪个RegionHFile里面的KV数据如何快速读取?



       回答Region寻址和HFile快速读取的问题,也要分析HBaseRegion的管理方式,Region信息属于HBase的内部抽象,对Region的管理HBase也建立了一颗B+树,我们前面聊过,如果一个数据块事16K,那么一个3阶的B+Tree能容纳2000w的数据量,一次查询只需要3IO,那么HBase数据块默认大小事64M(可以配置更大),那么这些Region一般会加载在内存中,所以在B+Tree的数据机构上HBaseregion的寻址非常快速。


       同样在HFile里面定位rowkey的位置也是基于二分查找的,如图查询Rowkeyfb的过程,图中红色箭头部分,第一层fam之间,所以查询a的子树,第二层fdh之间,所以查询d所指向的子树,第三层我就找到了f这个值,然后在第四层在查找fb数据最终返回数据。


       这里提醒一下HBase存储是LSMTree数据结构,但是为了加速查询内部对Rowkey的索引建立和Region的管理都是采用了B+Tree的数据结构。



       好了,解了HBase的基本内容之后,我们思考一下,我们如何基于HBase设计一个时序数据库呢,首先要思考的问题就是,如何在HBase中对时序数据的核心概念进行抽象映射。也就是时序数据核心概念是Metric相关内容,Tag相关内容,和Timestamp时间戳。HBase现有的概念是ColumnFamily,ColumnQualifier当然也具备Timestamp信息。


       那么我们无法改变HBase的KV数据结构,只能在表达时间序列数据的同时最充分的利用HBase现有的数据结构抽象,最核心的就是Rowkey的设计和ColumnFamily和ColumnQualifier的利用技巧。我们逐一看一下。




        那么基于HBase的时序数据库OpenTSDB是如何设计的呢?

     我们先看最核心的OpenTSDB的RowKey Schema的设计,其组成是salt] <metric_uid><timestamp><tagk1><tagv1>[...<tagkN><tagvN>]


       那么,按照HBase的思维这里有一个很奇怪的问题,就是【salt】设计竟然放在了rowkey的最前面,这个Salt解决了什么问题,同时有带来了什么问题呢?


       当然Salt部分和前面我说的HBaseRowkey里面也可以加Salt的作用是一样的,解决热点问题。但是这里问题是,在查询的时候用户不知道salt的值如果进行查询呢?这肯定是带来了数据查询的性能问题。其实OpenTSDB里面Salt是分桶的抽象,默认20个桶,用户可以配置,那么查询时候同一个业务rowkey就要查询20次然后在进行merge,这个会极大的影响get查询。



       好,那么通常Rowkey设计不携带salt部分,同时OpenTSDBRowkey设计上也下了不少功夫,首先大家看到Rowkey的开始是一个metricuid


      如图OpenTSDB里面的一个rowkey示例,包括 metric部分,时间戳部分,tagktagv的组成。


      我们以前面我们风力数据采集的信息为例,Rowkey就应该是speed+ts+city+hangzhou+region+xihu


      那么这里面我们还没有解释uid的作用,后面我们慢慢进行说明。现在要考虑的问题是,Metric&Time&Tag这个在rowkey中的顺序可以变化吗?或者说如何设计最合理呢?




       HBase是按照rowkey的字典序顺序排列的,为了减少查询IO,最好将相同的Metric写到相同的磁盘块,所以OpenTSDB需要将Metric作为Rowkey的最前面。

       Timestamp没有业务语义,不适合放到最前面(如何放到前面可能出现,多个Metric会混杂在一个磁盘块,不利于查询,写入也会造成热点问题)

       Tags放在最前面,会造成大量的Scan操作,比如,用户指定多个Tags中的某个查询,而且不是前缀Tag的话,就会在HBase里面将会变成大范围的扫描过滤查询,当然Tags放到Timestamp前面也存在Scan的问题。


       好,思考了这些现实问题,OpenTSDBrowkey的设计就是根据用户的metric+ts+tag信息的方式组成。那么目前看这个rowkey的设计还是很合理的,那么是否也在海量的时序数据场景有一些明显弊端呢?



       很明显,这个key组成里面根据业务的不同会变得复杂,首先就是HBasekey里面有timestamp,是一定要有的,然后业务的rowkey里面也是有时间戳信息的。


      为了套用先有的HBase结构,存储中有很多无用的信息,比如comumnFamily部分,KeyType部分等。同时Rowkey里面的metric是一个业务字符串,这些数据在实际存储过程中很多冗余,造成存储成本的浪费。


      还有HBase是弱类型问题,不能对Value部分根据业务的不同字段类型进行专门的压缩。同时,Rowkey部分包含很多tag信息,没有对Rowkeytag进行倒排索引,同样使得查询受限。大量scan操作性能很差。所以基于KV的时序数据库存在客观的设计弊端。



      面对HBase设计时序数据存储的弊端OpenTSDB做了哪些比较核心的优化呢?


      首先是Rowkey里面的Timestamp时间粒度不是毫秒,而是小时,这样极大的减小了scan的部分,一小时的数据可以一个get进行获取。另外我们一直提到的metric_uid,其实也是对存储的优化,将业务字符串映射成uid,可以极大简化存储。还有在Compactions部分,会将很多Qualifier数据合并成一个Qualifier里面,提高压缩效率。



       我们细致的看一下OpenTSDB的data Scheam和uid mapping的Schema。


       最核心要了解的就是RowKey部分,我们看到很多的编码,我们看到编码后的rowkey和上面编码前的字符串有极大的改进。


       关于UID,OpenTSDB里面采用3个字节存储,约1600万个值,足够满足大部分业务场景,同时我们也可以根据业务规模改变UID的生产策略。这样极大的节省存储和key排序成本。


      同时我们看到,TSDB-UID的映射表,是双向的,既可以同UID找到key,也可以通过key找到UID。大大提高搜索效率。




       OK,坦白讲,不论OpenTSDB怎么优化,首先都需要先填补HBase设计在时序数据库场景适配的坑,再进行优势发挥,那么这样的客观事实,势必会催生为IoT时序数据而生的原生时序数据库。


       一个是大家熟知的时序数据库领域的领头羊Influxdb,另一个是2020Apache 新晋的顶级项目ApacheIoTDB。我们分别探讨一下。



       InfluxDB在时序数据库排名第一,这个成绩不是浪得虚名,而是实至名归,有句话叫,天才出于勤奋,其实InfluxDB从诞生伊始,一直到现在都在不停的努力,不停的为解决现实问题而优化改进。我们接下来都是基于InfluxDB1.8版本进行描述的。


       我们了解一下Influxdb 引擎升级的历程,最初为避免B+Tree带来的写入问题,InfluxDB采用了LSMTree的存储设计,底层引擎采用LevelDB,但是后面发现LevelDB存储不能热备份问题,于是底层引擎又采用RocksDB解决,但是RocksDB当时又存在时序数据场景冷数据的高效删除问题,我们知道海量的数据通过api的方式逐条删除是相当耗时的(会死人的),所以InfuxDB又进行改进引入Shard,每个shard存储一片时间连续的数据,冷数据都是时间较老的数据,一个shard对应一个底层LevelDB数据库,要删除的时候只需要关库,删文件即可。但是这个设计又带来了新的问题,就是系统的文件句柄问题,后面又有BoltdDB来解决,但是BoltDB里面利用了mmapB+Tree的数据结构,B+Tree的随机写问题又糙成了IOPS限制问题,所谓痛苦造就成就,种种投产问题推升了目前InfulxDB全新的存储和索引架构,TSM架构。


       接下来我们看看目前InfluxDBTSM架构细节,尝试 尽量 知其然,知其所以然。Lets go...



      首先,不可避免的我们要了解InfuxDB针对时序数据场景的核心概念的抽象,我们看InfluxDB新增了一些抽象,一个是database,一个是retentionpolicy,尤其retentionpolich是专门针对时序数据的冷数据设计的,原生的时序数据库的优势就是在设计之初就会考虑时序场景的种种典型问题。那么这些概念的层次结构是怎样的呢?


       首先Database是一个最上层的抽象,或者说是管理单元,然后数据也是要按时间进行Sharding的,每个shard都归属于一个ShardGroup,然后Shard里面就是具体时序数据了,当然包括时间戳,SeriesField信息,其中Series就包含了metrictag信息。上面的RP就是retentionpolicy


       同时Point相当于一行数据,如代码片段所示,包括被排序的keykey的组成是measurementtags的组合,后面我们还会大量的涉及大key的介绍。好,简单了解了这个层次逻辑,我们再简单看看这些概念的代码抽象。



       首先是StoreStore是为Database管理shardsindex的抽象。


       其中InfluxDB的索引机制有两种,一是基于内存的版本,一是基于文件的版本,后面我们逐步详细介绍。


       我继续向下进行钻取,那就是文件存储的分区管理Shard的代码抽象,Shard里面会包含时序数据和TagSeriesKey的倒排索引。大家会发现,有了tag的倒排索引,基于HBaseOpenTSDB里面根据一部分tag查询的scan操作就可以避免了。


       那么Shard里面还有一个EngineEngine负责进行合并多shard的查询结果等工作。Engine代表了一个存储引擎,这里面包含了TSM架构最核心的组成部分,WAL/CACHE/TSMFile/Compaction/Compression组件。我们后面一一介绍。在往下就是FileStore部分了FileStore里面包括很多的TSMFile那么一个TSMFile是怎样的结构呢?如图包括4个部分。


      其中Blocksindex部分尤为重要,我们先看看数据是如何写入的...



      首先,一个写请求到来之后的写入流程如下:

  • 第1步,进行AppendOnlyWAL写入。

  • 第2步,然后就要更新索引,这个是加速查询的本质,包含了tagserieskey的倒排索引内容。

  • 3步,就是数据更新到缓存,相当于LSMTree里面的Mem-table角色。

  • 4步就是返回客户端成功应答。


      当然数据不能一直保留在内存,我们需要某种机制进行落盘处理。同时落盘的时候最好不要影响写入请求。


  • 第5步,落盘的过程在InfluxDB里面叫做Snapshotting。进行快照的时机有2个,一个是内存使用达到预定的阈值大小,一个是给定时间间隔没有数据写入。

    • cache-snapshot-memory-size= ”25m”

    • cache-snapshot-write-cold-duration= “10m”


       落盘之后我们就变成了Level的文件InfluxDB设计4层的TSMFlile,每个TSMFile内部又有精巧的结构设计。


       这里再特别注意一下内存中Cache结构,是一个SortedMap结构。


       同时说明一下,在目前InfluxDB1.8)版本,我没有看到做快照时候将Cache变成只读,快照影响写请求,然后写完清空Cache


  • 第6步,我想正是因为这个Cache没有只读动作,在进行Compaction时候也考虑了低层级采用低CPU消耗的算法,缓解对写入的影响。而高层级的文件合并就要考虑压缩成本。


       在回到索引部分,我们刚才提到InfluxDB有两种类型的索引可以选择,一个是基于内存的,一个是基于文件的。在索引的构建中也绞尽脑汁,利用了可用的一切优化手段,B+TreeBloomFilterHashIndex等等来加速查询。



       那么,我们来哦细究一下最核心的部分,内存Cache里面的数据结构是如何设计的,有怎样的优势呢?


       首先设计的初衷一定是在SeriesKey有序的前提下,高性能写入。


       如图,Cache内部提供了一个ring结构,来对数据进行分桶/分区管理理论上分区的数量最多水256个,为啥呢?因为InfluxDB采用 SeriesKey的前8bits进行Hash8bit最多256个值。而每个Partiton的数据存储是一个sortedmap,包括SeriesField时间戳信息。


       OK,这里YY一下,按照代码里面的注释这个取余操作是可以优化的。大家感兴趣可以看看源代码,怎样优化最好。:) 欢迎线下讨论...



       我们根据代码的的分析,可以得到Cache的数据结构是这样的,内存数据存储结构是partiton+map的层级管理,那么这样的数据结构有什么好处呢?


  • 环结构可以Split数据结构,降低读写时候锁的竞争

  • 取前8bit进行hash,确保连续数据存储在一个磁盘块(时间连续)

  • Map是有序的,确保在Snapshoting时候可以快速有序落盘

      接下来我们再来看看TSMFile细节内容。


       好,是时候聊聊TSMFile的数据结构和对读/写性能和存储成本的设计考虑了。


      首先,TSMFile的四核部分中,Header 是5个字节存放Magic和版本,Footer是8个字节,记录TSMFile中索引数据在TSMFile的偏移量。Blocks里面存储很多数据块,索引部分的Key是SeriesKey+Field。


      其中IndexEntry对应一个Block进行索引的构建。包括数据块包含数据的最小最大时间,以及数据块在TSMFile中的位置。


      Block部分除了CRC的数据校验部分,还有数据类型,时间戳信息和数据值。


      InfluxDB目前支持Integer/Float/Boolean/String等数据类型,每种数据类型都有专门的压缩算法。



      对于不可或缺的Timestamps信息,InfluxDB采用Facebook的Delta-delta算法,极大压缩有序时间戳的存储。


      同时在Index结构设计上也考虑了时间要素和区间查询。索引部分利用minTime为每个block的indexEntry构建基于B+Tree索引树,一遍更要的加速查询。




       这里,我们提出一个问题,在Footer中记录整体IndexTSMFile的偏移量,如果我想获得所有Key,需要怎样操作?我们看这个数据结构,会发现很明显是需要根据offset定位到Index的区域,然后读取所有的index数据。这样其实这是一个很耗时,耗空间的操作,需要将整体index进行读取,那么是否可以优化一下呢?


       当然InfluxDB对索引进行了优化,为每一个Key的索引信息都添加了offset信息,维护了一张offset表格。


       在代码的抽象上在IndirectIndex的数据结构中有offsets的数据维护,这样想要查询所有Kyes的时候,我们读取offsets信息根据偏移直接获取对应key的信息。



      在继续拷问还有什么其他优化吗?


       回答还是肯定的,在Measurement索引区域进行tagSeriesKey的倒排索引结构。这在很大程度增加了查询的灵活性和查询性能。


       同时我们发现在tagkeySeriesKey的映射中,我们还有seriesIDSeriesKey的映射维护,这又是出于怎样的考虑呢?对,这里是对存储成本也进行了考虑。




      这是索引文件TSI的结构设计,包括4个部分,

  • 首先是Trailer层面,这是索引的入口,记录Measurement/Tag/Series三个Block在TSI的偏移量(offset)和大小

  • 然后是Measurement block,记录所有的指标信息,也就是measurement(表)的Meta信息。

  • 然后是 tag信息部分,这个部分核心维护了面介绍的倒排数据结构<tagkey, <tagvalue, list<seriesID>>>

  • 最后series Block索引区域,记录了database中所有的SeriesKey信息


      当有了SeriesKey和时间戳之后,我们就可以进一步在Cache/File中进行查询,找到对应的分桶和对应value的时间序列值。


      找到对饮的TSMFile对应的数据块之后,加载数据块到内存,根据TSFMFile中的B+Tree树索引,二分法找到具体的查询值。


      具体的查询逻辑如图标注。




       OK,总之InfluxDB利用TSMTSI架构,完美解决了高吞吐,热备份,冷删除,高性能的查询,和基于tag的倒排索引索引所支撑了灵活的业务查询。

       这里特别提醒InfluxDB自动根据Tag进行倒排索引的建立,在实际业务中要充分利用。


       当然InfluxDB的龙头地位除了存储查询设计的优势,还有很多实用的功能,比如ContinuousQueries。这个的实现本质就是定时调度,我们可以根据语法很清楚的了解设计的语义。其中 EVERY是计算频度,FOR 是计算区间,GroupBy的时间区间,就是每次计算的数据窗口。


       我们简单看一个示例EVERY1小时,For90分钟,groupby30分钟。的业务含有是,1小时调度一下计算,每次计算的数据处理区间是90分钟,业务聚合计算的窗口数据30分钟的数据。


       我们假设8点中触发了计算,那么计算数据区间是6:308:0090分钟),计算窗口是30分钟,也就是90分钟的数据计算三次,有三个计算结果。


       当然1小时触发一次,到9点还会再次出发计算逻辑。



      OK,再来看看另外一个Apache开源社区优秀的时序数据库,ApacheIoTDB


      首先是够新,ApacheIoTDB20209月从Apache 孵化器毕业,成本Apache顶级项目的。


      第二是ApacheIoTDB足够强大,左边的写入和查询的性能测试已经超越了目前的领头羊InfluxDB


      那么,为什么IoTDB会有这样的优秀表现呢?



       首先ApacheIoTDB也是在LSMTree的基础进行了架构优化,提出了tLSM的存储架构,在tLSM的架构中,优化了LSM中的Mem-table部分,提出了为乱序数据提供独立的处理逻辑,这在写入上有很大的优势。同时IoTDB更加注重从SQL的角度进行查询优化,让用户的查询逻辑智能的获取最优的查询性能。OK,我们再看看目前IoTDB整体组件栈是一个怎样的情况呢?一起来看一下...


       图中,棕色部分是现在发布的版本已经具备的,橙色部分是后续发布版本中陆续规划的,当然还有一些有待进一步讨论和社区交流的部分。那么就IoTDB的现有功能来说,已经得到了一些工业企业的认可,我们一起看一下几个投产的案例。



       这是IoTDB的一部分工业企业用户,包括上海的地铁项目,这个场景1IoTDB实例就支持了每天4000多亿的海量时序数据采集和存储。关于IoTDB的分享还有太多的内容可以交流。。。时间原因我们将内容留在下次:)


    好,天的分享就到这里,我们下次有机会一起探讨ApacheIoTDB的技术细节:)谢谢大家!



扫码关注

关注我们

联系我们

Contact Us

* 为必填信息