目前,數據庫的存儲引擎可以粗略分為兩大類:一類是基于 B-Tree 的,另一類是基于 LSM Tree 的。前者常見于傳統(tǒng) OLTP 數據庫,比如 MySQL、PQ 這類的默認引擎,更適用于讀多寫少的場景;如 HBase、LevelDB、RocksDB 一類 Database 使用的是 LSM Tree,在寫多讀少的場景下比較適合。實際上,現代數據庫的存儲引擎,基本都會在某種程度下對這兩者融合。LSM Tree 上怎么就不可以建 B-Tree Index 了?(HBase 在 region 上也有 B-Tree Index)B-Tree 怎么就一定要直寫硬盤,不能先寫 WAL 和走內存 Cache 呢?
對于存儲引擎,時序數據庫 Time Series Database(TSDB) 的先行者 InfluxDB 曾經做過很多嘗試,在各個存儲引擎(LevelDB、RocksDB、BoltDB 等)之間反復橫跳,遇到過的問題也有很多,比如 BoltDB 中 mmap+BTree 模型中隨機 IO 導致的吞吐量低、RocksDB 這類純 LSM Tree 存儲引擎沒辦法很優(yōu)雅快速地按時間分區(qū)刪除、多個 LevelDB + 劃分時間分區(qū)的方法又會產生大量句柄……踩了這一系列的坑后,最終 InfluxDB 換成了自研的存儲引擎 TSM??梢妼?TSDB 來說,一個好的存儲引擎有多么重要,又是多么難得,要想做到極致,還得自己研發(fā)。
同為時序數據庫Time Series Database,不同于 InfluxDB 的是,TDengine 從一開始就是自研的——從 LSM Tree 中汲取了 WAL、先寫內存的 skip list 等技術,但把 LSM Tree 的樹層級結構去掉了,而只是按時間段分區(qū)、按表分塊的 log 塊。
讀到這里,細心的讀者可能會發(fā)現,按表分塊的設計和 OpenTSDB 的行聚合有些相似。 OpenTSDB 的行聚合是把相同 tag 以一小時為時間范圍,將這些數據都放到一行中存儲,這樣大大減少了聚合查詢要掃描的數據量。不過不同的是,TDengine 是多列模型,而 OpenTSDB 是單列模型,單列模型下是多行的聚合,多列模型下聚合會自然形成數據塊。
而熟悉 LSM Tree 的 KV 分離設計的朋友應該也能夠從 TDengine 的存儲引擎設計中看到一些熟悉的影子。如果把數據塊作為 TSDB 存儲引擎的 value,那么 key 就應該是塊的起止時間 ,把 key 提出來自然就得到了 TDengine 的 BRIN 索引。從這種視角來看,TDengine 的 .head 文件就是 key,而 .data 和 .last 文件就是 value,而 key 自身又可以結合時間序列數據的特征組合成有序文件。 在時序場景下,有了 BRIN 索引,也就可以不需要 bloom filter,這樣一看,TDengine 的存儲引擎設計就很清晰了。
此外,TDengine 會將 tag 數據和時間序列數據分離開來,這樣就能夠大大減少 tag 數據占用的存儲空間,在數據量大的情況下尤其顯著。
TDengine 的 tag 與時間序列數據的劃分,和數倉的維度建模里面維度表與事實表的劃分有些類似,tag 數據類似維度表,而時間序列數據類似事實表。但又有所不同,因為 TDengine 中表的數目是和設備數目相同的,上億設備就是上億張表,這樣頻繁創(chuàng)建、又極其龐大的表,并不容易處理,主要的麻煩是其產生了大量的元數據,超過了單點的處理能力,這就要求 TDengine 能將這部分元數據也進行分片存儲。
當數據與元數據進行分片、多副本操作時,就自然涉及到一致性與可用性的問題。在 TSDB 中,時間序列數據通常是最終一致同步的,因為最終一致算法的吞吐量高延遲低、可用性也比強一致算法好,比如InfluxDB集群版會用 Dynamo 這種無主風格的數據同步。但元數據(也就是我們上面提到的標簽和表數據)需要強一致,強一致通常會用 Raft、Paxos 這類算法來保證正確性。
由于元數據量的巨大需要分片,而當時序數據與元數據都做分片(甚至時間序列數據和其關聯(lián)的元數據應該在同一分片),但又有截然不同的一致性要求,這就導致 TDengine 的副本復制并不是簡單地使用 Raft 這類算法就能夠駕馭得了的,除非犧牲時序數據的寫入吞吐和可用性,也做強一致復制。這就是 TDengine 使用自研復制算法的根本原因。當然,這些算法在復雜的實時數據庫分布式環(huán)境下的一致性保證又是另外的問題了,也是我們要著重解決的挑戰(zhàn)。



互聯(lián)網.png)



-1.png)












伙伴.png)



