influxdb官網文件翻譯

gongpulin發表於2018-07-30

儲存引擎

儲存引擎將多個元件結合在一起,並提供用於儲存和查詢series資料的外部介面。 它由許多元件組成, 每個元件都起著特定的作用:

  • In-Memory Index —— 記憶體中的索引是分片上的共享索引,可以快速訪問measurement,tag和 series。 引擎使用該索引,但不是特指儲存引擎本身。
  • WAL —— WAL是一種寫優化的儲存格式,允許寫入持久化,但不容易查詢。 對WAL的寫入就是 append到固定大小的段中。
  • Cache —— Cache是儲存在WAL中的資料的記憶體中的表示。 它在執行時可以被查詢,並與TSM文 件中儲存的資料進行合併。
  • TSM Files —— TSM Files中儲存著柱狀格式的壓縮過的series資料。
  • FileStore —— FileStore可以訪問磁碟上的所有TSM檔案。 它可以確保在現有的TSM檔案被替換時 以及刪除不再使用的TSM檔案時,建立TSM檔案是原子性的。
  • Compactor —— Compactor負責將不夠優化的Cache和TSM資料轉換為讀取更為優化的格式。 它通 過壓縮series,去除已經刪除的資料,優化索引並將較小的檔案組合成較大的檔案來實現。
  • Compaction Planner —— Compaction Planner決定哪個TSM檔案已準備好進行壓縮,並確保多個並 發壓縮不會彼此干擾。
  • Compression —— Compression由各種編碼器和解碼器對特定資料型別作處理。一些編碼器是靜態 的,總是以相同的方式編碼相同的型別; 還有一些可以根據資料的型別切換其壓縮策略。
  • Writers/Readers —— 每個檔案型別(WAL段,TSM檔案,tombstones等)都有相應格式的Writers和 Readers。

 Write Ahead Log(WAL)

WAL被組織成一堆看起來像 _000001.wal 這樣的檔案。 檔案編號單調增,並稱為WAL段。 當分段達 到10MB的大小時,該段將被關閉並且開啟一個新的分段。每個WAL段儲存多個壓縮過的寫入和刪除 塊。 當一個新寫入的點被序列化時,使用Snappy進行壓縮,並寫入WAL檔案。 該檔案是 fsync'd ,並且 在返回成功之前將資料新增到記憶體中的索引。 這意味著批量的資料點寫入可以實現更高的效能。(在 大多數情況下,最佳批量大小似乎是每批5,000-10,000點。) 儲存引擎 58 WAL中的每個條目都遵循TLV標準,以一個單位元組表示條目型別(寫入或刪除),然後壓縮塊長度的4 位元組 uint32 ,最後是壓縮塊

Cache

快取是對儲存在WAL中的所有資料點的記憶體拷貝。這些點由這些key組成,他們是measurement,tag set 和唯一field組成。每個field都按照自己的有序時間範圍儲存。快取資料在記憶體中不被壓縮。 對儲存引擎的查詢將會把Cache中的資料與TSM檔案中的資料進行合併。在查詢執行時間內,對資料副 本的查詢都是從快取中獲取的。這樣在查詢進行時寫入的資料不會被查詢出來。 傳送到快取的刪除指令,將清除給定鍵或給定鍵的特定時間範圍的資料。 快取提供了一些控制器用於快照。兩個最重要的控制器是記憶體限制。 有一個下限, cache-snapshotmemory-size ,超出時會觸發快照到TSM檔案,並刪除相應的WAL段。 還有一個上限, cache-maxmemory-size ,當超出時會導致Cache拒絕新的寫入。 這些配置有助於防止記憶體不足的情況,並讓客 戶端寫資料比例項可承受的更快。 記憶體閾值的檢查發生在每次寫入時。 還有快照控制器是基於時間的。 cache-snapshot-write-cold-duration ,如果在指定的時間間隔內 沒有收到寫入,則強制快取到TSM檔案的快照。 通過重新讀取磁碟上的WAL檔案,可以重新建立記憶體中快取。

TSM Files

TSM files是記憶體對映的只讀檔案的集合。 這些檔案的結構看起來與LevelDB中的SSTable或其他LSM Tree變體非常相似。 一個TSMfile由四部分組成:header,blocks,index和footer:

Header是識別檔案型別和版本號的一個魔法數字:

blocks是一組CRC32校驗和資料對的序列。block資料對檔案是不透明的。 CRC32用於塊級錯誤檢測。 block的長度儲存在索引中。

blocks之後是檔案中blocks的索引。

索引由先按key順序,如果key相同則按時間順序排列的索引條目序 列組成。key包括measurement名稱,tag set和一個field。如果一個點有多個field則在TSM檔案中建立多 個索引條目。每個索引條目以金鑰長度和金鑰開始,後跟block型別(float,int,bool,string)以及該 金鑰後面的索引block條目數的計數。 然後是每個索引block條目,其由block的最小和最大時間組成, 之後是block所在的檔案的偏移量以及block的大小。 包含該key的TSM檔案中每個block都有一個索引 block條目。 索引結構可以提供對所有block的有效訪問,以及能夠確定訪問給定key相關聯資料需要多大代價。給定 一個key和時間戳,我們可以確定檔案是否包含該時間戳的block。我們還可以確定該block所在的位 置,以及取出該block必須讀取多少資料。瞭解了block的大小,我們可以有效地提供IO語句。

最後一部分是footer,它儲存了索引開頭的offset。

Compression 每個block都被壓縮,以便減少儲存空間和查詢時磁碟IO。block包含時間戳和給定seris和field的值。每 個block都有一個位元組的header,之後跟著壓縮過的時間戳,然後是壓縮後的值。

時間戳和值都會被壓縮,並使用依賴於資料型別及其形狀的編碼分開儲存。獨立儲存允許時間戳編碼 用於所有時間戳,同時允許不同欄位型別的不同編碼。例如,一些點可能能夠使用遊程長度編碼,而 其他點可能不能。 每個值型別還包含一個1byte的header,表示剩餘位元組的壓縮型別。 四個高位儲存壓縮型別,如果需 要,四個低位由編碼器使用。

Timestamps

時間戳編碼是自適應的,並且基於被編碼的時間戳的結構。它使用delta編碼,縮放和使用simple8b遊程 編碼壓縮的組合,當然如果需要,可以回退到無壓縮。 時間戳解析度是可變的,可以像納秒一樣粒度,最多需要8個位元組來儲存未壓縮的時間戳。在編碼期 間,這些值首先進行delta編碼。第一個值是起始時間戳,後續值是與先前值的差值。這通常將值轉換 成更小的整數,更容易壓縮。許多時間戳也是單調增加,並且在每10秒的時間的均勻邊界上落下。當 時間戳具有這種結構時,它們由也是10的因子的最大公約數來縮放。這具有將非常大的整數增量轉換 成更小的壓縮更好的效果。 使用這些調整值,如果所有delta都相同,則使用遊程編碼來儲存時間範圍。如果遊程長度編碼是不可 能的,並且納秒解析度的所有值都小於(1 << 60)-1(〜36.5年+ - + 1 +納秒+至+年)),則使用 simple8b編碼對時間戳進行編碼。 Simple8b編碼是一個64位字對齊的整數編碼,將多個整數打包成一個 64位字。如果任何值超過最大值,則使用每個塊的8個位元組對未壓縮的三進位制進行儲存。未來的編碼可 能使用修補方案,如“Patched Frame-Of-Reference (PFOR)”來更有效地處理異常值。

Floats

使用Facebook Gorilla paper實現對浮點數的編碼。當值靠近在一起時,編碼將連續值XORs連在一起讓 結果集變得更小。然後使用控制位儲存增量,以指示XOR值中有多少前導零和尾隨零。我們的實現會 刪除paper中描述的時間戳編碼,並且僅對浮點值進行編碼。

Integers

整數編碼使用兩種不同的策略,具體取決於未壓縮資料中的值的範圍。編碼值首先使用ZigZag編碼進 行編碼。這樣在正整數範圍內交錯正整數和負整數。 例如,[-2,-1,0,1]變成[3,1,0,2]。 有關詳細資訊,請參閱Google的Protocol Buffers文件。 如果所有ZigZag編碼值都小於(1 << 60)-1,則使用simple8b編碼進行壓縮。如果有值大於最大值,則 所有值都將在未壓縮的塊中儲存。如果所有值相同,則使用遊程長度編碼。這對於頻繁不變的值非常 有效。

Booleans

布林值使用簡單的位打包策略進行編碼,其中每個布林值使用1位。使用可變位元組編碼在塊的開始處存 儲編碼的布林值的數量。

Strings

字串使用Snappy壓縮排行編碼。每個字串連續打包,然後被壓縮為一個較大的塊。

 

Compactions

  • Compactions是將以寫優化格式儲存的資料遷移到更加讀取優化的格式的迴圈過程。在分片寫入時,會 發生Compactions的許多階段:
  • Snapshots —— Cache和WAL中的資料必須轉換為TSM檔案以釋放WAL段使用的記憶體和磁碟空間。 這些Compactions基於快取記憶體和時間閾值進行。
  • Level Compactions —— Level Compactions(分為1-4級)隨TSM檔案增長而發生。TSM檔案從 snapshot壓縮到1級檔案。多個1級檔案被壓縮以產生2級檔案。該過程繼續,直到檔案達到級別4和 TSM檔案的最大大小。除非需要執行刪除,index optimization compactions或者full compactions,否 則它們會進一步壓縮。較低階別的壓縮使用避免CPU密集型活動(如解壓縮和組合塊)的策略。 較高的水平(因此較不頻繁)的壓縮將重新組合塊來完全徹底壓縮它們並增加壓縮比。
  • Index Optimization —— 當許多4級TSM檔案累積時,內部索引變大,訪問成本更高。Index Optimization compaction通過一組新的TSM檔案分割series和index,將給定series的所有點排序到一 個TSM檔案中。 在Index Optimization之前,每個TSM檔案包含大多數或全部series的點,因此每個 TSM檔案包含相同的series索引。Index Optimization後,每個TSM檔案都包含從最小的series中得到 的點,檔案之間幾乎沒有series重疊。因此,每個TSM檔案具有較小的唯一series索引,而不是完整 series列表的副本。此外,特定series的所有點在TSM檔案中是連續的,而不是分佈在多個TSM檔案 中。
  • Full Compaction —— 當分片資料已經寫入很長時間(也就是冷資料),或者在分片上發生刪除時, Full Compaction就會執行。Full Compaction產生最佳的TSM檔案集,幷包括來自Level和Index Optimization的所有優化。一旦一個shard上執行了Full Compaction,除非儲存有新的寫入或刪除, 否則不會在其上執行其他壓縮。

 

Writes(寫)

寫入是同時寫到WAL的segment和Cache中。每個WAL segment都有最大尺寸。一旦當前檔案填滿,將寫 入一個新檔案。Cache也有大小限制,當Cache滿了之後,會產生snapshot並且啟動WAL的compaction。 如果在一定時間內,寫入速率超過了WAL的compaction速率,則Cache可能變得過滿,在這種情況下, 新的寫入將失敗直到snapshot程式追趕上。 當WAL segment填滿並關閉時,Compactor會將Cache並將資料寫入新的TSM檔案。當TSM檔案成功寫入 和 fsync 'd時,它將會被FileStore載入和引用。

Updates(更新)

正常寫入會發生更新(為已存在的點寫入較新的值),由於快取值覆蓋現有值,因此較新的寫入優 先。如果寫入將覆蓋先前TSM檔案中的一個點,則這些點在查詢執行時會合並,較新的寫入優先。

Deletes(刪除)

通過向measurement或series的WAL寫入刪除條目然後更新Cache和FileStore來進行刪除。這時Cache會去 掉所有相關條目,FileStore為包含相關資料的每個TSM檔案寫入一個tombstone檔案。這些tombstone文 件被用於在啟動時忽略相應的block,以及在compaction期間移除已刪除的條目。 在compaction完全從TSM檔案中刪除資料之前,部分刪除的series在查詢時處理。

 Queries(查詢)

當儲存引擎執行查詢時,它本質上是尋找給定時間相關的特定series key和field。首先,我們對資料文 件進行搜尋,以查詢包含與查詢匹配的時間範圍以及包含匹配series的檔案。 一旦我們選擇了資料檔案,我們接下來需要找到series key索引條目的檔案中的位置。我們針對每個 TSM索引執行二進位制搜尋,以查詢其索引塊的位置。 在通常的情況下,這些塊不會跨多個TSM檔案重疊,我們可以線性搜尋索引條目以找到要讀取的起始 塊。如果存在重疊的時間塊,則索引條目將被排序,以確保較新的寫入將優先,並且可以在查詢執行 期間按順序處理該塊。 當迭代索引條目時,塊將從其位置順序地被讀取。該塊被解壓縮,以便我們尋求具體的資料點。

 

新的InfluxDB儲存引擎:從LSM樹到B+樹,然後重新建立TSM

寫一個新的儲存引擎應該是最後的手段。那麼InfluxData最終如何寫我們自己的引擎的呢?InfluxData已 經嘗試了許多儲存格式,發現每個在某一方面都有一些缺點。InfluxDB的效能要求非常高,當然最終 壓倒了其他儲存系統。InfluxDB的0.8版本允許多個儲存引擎,包括LevelDB,RocksDB, HyperLevelDB和LMDB。 InfluxDB的0.9版本使用BoltDB作為底層儲存引擎。下面要介紹的TSM,它在 0.9.5中釋出,是InfluxDB 0.11+中唯一支援的儲存引擎,包括整個1.x系列。 時間序列資料用例的特性使許多現有儲存引擎很有挑戰性。在InfluxDB開發過程中,我們嘗試了一些 更受歡迎的選項。我們從LevelDB開始,這是一種基於LSM樹的引擎,針對寫入吞吐量進行了優化。之 後,我們嘗試了BoltDB,這是一個基於記憶體對映B+ Tree的引擎,它是針對讀取進行了優化的。最後, 我們最終建立了我們自己的儲存引擎,它在許多方面與LSM樹類似。 藉助我們的新儲存引擎,我們可以達到比B+ Tree實現高達45倍的磁碟空間使用量的減少,甚至比使用 LevelDB及其變體有更高的寫入吞吐量和壓縮率。這篇文章將介紹整個演變的細節,並深入瞭解我們的 新儲存引擎及其內部工作。

 

時序資料的特性

時間序列資料與正常的資料庫工作負載有很大的不同。有許多因素使得它難以提高和保持效能:

  • 數十億個資料點
  • 高吞吐量的寫入
  • 高吞吐量的讀取 大量刪除(資料到期)
  • 大部分資料是插入/追加,很少更新

第一個也是最明顯的問題是規模。在DevOps,IoT或APM中,每天很容易收集數億或數十億的資料 點。 例如,假設我們有200個VM或伺服器執行,每個伺服器平均有100個measurement每10秒收集一次。鑑 於一天中有86,400秒,單個measurement每個伺服器將在一天內產生8,640點。這樣我們每天總共200 100 8,640 = 172,800,000個資料點。我們在感測器資料用例中還可以找到類似或更大的數字。 資料量大意味著寫入吞吐量可能非常高。一些較大的公司一般需要每秒處理數百萬次寫入的系統。 儲存引擎 63 同時,時間序列資料也可能是需要高吞吐量讀取的。的確,如果您正在跟蹤70萬個metric或時間序列, 那麼你肯定不希望將其全部視覺化。這導致許多人認為您實際上並沒有讀取大量資料的需求。然而, 除了人們在螢幕上的儀表板之外,還有自動化系統用於監視或組合大量時間序列資料與其他型別的數 據。 在InfluxDB內部,實時計算的聚合函式可將數萬個不同時間series組合成單個檢視。這些查詢中的每一 個都必須讀取每個聚合資料點,因此對於InfluxDB,讀取吞吐量通常比寫入吞吐量高許多倍。 鑑於時間序列大多是順序插入,你可能會認為可以在B+樹上獲得出色的效能,因為順序插入是高效 的,您可以達到每秒100,000以上。但是,我們有這些資料的寫入發生在不同的時間序列。因此,插入 最終看起來更像是隨機插入,而不僅僅是順序插入。 使用時間序列資料發現的最大問題之一是,在超過一定時間後需要刪除所有資料。這裡的常見模式是 使用者擁有高精度的資料,儲存在短時間內,如幾天或幾個月。然後使用者將資料取樣並將其彙總到儲存 較長時間的較低精度資料。 最容易的實現將是簡單地刪除每個記錄一旦超過其過期時間。然而,這意味著一旦寫入的第一個點到 達其到期日期,系統正在處理與寫入一樣多的刪除,大多數儲存引擎都不會這樣去設計的。 我們來看看我們嘗試過的兩種儲存引擎的細節,以及這些特性對我們效能的重大的影響。

LevelDB和LSM樹

當InfluxDB專案開始時,我們選擇了LevelDB作為儲存引擎,因為我們將其用於作為InfluxDB前身的產 品中的時間序列資料儲存。我們知道它具有很好的寫入吞吐量,但一切似乎都“just work”。 LevelDB是在Google構建為開源專案的Log Structured Merge Tree(或LSM樹)的實現。 它暴露了鍵/值 儲存的API,其中key space是經過排序的。這最後一部分對於時間序列資料很重要,只要把時間戳放在 key中,就允許我們快速掃描時間範圍。 LSM樹基於採用寫入和兩個稱為Mem Tables和SSTables的結構的日誌。 這些tables代表了排序的 keyspace。SSTables是隻讀檔案,只能被其插入和更新的其他SSTables所替換。 LevelDB為我們帶來的兩大優勢是寫入吞吐量高,內建壓縮。 然而,當我們從時間序列資料中瞭解到 人們需要什麼時,我們遇到了一些不可逾越的挑戰。 我們遇到的第一個問題是LevelDB不支援熱備份。如果要對資料庫進行安全備份,則必須將其關閉,然 後將其複製。 LevelDB變體RocksDB和HyperLevelDB解決了這個問題,但還有另一個更緊迫的問題, 我們認為他們解決不了。 我們的使用者需要一種自動管理資料保留的方法。這意味著我們需要大量的刪除。在LSM樹中,刪除與 寫入一樣甚至更加昂貴。刪除需要寫入一個稱為tombstone的新紀錄。之後,查詢會將結果集與任何 tombstone合併,以從查詢返回中清除已刪除的資料。之後,將執行一個compaction操作,刪除SSTable 檔案中的tombstone和底層刪除的記錄。 為了避免刪除操作,我們將資料分割成我們稱之為shard的資料,這些資料是連續的時間塊。shard通常 會持有一天或七天的資料。每個shard對映到底層的LevelDB。這意味著我們可以通過關閉資料庫並刪 除底層檔案來刪除一整天的資料。 儲存引擎 64 RocksDB的使用者現在可以提出一個名為ColumnFamilies的功能。當將時間序列資料放入Rocks時,通常 將時間塊分成列族,然後在時間到達時刪除它們。這是一個一般的想法:建立一個單獨的區域,您可 以在刪除大量資料時只刪除檔案而不是更新索引。 刪除列族是一個非常有效的操作。然而,列族是一 個相當新的功能,我們還有另一個shard的用例。 將資料組織成shard意味著它可以在叢集內移動,而不必檢查數十億個key。在撰寫本文時,不可能將 RocksDB中的列族移動到另一個。舊的碎片通常是冷寫的,所以移動它們將會很便宜而且代價很小。 我們將獲得額外的好處是在keyspace中存在一個寫冷的地方,所以之後進行的一致性檢查會更容易。 將資料組織到shard中執行了一段時間,直到大量的資料進入InfluxDB。 LevelDB將資料分解成許多小 檔案。在單個程式中開啟數十個數百個這些資料庫,最終造成了一個大問題。有六個月或一年資料的 使用者將用盡檔案控制程式碼。這不是我們與大多數使用者發現的,任何將資料庫推到極限的人都會遇到這個問 題,我們沒有解決。開啟的檔案柄實在太多了。

 

BoltDB和mmap B+樹

在與LevelDB及其變體一起掙扎了一年之後,我們決定轉移到BoltDB,BoltDB是一個純粹的Golang數 據庫,它受到LMDB的高度啟發,這是一個用C編寫的mmap B+ Tree資料庫。它具有與LevelDB相同的 API語義:keyspace有序地儲存。我們的許多使用者感到驚訝,我們自己釋出的LevelDB變體與 LMDB(mmap B+ Tree)的測試顯示,RocksDB是表現最好的。 然而,在純粹的寫的表現之外,還有其他因素需要考慮進來。 在這一點上,我們最重要的目標是獲得 可以在生產和備份中執行的穩定的東西。BoltDB還具有以純Go編寫的優勢,它極大地簡化了我們的構 建鏈,並使其易於構建在其他作業系統和平臺上。 對我們來說,最大的好處是BoltDB使用單個檔案作為資料庫。在這一點上,我們之前最常見的bug報告 來源是使用者用盡了檔案控制程式碼。Bolt還同時解決了熱備份問題和檔案限制問題。 如果這意味著我們可以建立一個更可靠和穩定的系統,我們願意對寫入吞吐量上作出妥協。 我們的理 由是,對於任何人想要真正大的寫入負載,他們將會執行一個叢集。我們根據BoltDB釋出了0.9.0到 0.9.2版本。從發展的角度來看,這是令人愉快的。 簡潔的API,快速輕鬆地構建在我們的Go專案中, 並且可靠。 然而,執行一段時間後,我們發現了寫入吞吐量的一大問題。在資料庫超過幾GB之後, IOPS開始成為瓶頸。 有些使用者可以通過將InfluxDB放在具有接近無限制IOPS的大硬體上,從而達到這個目標。但是,大多 數使用者都是雲端資源有限的虛擬機器。 我們必須找出一種方法來減少同時將一堆資料寫入到成百上千個 series的影響。 隨著0.9.3和0.9.4版本的釋出,我們的計劃是在Bolt面前寫一個WAL,這樣我們可以減少隨機插入到 keyspace的數量。相反,我們會緩衝彼此相鄰的多個寫入,然後一次flush它們 但是,這僅僅是為了延 緩了這個問題。高IOPS仍然成為一個問題,對於任何在適度工作負荷的場景下,它都會很快出現。 然而,我們在Bolt面前建立一個WAL實施的經驗使我們有信心可以解決寫入問題。WAL本身的表現太 棒了,索引根本無法跟上。在這一點上,我們再次開始思考如何建立類似於LSM Tree的東西,使之可 以跟上我們的寫入負載。 這就是TSM Tree的誕生過程。

相關文章