淺談時序資料庫核心:如何用單機扛住億級資料寫入

泊浮目發表於2021-10-19
本文首發於泊浮目的簡書:https://www.jianshu.com/u/204...
版本日期備註
1.02021.10.19文章首發

0. 背景

標題來源於InfluxDB對於它們的儲存引擎誕生的背景介紹:

The workload of time series data is quite different from normal database workloads. There are a number of factors that conspire to make it very difficult to get it to scale and perform well:
- Billions of individual data points
- High write throughput
- High read throughput
- Large deletes to free up disk space
- Mostly an insert/append workload, very few updates

The first and most obvious problem is one of scale. In DevOps, for instance, you can collect hundreds of millions or billions of unique data points every day.

To prove out the numbers, let’s say we have 200 VMs or servers running, with each server collecting an average of 100 measurements every 10 seconds. Given there are 86,400 seconds in a day, a single measurement will generate 8,640 points in a day, per server. That gives us a total of 200 * 100 * 8,640 = 172,800,000 individual data points per day. We find similar or larger numbers in sensor data use cases.

最近負責了產品中一部分的監控事宜。我想到時序資料庫的對於RT和IOPS的要求應該很高,因此想看看它內部是怎麼實現的——會不會和我認識的Kafka、HBase很像。

先簡單做一下科普。時序資料庫是用於儲存跟隨時間而變化的資料,並且以時間(時間點或者時間區間)來建立索引的資料庫。 那麼它最早是應用於工業(電力行業、化工行業)應用的各型別實時監測、檢查與分析裝置所採集、產生的資料,這些工業資料的典型特點是產生頻率快(每一個監測點一秒鐘內可產生多條資料)、嚴重依賴於採集時間(每一條資料均要求對應唯一的時間)、測點多資訊量大(常規的實時監測系統均可達到成千上萬的監測點,監測點每秒鐘都在產生資料)。 其資料是歷史烙印,它具有不變性、唯一性、有序性。時序資料庫同時具有資料結構簡單、資料量大的特點。

1.問題

用過時序資料庫的同學都知道。時序資料庫的資料通常只是追加,很少刪改或者根本不允許刪改,查詢的場景一般也是有連續性的。比如:

  • 我們通常會在監控頁面上根據觀察某個時間端的資料。在需要時,會尋找其中更細的時間段來觀察。
  • 時序資料庫會將告警系統關心的指標推送過去

1.1 Prometheus踩過的坑

在這裡,我們先簡單複習一下Prometheus中的資料結構。其為典型的k-v對,k(一般叫Series)由MetricNameLablesTimeStamp組成,v則是值。

在早期的設計中,相同的Series會按照一定的規則組織起來,同時也會根據時間去組織檔案。於是就變成了一個矩陣:

優點是寫可以並行寫,讀也可以並行讀(無論是根據條件還是時間段)。但缺點也很明顯:首先是查詢會變成一個矩陣,這樣的設計容易觸發隨機讀寫,這無論在HDD(限制於轉速)還是SSD(寫放大)上都很難受。

於是Prometheus又改進了一版儲存。每一個Series一個檔案,每個Series的資料在記憶體裡存滿1KB往下刷一次。

這樣緩解了隨機讀寫的問題,但也帶來新的問題:

  1. 在資料沒達到1KB還在記憶體裡時,如果機器carsh了,那麼資料則丟失
  2. Series很容易變成特別多,這會導致記憶體佔用居高不下
  3. 繼續上面的,當這些資料一口氣被刷下去時,磁碟會變得很繁忙
  4. 繼上,很多檔案會被開啟,FD會被消耗完
  5. 當應用很久沒上傳資料時,記憶體裡的資料該刷不該刷?其實是沒法很好的判定的

1.2 InfluxDB踩過的坑

1.2.1 基於LSM Tree的LevelDB

LSM Tree的寫效能比讀效能好的多。不過InfluxDB提供了刪除的API,一旦刪除發生時,就很麻煩——它會插入一個墓碑記錄,並等待一個查詢,查詢將結果集和墓碑合併,稍後合併程式則會執行,將底層資料刪除。並且InfluxDB提供了TTL,這意味著資料刪起來是Range刪除的。

為了避免這種較慢的刪除,InfluxDB採用了分片設計。將不同的時間段切成不同的LevelDB,刪除時只需關閉資料庫並刪檔案就好了。不過當資料量很大的時候,會造成檔案控制程式碼過多的問題。

1.2.2 基於mmap B+Tree的BoltDB

BoltDB基於單個檔案作為資料儲存,基於mmap的B+Tree在執行時的效能也並不差。但當寫入資料大起來後,事情變得麻煩了起來——如何緩解一次寫入數十萬個Serires帶來的問題?

為了緩解這個問題,InfluxDB引入了WAL,這樣可以有效緩解隨機寫入帶來的問題。將多個相鄰的寫入緩衝,然後一起fresh下去,就像MySQL的BufferPool。不過這並沒有解決寫入吞吐量下降的問題,這個方法僅僅是拖延了這個問題的出現。

2. 解決方案

細細想來,時序資料庫的資料熱點只集中在近期資料。而且多寫少讀、幾乎不刪改、資料只順序追加。因此,對於時序資料庫我們則可以做出很激進的儲存、訪問和保留策略(Retention Policies)。

2.1 關鍵資料結構

  • 參考日誌結構的合併樹(Log Structured Merge Tree,LSM-Tree)的變種實現代替傳統關係型資料庫中的B+Tree作為儲存結構,LSM 適合的應用場景就是寫多讀少(將隨機寫變成順序寫),且幾乎不刪改的資料。一般實現以時間作為key。在InfluxDB中,該結構被稱為Time Structured Merge Tree。
  • 時序資料庫中甚至還有一種並不罕見卻更加極端的形式,叫做輪替型資料庫(Round Robin Database,RRD),它是以環形緩衝的思路實現,只能儲存固定數量的最新資料,超期或超過容量的資料就會被輪替覆蓋,因此它也有著固定的資料庫容量,卻能接受無限量的資料輸入。

2.2 關鍵策略

  • WAL(Write ahead log,預寫日誌):和諸多資料密集型應用一樣,WAL可以保證資料的持久化,並且緩解隨機寫的發生。在時序資料庫中,它會被當作一個查詢資料的載體——當請求發生時,儲存引擎會將來自WAL和落盤的資料做合併。另外,它還會做基於Snappy的壓縮,這是個耗時較小的壓縮演算法。
  • 設定激進的資料保留策略,比如根據過期時間(TTL),自動刪除相關資料以節省儲存空間,同時提高查詢效能。對於普通的資料庫來說,資料會儲存一段時間後被自動刪除的這個做法,可以說是不可想象的。
  • 對資料進行再取樣(Resampling)以節省空間,比如最近幾天的資料可能需要精確到秒,而查詢一個月前的冷資料只需要精確到天,查詢一年前的資料只要精確到周就夠了,這樣將資料重新取樣彙總,可以節省很多儲存空間

3.小結

總體看下來,相比Kafka、HBase來說,時序資料庫的內部結構並不簡單,非常有學習價值。

3.1 參考連結

相關文章