大家好,很開心能夠和大家一起交流時序資料庫的相關的内容

首先還是簡單自我介紹一下,我是 孫金城,花名 金竹。我是2011年加入阿裡,在2016年之前一直做公司内部的研發工作,包括阿裡郎,Blink等平台。
從2016年到現在我一直重心在開源建設上面,包括ApacheFlink/ApacheBeam/ApacheIoTDB,在這個過程中也得到了開源的一些肯定,目前是BeamCommitter,ApacheFlink和ApacheIoTDB的PMC,也是Apache Member,目前全球華人大概有30+的ApacheMember,當然,随着開源的越來越熱,國内每年參與開源建設的同學也在逐漸的在增加。
那麼2020之後會有怎樣的規劃呢?本着但做好事,莫問前程的心态,會多多在訂閱号中記錄我在流計算和IoT方面的認知。最終努力做到走進阿裡/踏入開源,成為最好的自己.
那麼為什麼一直做流計算會慢慢選擇了解IoT相關領域呢?因為在馬老師看來“5G時代,加速的不僅僅是通訊行業,而是更多的促進物聯網(IOT)領域的發展。IoT将是一個新的浪潮。那麼我參與IOT領域的切入點是什麼呢,就是從了解時序資料庫 進行着陸的...
其實資料存到哪裡合适,還是要看資料本身的特點,以及資料處理的需求。面對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更有優勢。
這個圖應該是我們上學課本或者資料庫理論方面書籍裡的圖檔,處理器,資料總線,主存,磁盤等等,我們再熟悉不過的名詞,當然内部也是非常複雜的,我們今天挑與我們要讨論的問題相關的内容進行分析。
那就聊聊磁盤,首先磁盤内有很多的盤片,每個盤片又由若幹扇區組成,扇區是磁盤讀寫的最小單元,每個扇區多大呢?一般是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=38deviceID是一個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越大,相同的查詢開銷越小。
- 根據根結點指針找到檔案目錄的根磁盤塊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] [...]
那麼,按照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資訊部分,這個部分核心維護了面介紹的倒排資料結構>>
- 最後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的分享還有太多的内容可以交流。。。時間原因我們将内容留在下次:)
好,天的分享就到這裡,我們下次有機會一起探讨ApacheIoTDB的技術細節:)也歡迎關注公衆号:【孫金城】 了解更多技術分享,謝謝大家!