No.1-Apache IoTDB 隨筆 - Time Series DBMS 綜述

大家好,很開心能夠和大家一起交流時序數據庫的相關的內容

首先還是簡單自我介紹一下,我是 孫金城,花名 金竹。我是2011年加入阿里,在2016年之前一直做公司內部的研發工作,包括阿里郎,Blink等平臺。

從2016年到現在我一直重心在開源建設上面,包括ApacheFlink/ApacheBeam/ApacheIoTDB,在這個過程中也得到了開源的一些肯定,目前是BeamCommitter,ApacheFlink和ApacheIoTDB的PMC,也是Apache Member,目前全球華人大概有30+的ApacheMember,當然,隨着開源的越來越熱,國內每年參與開源建設的同學也在逐漸的在增加。

那麼2020之後會有怎樣的規劃呢?本着但做好事,莫問前程的心態,會多多在訂閱號中記錄我在流計算和IoT方面的認知。最終努力做到走進阿里/踏入開源,成爲最好的自己.

那麼爲什麼一直做流計算會慢慢選擇瞭解IoT相關領域呢?因爲在馬老師看來“5G時代,加速的不僅僅是通訊行業,而是更多的促進物聯網(IOT)領域的發展。IoT將是一個新的浪潮。那麼我參與IOT領域的切入點是什麼呢,就是從瞭解時序數據庫 進行着陸的...

如圖,這不是一篇經驗分享,而是一個學習過程分享

瞭解時序數據庫最先想到的是: 瞭解一下時序數據庫的現狀及發展趨勢,那麼這個數據的權威性,大家應該有所共識,在db-engines網站的排名應該很客觀的。如圖,從2018年開始,TimeSeriesDBMS的關注度就持續迅猛的增長。其實不僅僅db-engines網站有這樣的數據,Gartner也給出了預見性的判斷。我們一起看一下...

在2019年"賦權的邊緣" 就成爲了十大戰略性技術趨勢之一,邊緣由物聯網驅動,需要使處理計算更接近端而不是集中式的雲服務器。當然早在2018年 "雲向邊緣計算挺進" 的趨勢就已經非常明顯。邊緣設備的信息的收集/存儲和計算需求與日俱增,一直到目前2020年,更多的計算和存儲能力都逐漸下沉到設備端,雲邊端一體將是今後幾年持續的焦點。設備數據具備時序性,而作爲物聯網領域具備存儲和計算能力的產品就是時序數據庫。那麼目前業界時序數據庫的陣營是怎樣的狀況呢?我們繼續看一下...

同樣出自DB-Engines的排名信息,目前有幾十種(大概34種)時序數據庫,大家熟知的InfluxDB自2016年以來穩穩的位居榜首,隨着2018年IoT領域的崛起,InfluxDB的熱度也持續飆升,穩穩地龍頭位置,那麼InfluxDB爲啥如此受到時序數據存儲技術的青睞呢?我們接下來就會和大家細緻進行分析...

其實時序數據庫早在1999年就已經有RRDtool,全稱RoundRobin Database。新數據會自動覆蓋老數據,也就是考慮了時序數據的時效性,在2008年又出現了Graphite,Graphite是一個用於採集網站實時信息,並進行統計的開源項目,可用於採集多種網站服務運行狀態信息。隨後就是大家熟知的OpenTSDB,InfluxDB,還有feacebook的內存版時序數據庫,再有就是非常著名的用於監控的Prometueus,2017年,2018年的時候時序數據庫已經發展的非常迅速來,這個在剛纔的DB-engines網站數據和Gartner給出的戰略技術趨勢信息是非常吻合的。那麼在2020年,Apache開源社區又出現了時序數據庫的黑馬 ApacheIoTDB,將來還會有哪些時序數據庫產品呢?我們讓時間來揭曉。

那麼,在這裏,拋出一個問題,就是:“時序數據庫難道不能直接存儲到關係數據庫嗎?”爲啥要造出很多時序數據庫,時序數據庫和關係數據庫又有怎樣的本質區別呢?

其實數據存到哪裏合適,還是要看數據本身的特點,以及數據處理的需求。面對IoT領域,時序數據有很多的數據來源,比如汽車,火車,飛機等交通工具,以及我們越來越被大家認可的智能家居產品等。

當然最大的數據量來源還是工業領域的各種設備傳感器數據,這些設備的工況數據收集和處理將給存儲和計算帶來巨大的挑戰。

我們以一個具體的案例來說,這是GoldWind發電數據採集,GoldWind有超過2w個風機,一個風機有120-510個傳感器,採集頻率高達50Hz,就是每個傳感器1秒50個數據點採集峯值。這要算下來就是每秒5億個時序指標點的數據。

這個數據量讓數據採集/存儲/計算面臨很大的挑戰。同時還有我們業務中的一些非常常見的採集/查詢需求,20萬/秒的吞吐需求是非常常見的,但是這樣的需求單機關係數據庫也是很難搞定的,我們接下來會分析一下具體原因。

好的,那麼還是簡單梳理一下時序數據的特點,首先時序數據有特定的一些概念。如圖:Metric,就是我們要採集的指標,類似一張表,還有Tag,就是metric的屬性特點,比如指標屬於哪個設備,哪個區域等等維度信息。再有Field就是真實要採集的指標名稱。那麼非常重要的時序數據一定有數據產生的時間,就是Timestamp時間戳信息,指標數據的時效性也是非常重要的,所以要有時間信息。那麼Point就是類似表的一行數據。我們以風力 指標 爲例 簡單圖示一下這些概念。

Wind-force就是Metric,其中 city,region就是Tag,speed和direction就是Field。當然Timestamp不用多說就是指標數據產生的時間了。

那麼這種關係設計的方式大家不難發現,tag的數據存儲會有很多的冗餘。這是關係數據庫存儲時序數據的一個涉及到存儲成本的問題。當然問題不僅如此,我們繼續往下看。

現在,我們簡單總結一下時序數據場景的特點或者說是需求核心會涉及到寫入/讀取/存儲成本三個維度。寫入要支撐上千萬甚至上億的吞吐能力;讀取就需要根據不同的業務有不同的讀取需求,後面我們會慢慢分析到。當然面對成本問題,海量數據的存儲成本無疑是時序數據庫設計的重中之重。所以,面對這樣的時序數據場景的特點,時序數據存儲到關係數據庫就會有很多弊端,比如剛纔說的數據冗餘造成的成本問題,還有寫入的吞吐問題等,那麼我們接下來就要討論這些問題存在的本質原因...

好,回過頭來我們在來看看目前的時序數據庫從架構的角度有哪幾種?

第一種,就是在關係數據庫基礎上進行改進的時序數據庫,比如基於PG開發的Timescale。

第二種,就是在KV數據庫的基礎之上進行改進的時序數據庫,比如,基於HBase開發的OpenTSDB。

第三種,就是爲時序數據量身定製的時序數據庫,那麼目前的領頭羊就是InfluxDB。當然,我前面說過,我也很看好2020新晉的Apache頂級項目ApacheIoTDB。

這三類時序數據庫又怎樣的特點呢,我們接下來逐一進行討論分析...

我們先來看看基於關係數據庫的時序數據庫,既然基於關係數據庫,那麼我就要聊聊和關係數據庫密切相關的存儲結構。

首先我們看一個簡單的二叉樹,我們知道二叉樹是樹形結構的一個重要類型。許多實際問題抽象出來的數據結構往往是二叉樹形式,而且二叉樹的存儲結構及其算法都較爲簡單,二叉樹特點是每個結點最多隻能有兩棵子樹,且有左右之分。那麼我們看看怎樣向二叉樹裏面插入一個數據點呢?

如圖,我們插入節點10,插入的過程是二分法,即使從整個節點的數值的中間開始,如果要插入的值比這個數小就從左子樹繼續二分查找,如果要插入的數據比當前數大,就從右子樹繼續查找,那麼按照這個算法,二叉樹的寫入和查詢的複雜度都是logN。

我們再舉一個小例子,可能目前我們講解的內容很簡單,大家都很熟悉,不過爲了爲後面的內容做鋪墊,大家還是稍微耐心聽我多囉嗦幾句。這個例子,就是假設我們有如圖8個數據,我們如果想要查找數據2,那麼我們先看中間數據(第4個)點,13,比較一下13比2大,所以要查詢的2一定在13的左邊,我們再從左邊的數據進行二分查找,左邊的中間數據5,發現5還是比2大,那麼就需要查找左子樹,目前只剩下了2,當然也就是我們要找的數據了。那麼logN其實就是log2N對吧,集合8個數據,查找一條數據就是log8,2的3次方等於8,所以log8就是3次,這是二叉樹的查詢複雜度的一個簡單示例說明。OK,這個二叉樹介紹呢有點浪費大家時間,廢話有點多,大家見諒,那麼,接下來我們就介紹關係數據庫中使用的存儲數據結構。

在實際的關係數據庫中有2種數據存儲結構,一個是BTree一個是B+Tree,那麼,這兩種數據結構格局特色,都有使用的場景。那麼這兩種數據結構的查詢複雜度是怎樣的呢?有什麼本質區別。

BTree的寫入成本是LogBN,B就是樹的階數,如圖就是3階BTree,每個節點的黃色方框表示的指針個數就是樹的階數,藍色部分就是具體構建BTree節點的數值,如果是索引樹的話,藍色部分就是構建索引的key的值,比如訂單號之類的關係數據庫的索引字段。

B+Tree的寫入成本是LogBN,如果都是LogBN那麼Btree和T+Tree有啥本質區別呢?這要看看BTree和B+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值查找。這個大家如果感興趣,可以思考一下,如果有問題歡迎留言:)

OK,放鬆3秒鐘,然後繼續分析 B+ Tree ?我們繼續和這個問題死磕...

要想清楚上面的問題,我們還需要一些算法之外的輔助內容,雖然是輔助但卻是和存儲系統密切相關的部分。那就是磁盤。在存儲領域選擇BTree還是B+Tree和磁盤IO有着直接的關係。那麼什麼是磁盤IO呢?

我們知道計算機的CPU就如同人的大腦,大腦根據外部輸入進行思考,併爲我們下達行爲命令,那麼CUP同樣要依託於外部輸入進行計算,然後將計算的結果再向外輸出。那麼我們存儲裏面的磁盤IO可以簡單理解成從磁盤讀取數據和向磁盤寫數據的過程。

這個圖應該是我們上學課本或者數據庫理論方面書籍裏的圖片,處理器,數據總線,主存,磁盤等等,我們再熟悉不過的名詞,當然內部也是非常複雜的,我們今天挑與我們要討論的問題相關的內容進行分析。

那就聊聊磁盤,首先磁盤內有很多的盤片,每個盤片又由若干扇區組成,扇區是磁盤讀寫的最小單元,每個扇區多大呢?一般是512個字節。好,那麼這個和數據庫又有啥關係呢?

數據庫的數據就存儲在磁盤上,但是從數據庫系統角度如果每次讀取一個扇區的數據就太慢了,所以數據庫一般會有數據塊的抽象設計,數據都是一個數據塊讀取一次磁盤,一般數據塊大小是4~64KB,那麼重點來了,通常讀取一個數據塊的磁盤IO需要消耗大概10ms左右的時間,這是非常耗時的操作了,所以我們在數據庫設計的時候一定要儘量減少磁盤的訪問次數。那麼MySQL的InnoDB默認數據塊的大小是16KB,當然這是可以配置的。不同數據庫默認值不一樣,比如HBase存儲的數據塊大小應該默認64M。好的,說到這裏,不知道大家是不是知道我接下來要說什麼了?有這方面經驗的估計秒懂,但是第一次思考這個問題的,還是需要些解釋的,我們往下繼續進行...

OK,我們還是舉一個例子來討論,假設以16K爲塊大小,也就是每16K數據需要訪問一次磁盤,我們的目標是減少磁盤I/0,那麼數據相同的情況下,B-Tree和B+Tree哪個磁盤I/O更少呢?假設我們有一個查詢:select from device where deviceId=38*deviceID是一個bigint的8字節類型,作爲索引key。我們分別分析一下,Btree和B+Tree哪個訪問磁盤更少?

主鍵deviceId爲bigint類型,長度爲8字節,而指針大小在InnoDB源碼中設置爲6字節,這樣一共14字節,也即是如圖一個藍色框數據和一個橙色框指針需要14個字節存儲。那麼,我們一個數據塊中能存放多少這樣的單元,就代表一次可以在內存加載多少數據key(deviceID)和對應的地址指針,即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個指針(剛纔我們計算過),那麼我們可以計算第三層的記錄行數了,那就是:1170117016=2000w+。也即是3層的1170階B+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索引樹呢?

好,首先來的數據是3,35,90我們構建一個樹的形態如圖,35是root,左右各有一個孩子。

然後又來了17和26,二分的方式,17和26都小於35,所以都在35的左子樹,但是這裏有個問題?

就是我們是一個3階Btree,指針有3個,節點只能有2個,所以3,17,26已經超了,我們要進行節點的分裂,後面存儲層面就涉及到磁盤塊的分裂。

好,我們進行節點分裂之後,樹的樣子變成,root節點有2個key3個指針。我們繼續看後面的變化。。。

我們看看後面來了哪些數據。。。樹的變化如圖,這裏提醒一點,中序遍歷的不同的樹形狀可以得到相同的結果。我們這個例子核心是想說明數據的構建過程有節點的分裂,也意味着存儲有磁盤塊的分裂,會產生大量的隨機磁盤IO。我們繼續看變化。。。

接下來再後面的數據。。好,36,87,29,65,75到來後,樹的形狀如圖。。。(這棵樹是故意弄的很平衡哈:)

最後,我們看看有哪些數據到來,最終樹的形成。基於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的應用,如圖大家應該很熟悉,HBaseClient向HBase的RegionServer發起讀寫請求,當然HLog就是LSMTree中的WAL部分,MemStore就相當於LSM裏面的Mem-table部分,HFile就是持久化文件。

簡化一下就是HBase的HRegionServer包括HLog和HRegion,一個HRegion包括很多的HStore又分爲MemStore和HFile兩個核心部分。

其中按照Rowkey進行region劃分,每個region對應一個ColumnFamily,一個MemStore。

每個ColumnFamily裏面又有若干Qualifiler。當然任何一個KV數據又會包含一個時間戳Timestamp。

那麼按照這個架構,一條完整的數據是怎樣的呢?

如圖這是一條完整的KV數據,開頭KeyLength和ValueLength:兩個固定的長度,分別代表Key和Value的長度。

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

Column Family Length是固定長度的數值,表示ColumnFamily的長度

接着就是ColumnFamily,再接着是Qualifier,然後是兩個固定長度的數值,表示TimeStamp和KeyType(Put/Delete)

Value部分沒有什麼複雜的結構,就是純粹的二進制數據,這個也是很大的成本問題,後面我們會介紹。

那麼這裏有兩個常見問題,一是按Rowkey進行Region的劃分,熱點問題怎麼處理?二是按ColumnFamily進行Store存儲,如何設計ColumnFamily包含的Qualifier?這個磁盤塊,磁盤IO有什麼關係?

我們知道HBase的Rowkey都是字典序的,一般Rowkey的典型組成會是:業務KEY+時間倒序(Long.MAX_VALUE -timestamp)/加鹽/Hash組成,那麼時間倒序有利於最新的數據在最前面,加鹽或者Hash可以緩解熱點問題。關於ColumnFamily的Qualifier的設計,簡單講一個原則就是經常一起查詢的數據要作爲同一個ColumFamily的Qualifier。

除了這兩個問題,我們還有2個問題需要考慮,那就是數據查詢時候如何定位一條數據屬於哪個Region?HFile裏面的KV數據如何快速讀取?

回答Region尋址和HFile快速讀取的問題,也要分析HBase對Region的管理方式,Region信息屬於HBase的內部抽象,對Region的管理HBase也建立了一顆B+樹,我們前面聊過,如果一個數據塊是16K,那麼一個3階的B+Tree能容納2000w的數據量,一次查詢只需要3次IO,那麼HBase數據塊默認大小是64M(可以配置更大),那麼這些Region一般會加載在內存中,所以在B+Tree的數據機構上HBase對region的尋址非常快速。

同樣在HFile裏面定位rowkey的位置也是基於二分查找的,如圖查詢Rowkey爲fb的過程,圖中紅色箭頭部分,第一層f在a和m之間,所以查詢a的子樹,第二層f在d和h之間,所以查詢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部分,同時OpenTSDB在Rowkey設計上也下了不少功夫,首先大家看到Rowkey的開始是一個metric的uid。

如圖,OpenTSDB裏面的一個rowkey示例,包括 metric部分,時間戳部分,tagk和tagv的組成。

我們以前面我們風力數據採集的信息爲例,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的問題。

好,思考了這些現實問題,OpenTSDB對rowkey的設計就是根據用戶的metric+ts+tag信息的方式組成。那麼目前看這個rowkey的設計還是很合理的,那麼是否也在海量的時序數據場景有一些明顯弊端呢?

很明顯,這個key組成裏面根據業務的不同會變得複雜,首先就是HBase的key裏面有timestamp,是一定要有的,然後業務的rowkey裏面也是有時間戳信息的。

爲了套用現有的HBase結構,存儲中有很多無用的信息,比如comumnFamily部分,KeyType部分等。同時Rowkey裏面的metric是一個業務字符串,這些數據在實際存儲過程中很多冗餘,造成存儲成本的浪費。

還有HBase是弱類型問題,不能對Value部分根據業務的不同字段類型進行專門的壓縮。同時,Rowkey部分包含很多tag信息,沒有對Rowkey和tag進行倒排索引,同樣使得查詢受限。大量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,另一個是2020年Apache 新晉的頂級項目ApacheIoTDB。我們分別探討一下。

InfluxDB在時序數據庫排名第一,這個成績不是浪得虛名,而是實至名歸,有句話叫,天才出於勤奮,其實InfluxDB從誕生伊始,一直到現在都在不停的努力,不停的爲解決現實問題而優化改進。我們接下來都是基於InfluxDB1.8版本進行描述的。

我們瞭解一下Influxdb 引擎升級的歷程,最初爲避免B+Tree帶來的寫入問題,InfluxDB採用了LSMTree的存儲設計,底層引擎採用LevelDB,但是後面發現LevelDB存儲不能熱備份問題,於是底層引擎又採用RocksDB解決,但是RocksDB當時又存在時序數據場景冷數據的高效刪除問題,我們知道海量的數據通過api的方式逐條刪除是相當耗時的(會死人的),所以InfuxDB又進行改進引入Shard,每個shard存儲一片時間連續的數據,冷數據都是時間較老的數據,一個shard對應一個底層LevelDB數據庫,要刪除的時候只需要關庫,刪文件即可。但是這個設計又帶來了新的問題,就是系統的文件句柄問題,後面又有BoltdDB來解決,但是BoltDB裏面利用了mmap和B+Tree的數據結構,B+Tree的隨機寫問題又造成了IOPS限制問題,所謂痛苦造就成就,種種投產問題推升了目前InfulxDB全新的存儲和索引架構,TSM架構。

接下來我們看看目前InfluxDB的TSM架構細節,嘗試 儘量 知其然,知其所以然。Lets go...

首先,不可避免的我們要了解InfuxDB針對時序數據場景的核心概念的抽象,我們看InfluxDB新增了一些抽象,一個是database,一個是retentionpolicy,尤其retentionpolich是專門針對時序數據的冷數據設計的,原生的時序數據庫的優勢就是在設計之初就會考慮時序場景的種種典型問題。那麼這些概念的層次結構是怎樣的呢?

首先Database是一個最上層的抽象,或者說是管理單元,然後數據也是要按時間進行Sharding的,每個shard都歸屬於一個ShardGroup,然後Shard裏面就是具體時序數據了,當然包括時間戳,Series和Field信息,其中Series就包含了metric和tag信息。上面的RP就是retentionpolicy。

同時Point相當於一行數據,如代碼片段所示,包括被排序的key,key的組成是measurement和tags的組合,後面我們還會大量的涉及大key的介紹。好,簡單瞭解了這個層次邏輯,我們再簡單看看這些概念的代碼抽象。

首先是Store,Store是爲Database管理shards和index的抽象。

其中InfluxDB的索引機制有兩種,一是基於內存的版本,一是基於文件的版本,後面我們逐步詳細介紹。

我繼續向下進行鑽取,那就是文件存儲的分區管理Shard的代碼抽象,Shard裏面會包含時序數據和Tag到SeriesKey的倒排索引。大家會發現,有了tag的倒排索引,基於HBase的OpenTSDB裏面根據一部分tag查詢的scan操作就可以避免了。

那麼Shard裏面還有一個Engine,Engine負責進行合併多shard的查詢結果等工作。Engine代表了一個存儲引擎,這裏麪包含了TSM架構最核心的組成部分,WAL/CACHE/TSMFile/Compaction/Compression組件。我們後面一一介紹。在往下就是FileStore部分了,FileStore裏面包括很多的TSMFile。那麼一個TSMFile是怎樣的結構呢?如圖包括4個部分。

其中Blocks和index部分尤爲重要,我們先看看數據是如何寫入的...

首先,一個寫請求到來之後的寫入流程如下:

  • 第1步,進行AppendOnly的WAL寫入。
  • 第2步,然後就要更新索引,這個是加速查詢的本質,包含了tag和serieskey的倒排索引內容。
  • 第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結構。

同時說明一下,在目前InfluxDB(1.8)版本,我沒有看到做快照時候將Cache變成只讀,快照影響寫請求,然後寫完清空Cache。

  • 第6步,我想正是因爲這個Cache沒有隻讀動作,在進行Compaction時候也考慮了低層級採用低CPU消耗的算法,緩解對寫入的影響。而高層級的文件合併就要考慮壓縮成本。

在回到索引部分,我們剛纔提到InfluxDB有兩種類型的索引可以選擇,一個是基於內存的,一個是基於文件的。在索引的構建中也絞盡腦汁,利用了可用的一切優化手段,B+Tree,BloomFilter,HashIndex等等來加速查詢。

那麼,我們來哦細究一下最核心的部分,內存Cache裏面的數據結構是如何設計的,有怎樣的優勢呢?

首先設計的初衷一定是在SeriesKey有序的前提下,高性能寫入。

如圖,Cache內部提供了一個ring結構,來對數據進行分桶/分區管理理論上分區的數量最多水256個,爲啥呢?因爲InfluxDB採用 SeriesKey的前8個bits進行Hash,8個bit最多256個值。而每個Partiton的數據存儲是一個sortedmap,包括Series,Field和時間戳信息。

OK,這裏YY一下,按照代碼裏面的註釋這個區域操作是可以優化的。大家感興趣可以看看源代碼,怎樣優化最好。:) 歡迎線下討論...

我們根據代碼的的分析,可以得到Cache的數據結構是這樣的,內存數據存儲結構是partiton+map的層級管理,那麼這樣的數據結構有什麼好處呢?

  • 環結構可以Split數據結構,降低讀寫時候鎖的競爭
  • 取前8個bit進行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中記錄整體Index在TSMFile的偏移量,如果我想獲得所有Key,需要怎樣操作?我們看這個數據結構,會發現很明顯是需要根據offset定位到Index的區域,然後讀取所有的index數據。這樣其實這是一個很耗時,耗空間的操作,需要將整體index進行讀取,那麼是否可以優化一下呢?

當然,InfluxDB對索引進行了優化,爲每一個Key的索引信息都添加了offset信息,維護了一張offset表格。

在代碼的抽象上,在IndirectIndex的數據結構中有offsets的數據維護,這樣想要查詢所有Kyes的時候,我們讀取offsets信息根據偏移直接獲取對應key的信息。

那麼還有什麼更多的設計考慮嗎?

當然,除了剛纔我們提到的Level文件合併算法考慮,還有索引優化考慮,以及定期的FullCompaction。除此之外,在索引設計上面增加BloomFilter輔助判斷key在索引樹裏面的存在性,還利用HashIndex加速key的查找。總體看來InfluxDB在查詢上面花了很多的細節優化。

在繼續拷問,還有什麼其他優化嗎?

回答還是肯定的,在Measurement索引區域進行tag和SeriesKey的倒排索引結構。這在很大程度增加了查詢的靈活性和查詢性能。

同時我們發現在tagkey到SeriesKey的映射中,我們還有seriesID和SeriesKey的映射維護,這又是出於怎樣的考慮呢?對,這裏是對存儲成本也進行了考慮。

這是索引文件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樹索引,二分法找到具體的查詢值。

具體的查詢邏輯如圖標註。

對應TSI的每個部分,內部都有精細的設計和優化。這裏簡單提兩點:

  • 一個是每個部分都有一個Trailer部分保存該部分的組成的偏移量。
  • 另一個是,每個部分都利用hashindex加速對應的key查詢。

OK,總之InfluxDB利用TSM和TSI架構,完美解決了高吞吐,熱備份,冷刪除,高性能的查詢,和基於tag的倒排索引索引所支撐了靈活的業務查詢。

這裏特別提醒InfluxDB自動根據Tag進行倒排索引的建立,在實際業務中要充分利用。

當然,InfluxDB的龍頭地位除了存儲查詢設計的優勢,還有很多實用的功能,比如ContinuousQueries。這個的實現本質就是定時調度,我們可以根據語法很清楚的瞭解設計的語義。其中 EVERY是計算頻度,FOR 是計算區間,GroupBy的時間區間,就是每次計算的數據窗口。

我們簡單看一個示例,EVERY1小時,For90分鐘,groupby30分鐘。的業務含有是,每1小時調度一下計算,每次計算的數據處理區間是90分鐘,業務聚合計算的窗口數據30分鐘的數據。

我們假設8點鐘觸發了計算,那麼計算數據區間是6:30~8:00(90分鐘),計算窗口是30分鐘,也就是90分鐘的數據計算三次,有三個計算結果。

當然1小時觸發一次,到9點還會再次觸發計算邏輯。

還有豐富的生態,也是InfluxDB親民的資本。既後數據的採集又有基於訂閱的實時計算,還有方便的界面查看。OK,這裏可以爲InfluxDB點個讚了,很優秀。:)

OK,再來看看另外一個Apache開源社區優秀的時序數據庫,ApacheIoTDB。

首先是夠新,ApacheIoTDB是2020年9月從Apache 孵化器畢業,成本Apache頂級項目的。

第二是,ApacheIoTDB足夠強大,左邊的寫入和查詢的性能測試已經超越了目前的領頭羊InfluxDB。

那麼,爲什麼IoTDB會有這樣的優秀表現呢?

首先,ApacheIoTDB也是在LSMTree的基礎進行了架構優化,提出了tLSM的存儲架構,在tLSM的架構中,優化了LSM中的Mem-table部分,提出了爲亂序數據提供獨立的處理邏輯,這在寫入上有很大的優勢。同時IoTDB更加註重從SQL的角度進行查詢優化,讓用戶的查詢邏輯智能的獲取最優的查詢性能。OK,我們再看看目前IoTDB整體組件棧是一個怎樣的情況呢?一起來看一下...

圖中,棕色部分是現在發佈的版本已經具備的,橙色部分是後續發佈版本中陸續規劃的,當然還有一些有待進一步討論和社區交流的部分。那麼就IoTDB的現有功能來說,已經得到了一些工業企業的認可,我們一起看一下幾個投產的案例。

這是IoTDB的一部分工業企業用戶,包括上海的地鐵項目,這個場景1臺IoTDB實例就支持了每天4000多億的海量時序數據採集和存儲。關於IoTDB的分享還有太多的內容可以交流。。。時間原因我們將內容留在下次:)

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章