天天看點

InfluxDB的存儲引擎演化過程

時序資料的來源很多,比如DevOps,目前公司的的機房裡面成百上千台的伺服器和虛機已經很常見了,這些伺服器的運作狀态、内部服務的log這些都需要監控。還有IoT,5G技術逐漸成熟,IoT将直接受益,終端裝置每時每刻對會将其探測到資料以及其自身的運作狀态資料傳回伺服器。時序資料資料以下特點:

  • 資料量非常大
  • 高并發讀/寫,取決于産生資料的終端數目、頻率、資料次元,以及監控分析系統的需求
  • 資料持續穩定生成,系統設計時就可以基本确定其IOPS
  • 與時間次元完全相關
    • 時間越久的資料其價值越來越低,這也會導緻大量過期資料的删除操作
    • 單點的資料沒有價值,價值在連續的資料分布,是以就算單點資料出錯了也沒有必要修正

目前有一些公司開始關注時序資料系統,比如Facebook的beringei資料庫,阿裡雲上實作的HiTSDB服務。InfluxDB是目前最為活躍的開源時序資料庫(TSDB: Time Series Database)。InfluxDB嘗試了不同的資料存儲引擎,最終還是設計了針對時間序列進行優化的TSM Tree(Time-Structured Merge Tree)。

資料組織形式

以下是批量寫入InfluxDB的資料

Line Protocol

。資料中包含了伺服器運作參數和空氣品質監測資料。

Labs,location=SH,host=server1 CUP=73,Mem=16.067 1574179200s
Labs,location=SH,host=server1 CUP=74,Mem=15.123 1574179210s
Labs,location=SH,host=server1 CUP=71,Mem=15.450 1574179220s
Labs,location=SH,host=server2 CUP=31,Mem=32.087 1574179200s
Labs,location=SH,host=server2 CUP=20,Mem=32.167 1574179210s
Labs,location=SH,host=server2 CUP=25,Mem=32.257 1574179220s
Labs,location=SZ,host=server1 CUP=11,Mem=8.021 1574179200s
Labs,location=SZ,host=server1 CUP=17,Mem=7.530 1574179210s
Labs,location=SZ,host=server1 CUP=37,Mem=7.214 1574179220s
Labs,location=SZ,host=server2 CUP=43,Mem=16.779 1574179200s
Labs,location=SZ,host=server2 CUP=22,Mem=16.326 1574179210s
Labs,location=SZ,host=server2 CUP=21,Mem=16.117 1574179220s
Air,city=SH,sensor=e6b58be8 PM25=62,O3=16,CO=5 1574179200s
Air,city=SH,sensor=e6b58be8 PM25=62,O3=19,CO=6 1574179210s
Air,city=SH,sensor=e6b58be8 PM25=62,O3=23,CO=5 1574179220s           

measurement

:是邏輯上的資料容器,類似于MySQL裡面的

table

,資料樣本中的Labs和Air。

tag set

:以第一行資料為例,location和host就是tag key,SH和server1就是對應的tag value。組合在一起

location=SH,host=server1

就是tag set,還有

city=SH,sensor=e6b58be8

等。

field set

:第一行資料中的

CUP=73,Mem=16.067

timestamp

1574179200s

point

:資料點,樣本資料中每一行就是一個資料點,它包括了以上列出來的幾個部分。

series

:給定一組

tag set

,所包含的所有資料點組成的相關資料序列,基于

timestamp

有序的。比如在Labs這個measurement中,

location=SH,host=server1

這個

tag set

對應的就是一個series,這個series包含了3個point。這裡

measurement + tag set

就可以唯一辨別series,稱為

SeriesKey

。這是對InfluxDB裡面最基本的時序資料組織形式。

entry

:每個

series

中包含了

field set

,每個field對應的一列資料以及相對的timestamp就組成一個

entry

,這裡

SeriesKey + FiledName

就是

EntryKey

。比如

Labs + location=SH,host=server1 + CUP

對應的

entry

[1574179200s|73, 1574179210s|74, 1574179220s|71]

。是以

entry

就是一個值序列。

時序資料中,單個point是沒有意義的,意義在

series

或者

entry

上面進行聚合計算,也就是給定tags、fields以及一個時間區間,進行查詢計算。

LSM Tree

一直到0.8版本以前,InfluxDB使用LSM Tree(Log-Structured Merge Tree)作為存儲引擎,底層直接使用LevelDB,用timestamp作為key進行排序存儲。關于LSM Tree可以參考序列文章:

使用LSM Tree遇到了一些問題:

  1. LevelDB其實是一個庫,不是服務,沒有實作熱備份。
  2. LevelDB的删除操作的實作方式是寫入一條tombstone記錄,由背景的compaction進行删除,是以删除等效于寫入。但是如上文所說,時序資料會涉及到大量的過期資料需要删除,這會嚴重影響資料寫入效率。為了解決這個問題,InfluxDB根據時間段将資料分為多個shard,每個shard起一個LevelDB存儲。這樣删除過期資料就可以直接根據shard的時間區間進行,沒必要通過寫入操作。
  3. 使用shard又造成了另一個問題,随着資料量的增大,開了越來越多的LevelDB,每個LevelDB都會産生很多SSTable檔案,機器上的檔案句柄不久就被用完了。

mmap B+Tree

0.9版本使用BoltDB來做底層存儲,其存儲引擎用的是 mmap B+Tree(memory mapped B+Tree)。這帶來了3個好處:

  1. InfluxDB和BoltDB都是以Go編寫的,BoltDB可以直接嵌入到InfluxDB中。
  2. BoltDB支援熱備份。
  3. BoltDB中每個資料庫存儲為一個檔案,節省檔案句柄。

InfluxDB還在BoltDB前面加了一層WAL。但是BoltDB由于資料寫入時需要更新索引,造成随機寫,其性能滿足不了高IOPS的場景。

TSM Tree

從0.9.5版本開始,到目前最新1.7版本,InfluxDB使用新設計實作的存儲引擎TSM Tree,這次又回到了LSM Tree,針對時序資料進行優化。對于其實作細節,已經有很多很專業的文章,但是不太易于了解。

LSM Tree對于寫入效率的優化已經達到了極緻,是以TSM的優化最主要的是對于資料壓縮、資料清除和查詢效率。以上文給出的資料為例,我們主要的需求場景有2種:

  1. location=SH,host=server1

    這一台伺服器最近24小時的運作狀态,CUP和Mem使用情況。這是給定了

    SeriesKey

    查找對應的series。這個其實是比較直接的,因為每一個entry都可以通過其EntryKey定位出來。
  2. location=SH

    這個機房所有機子最近24小時的總體運作狀态,比如最大CPU,平均Mem。這裡沒有給定完全的

    SeriesKey

    ,而是通過某些tag需要查詢出所有相關的series。這個情況稍微麻煩一下,我們放到下面說。

下面我們來看InfluxDB資料存儲的實作。

Shard

Shard是InfluxDB中真正完成資料存儲的部件。延用了前面版本基于LevelDB存儲的思想,每個shard等價于一個LevelDB執行個體,包含獨立的wal,cache(LevelDB中的MemTable),分層級的tsm檔案(LevelDB中的SSTable)以及compaction。新資料進來會根據timestamp寫入對應的shard,如果是分布式環境,還可以通過hash放到不同的機器上面。

Cache

放在記憶體中,設計相對簡單,就是一個字典,key是

EntryKey

,value是

Entry

。新資料寫入時,找到相對的

Entry

追加進去。當大小達到門檻值,再寫到TSM檔案。

TSM檔案

TSM檔案的設計類似于SSTable,cache中的一個

Entry

會持久化到tsm檔案中一個或者多個block,并為每一個block建立index,在index中記錄該block的

EntryKey

和timestamp區間,用于查詢。上面的資料寫入tsm檔案會生成以下key-value條目:

Labs,location=SH,host=server1___CUP ===> (1574179200s,1574179210s,1574179220s), (73,74,71)
Labs,location=SH,host=server1___Mem ===> (1574179200s,1574179210s,1574179220s), (16.067,15.123,15.450)
Labs,location=SH,host=server2___CUP ===> (1574179200s,1574179210s,1574179220s), (31,20,25)
Labs,location=SH,host=server2___Mem ===> (1574179200s,1574179210s,1574179220s), (32.087,32.167,32.257)
Labs,location=SZ,host=server1___CUP ===> (1574179200s,1574179210s,1574179220s), (11,17,37)
Labs,location=SZ,host=server1___Mem ===> (1574179200s,1574179210s,1574179220s), (8.021,7.530,7.214)
......           

到這裡,解決上面提出來的第一個問題就比較清楚了,因為對

EntryKey

和timestamp都已經建了索引。還有一點,tsm檔案中的每一個block隻會

Entry

的資料,這個設計其實是列式存儲,有兩個好處:

  1. 根據timestamp進行區間檢查找時,隻需要拼接相關

    Entry

    ,可以提高查詢效率
  2. 列式存儲,相同資料類型,更好的壓縮率

TSI(Time Series Index)

這裡就來看一下第二個問題。首先對資料中的series進行編号,并維護一個字典從series id映射到

SeriesKey

{
    0: "location=SH,host=server1",
    1: "location=SH,host=server2",
    2: "location=SZ,host=server1",
    3: "location=SZ,host=server2"
}           

然後對所有tag建立反向索引(就是看跟每個tag相關的所有series):

{
    "location": {
        "SH": [0, 1],
        "SZ": [2, 3]
    },
    "host": {
        "server1": [0, 2],
        "server2": [1, 3],
    }
}           

現在第二個問題的解決方法就比較清楚了,隻要在反向索引中找出所有跟給定的tag相關的series,通過series的

SeriesKey

就可以回到第一個問題,是以最終還是基于

SeriesKey

來定位到相應的資料。另外一點,InfluxDB對tag是加了索引的,給定某一個tag進行查詢的效率很高,但是field沒有,也就是說類似于

SELECT * FROM Labs WHERE CUP=73

這樣對field進行檢索是比較慢的,因為會對tsm檔案中的資料進行周遊。

這又帶來另外一個問題,如果series太多,反向索引在記憶體中放不了怎麼辦?這是在1.3版本中解決的問題,也是使用LSM Tree持久化到磁盤,同理也是每一個shard都會有一個獨立的LSM Tree,磁盤檔案名為fields.idx。

資料庫檔案結構

<root>/
├── data/
|    └── <db name>/
|        └── autogen/
|            └── 123/ # shard name
|                ├── 000000009-0000000001.tsm
|                └── fields.idx
├── meta/
|    └── meta.db
└── wal/
    └── <db name>/
        └── autogen/
            └── 123/ # shard name
                └── _00085.wal