作者 | 劉繼聰
編輯 | 馬爾悅
小T導讀:近年來,隨著物聯網技術和市場的快速發展、企業業務的加速擴張,時序數據的處理難題也越來越受到行業和企業的重視,時序場景下通用型數據庫步履維艱,各種時序數據庫產品應運而起。但是,做一個優質的時序數據庫真的很容易嗎?本篇文章將從數據庫開發者的角度,解剖時序場景下的數據處理需求、分析時序數據庫設計思路,給到讀者一些硬核技術思考。
一、如何實現時序場景下對通用數據庫的“遠超”?
做一個 Prototype 或者 Demo 很簡單,但做出一個真正好的時序數據庫產品卻很難。
之所以說做 Prototype 簡單,是因為時序數據庫天生就不擅長處理一些數據,比如帶事務的交易數據。基于此,我們可以大刀闊斧地砍掉一些在通用型數據庫中很重要的特性,例如事務、MVCC、ACID(在 Facebook 的 Gorilla 中甚至提出不需要保證 Duration)。某些時序數據庫的存儲引擎,甚至不能處理亂序數據,在無亂序的前提下,存儲引擎幾乎可以退化為帶 Index 的 Log。所以,從這個角度來看,時序數據庫可以做得很簡單。
但是,從另一方面來說,做一個好的時序數據庫產品又很難。試想一下,在時序數據庫的設計上,我們大刀闊斧地砍掉了比如事務、ACID 等特性之后,如果依然不能使其在時序場景下的表現遠超通用型數據庫,那做一個專門的時序數據庫就毫無意義了。這樣的話,還不如不做,就直接用通用型數據庫好了。
所謂“在時序場景下的遠超”,應該是全方位的,比如寫入的延遲與吞吐量、查詢性能、處理的實時性、甚至包括集群方案的運維成本等,都應該有一個跨越式的提升。另一方面,從時序數據量大、價值偏低等特點出發,壓縮率就顯得比較重要了,而通用型數據庫卻很少強調壓縮率,由此可見,壓縮率是在時序場景下真實生長出來的需求。
高壓縮率的實現沒有什么黑科技,也不需要自己重新發明壓縮算法——無非就是列存并對各個類型使用其最好的壓縮算法;更多是工程實現的問題——好好寫代碼,認真做優化,平衡好寫入性能與壓縮比之間的關系。
此外,在時序數據場景下的“遠超”是建立在時序數據的寫入與查詢分布特點極其明顯的基礎上,當數據本身 key的特征分布十分明顯時,自然可以充分利用其特征來打造截然不同的存儲引擎與索引結構。
先說寫入。時序數據庫的吞吐量遠超一般的通用型數據庫,尤其是 IoT 設備,其設備規模可能達到千萬甚至上億,數據均為自動生成,假設 1s 采樣一次,那每秒就能產生千萬、億級別的數據寫入,這并不是普通數據庫能承受的,在這樣大的吞吐量的情況下,數據如何分區分片、如何實時地構建索引,都是具有挑戰性的問題。在寫入鏈路上,時序數據庫在時序場景下替代的是 OLTP 數據庫的位置,而后者在事務與強一致的模型下產生的讀寫延遲很難支撐時序數據庫的高吞吐量寫入。
再說查詢。在大寫入吞吐量的情況下,數據對實時性的要求也很高。例如,我們將時序數據的統計量關聯做監控、報警,能容忍的延遲可能在秒級。查詢的模式通常是聚合查詢,例如某時間段內的統計值,而不是精確的單條記錄。總的來說,時序數據庫的查詢模式通常是交互式分析,這不同于 T+1 的離線數倉,也區別于經常運行數小時的 OLAP 查詢,交互式分析查詢的響應時間通常是秒級、亞秒級。
以上,在明確了寫入與查詢需求的同時,下面我們以存儲引擎為例,來看一看一個時序數據庫的某一個部分應該如何設計。
二、存儲引擎想做到極致,還得自研
目前,數據庫的存儲引擎可以粗略分為兩大類:一類是基于 B-Tree 的,另一類是基于 LSM Tree 的。前者常見于傳統 OLTP 數據庫,比如 MySQL、PQ 這類的默認引擎,更適用于讀多寫少的場景;如 HBase、LevelDB、RocksDB 一類數據庫使用的是 LSM Tree,在寫多讀少的場景下比較適合。實際上,現代數據庫的存儲引擎,基本都會在某種程度下對這兩者融合。LSM Tree 上怎么就不可以建 B-Tree Index 了?(HBase 在 region 上也有 B-Tree Index)B-Tree 怎么就一定要直寫硬盤,不能先寫 WAL 和走內存 Cache 呢?
對于存儲引擎,時序數據庫的先行者 InfluxDB 曾經做過很多嘗試,在各個存儲引擎(LevelDB、RocksDB、BoltDB 等)之間反復橫跳,遇到過的問題也有很多,比如 BoltDB 中 mmap+BTree 模型中隨機 IO 導致的吞吐量低、RocksDB 這類純 LSM Tree 存儲引擎沒辦法很優雅快速地按時間分區刪除、多個 LevelDB + 劃分時間分區的方法又會產生大量句柄……踩了這一系列的坑后,最終 InfluxDB 換成了自研的存儲引擎 TSM。可見對時序數據庫來說,一個好的存儲引擎有多么重要,又是多么難得,要想做到極致,還得自己研發。
不同于InfluxDB,從一開始就是自研的——從 LSM Tree 中汲取了 WAL、先寫內存的 skip list 等等技術,但把 LSM Tree 的樹層級結構去掉了,而只是按時間段分區、按表分塊的 log 塊。
讀到這里,細心的讀者可能會發現,按表分塊的設計和 OpenTSDB 的行聚合有些相似。 OpenTSDB 的行聚合是把相同 tag 以一小時為時間范圍,將這些數據都放到一行中存儲,這樣大大減少了聚合查詢要掃描的數據量。不過不同的是,TDengine 是多列模型,而 OpenTSDB 是單列模型,單列模型下是多行的聚合,多列模型下聚合會自然形成數據塊。
而熟悉 LSM Tree 的 KV 分離設計的朋友應該也能夠從 TDengine 的存儲引擎設計中看到一些熟悉的影子。如果把數據塊作為存儲引擎的 value,那么 key 就應該是塊的起止時間 ,把 key 提出來自然就得到了 TDengine 的 BRIN 索引。從這種視角來看,TDengine 的 .head 文件就是 key,而 .data 和 .last 文件就是 value,而 key 自身又可以結合時序數據的特征組合成有序文件。 在時序場景下,有了 BRIN 索引,也就可以不需要 bloom filter,這樣一看,TDengine 的存儲引擎設計就很清晰了。
此外,TDengine 會將 tag 數據和時序數據分離開來,這樣就能夠大大減少 tag 數據占用的存儲空間,在數據量大的情況下尤其顯著。
TDengine 的 tag 與時序數據的劃分,和數倉的維度建模里面維度表與事實表的劃分有些類似,tag 數據類似維度表,而時序數據類似事實表。但又有所不同,因為 TDengine 中表的數目是和設備數目相同的,上億設備就是上億張表(在正在開發的 TDengine 3.0 中,我們要支持 100 億張表),這樣頻繁創建、又極其龐大的表,并不容易處理,主要的麻煩是其產生了大量的元數據,超過了單點的處理能力,這就要求 TDengine 能將這部分元數據也進行分片存儲。
當數據與元數據進行分片、多副本操作時,就自然涉及到一致性與可用性的問題。在時序數據庫中,時序數據通常是最終一致同步的,因為最終一致算法的吞吐量高延遲低、可用性也比強一致算法好,比如 InfluxDB 的集群版會用 Dynamo 這種無主風格的數據同步。但元數據(也就是我們上面提到的標簽和表數據)需要強一致,強一致通常會用 Raft、Paxos 這類算法來保證正確性。
由于元數據量的巨大需要分片,而當時序數據與元數據都做分片(甚至時序數據和其關聯的元數據應該在同一分片),但又有截然不同的一致性要求,這就導致 TDengine 的副本復制并不是簡單地使用 Raft 這類算法就能夠駕馭得了的,除非犧牲時序數據的寫入吞吐和可用性,也做強一致復制。這就是 TDengine 使用自研復制算法的根本原因。當然,這些算法在復雜的分布式環境下的一致性保證又是另外的問題了,也是我們要著重解決的挑戰。
三、TDengine 3.0 開發中,敬請期待
一個好的時序數據庫,起源于對時序數據領域的數據特征的洞察,成長于大量真實場景的考驗與用戶的反饋,又在數據庫領域的最先進技術中吸取經驗得以完善。只有這樣,最終才能做到在時序場景下“遠超”通用型數據庫,成為此場景下的優選數據庫。而要做到這一步,其實并不容易。
最后預告一下我們正在開發的 TDengine 3.0。在 3.0 版本中,我們對現在的 2.x 版本存在的一些待解問題做了重新設計與徹底重構,敬請期待。另外關于在 3.0 開發中踩過的坑,以后有機會再和大家慢慢道來。



























