- InfluxDB學習之InfluxDB的安裝和簡介
- InfluxDB學習之InfluxDB的基本概念
- InfluxDB學習之InfluxDB的基本操作
- InfluxDB學習之InfluxDB的HTTP API寫入操作
- InfluxDB學習之InfluxDB數據保留策略(Retention Policies)
- InfluxDB學習之InfluxDB連續查詢(Continuous Queries)
- InfluxDB學習之InfluxDB的HTTP API查詢操作
- InfluxDB學習之InfluxDB的關鍵概念
- InfluxDB學習之InfluxDB常用函數(一)聚合類函數
- InfluxDB學習之InfluxDB常用函數(二)選擇類函數
- InfluxDB學習之InfluxDB常用函數(三)變換類函數
- InfluxDB學習之再說連續查詢
- Influxdb原理詳解
- InfluxDB安裝后web頁面無法訪問的解決方案
- InfluxDB數據備份和恢復方法,支持本地和遠程備份
系列詳情請看:《InfluxDB系列教程》
InfluxDB是一款Go語言寫的時序數據庫,本文主要介紹下Influxdb的架構和基本原理。
更多InfluxDB詳細教程請看:InfluxDB系列學習教程目錄
InfluxDB技術交流群:580487672(點擊加入)
一、InfluxDB特點
- 可以設置metric的保存時間。
- 支持通過條件過濾以及正則表達式刪除數據。
- 支持類似 sql 的語法。
- 可以設置數據在集群中的副本數。
- 支持定期采樣數據,寫入另外的measurement,方便分粒度存儲數據。
二、InfluxDB概念
1)數據格式 Line Protocol
在 InfluxDB 中,我們可以粗略的將要存入的一條數據看作一個虛擬的 key 和其對應的 value(field value),格式如下:
cpu_usage,host=server01,region=us-west value=0.64 1434055562000000000
虛擬的 key 包括以下幾個部分: database, retention policy, measurement, tag sets, field name, timestamp。 database 和 retention policy 在上面的數據中並沒有體現,通常在插入數據時在 http 請求的相應字段中指定。
- database: 數據庫名,在 InfluxDB 中可以創建多個數據庫,不同數據庫中的數據文件是隔離存放的,存放在磁盤上的不同目錄。
- retention policy: 存儲策略,用於設置數據保留的時間,每個數據庫剛開始會自動創建一個默認的存儲策略 autogen,數據保留時間為永久,之后用戶可以自己設置,例如保留最近2小時的數據。插入和查詢數據時如果不指定存儲策略,則使用默認存儲策略,且默認存儲策略可以修改。InfluxDB 會定期清除過期的數據。
- measurement: 測量指標名,例如 cpu_usage 表示 cpu 的使用率。
- tag sets: tags 在 InfluxDB 中會按照字典序排序,不管是 tagk 還是 tagv,只要不一致就分別屬於兩個 key,例如
host=server01,region=us-west
和host=server02,region=us-west
就是兩個不同的 tag set。 - field name: 例如上面數據中的
value
就是 fieldName,InfluxDB 中支持一條數據中插入多個 fieldName,這其實是一個語法上的優化,在實際的底層存儲中,是當作多條數據來存儲。 - timestamp: 每一條數據都需要指定一個時間戳,在 TSM 存儲引擎中會特殊對待,以為了優化后續的查詢操作。
2)Point
InfluxDB 中單條插入語句的數據結構,series + timestamp 可以用於區別一個 point,也就是說一個 point 可以有多個 field name 和 field value。
3)Series
series 相當於是 InfluxDB 中一些數據的集合,在同一個 database 中,retention policy、measurement、tag sets 完全相同的數據同屬於一個 series,同一個 series 的數據在物理上會按照時間順序排列存儲在一起。
series 的 key 為 measurement + 所有 tags 的序列化字符串,這個 key 在之后會經常用到。
代碼中的結構如下:
type Series struct { mu sync.RWMutex Key string // series key Tags map[string]string // tags id uint64 // id measurement *Measurement // measurement }
4)Shard
shard 在 InfluxDB 中是一個比較重要的概念,它和 retention policy 相關聯。每一個存儲策略下會存在許多 shard,每一個 shard 存儲一個指定時間段內的數據,並且不重復,例如 7點-8點 的數據落入 shard0 中,8點-9點的數據則落入 shard1 中。每一個 shard 都對應一個底層的 tsm 存儲引擎,有獨立的 cache、wal、tsm file。
創建數據庫時會自動創建一個默認存儲策略,永久保存數據,對應的在此存儲策略下的 shard 所保存的數據的時間段為 7 天,計算的函數如下:
func shardGroupDuration(d time.Duration) time.Duration { if d >= 180*24*time.Hour || d == 0 { // 6 months or 0 return 7 * 24 * time.Hour } else if d >= 2*24*time.Hour { // 2 days return 1 * 24 * time.Hour } return 1 * time.Hour }
如果創建一個新的 retention policy 設置數據的保留時間為 1 天,則單個 shard 所存儲數據的時間間隔為 1 小時,超過1個小時的數據會被存放到下一個 shard 中。
三、存儲引擎 - TSM Tree
從 LevelDB(LSM Tree),到 BoltDB(mmap B+樹),現在InfluxDB使用的是自己實現的 TSM Tree 的算法,類似 LSM Tree,針對 InfluxDB 的使用做了特殊優化。
TSM Tree 是 InfluxDB 根據實際需求在 LSM Tree 的基礎上稍作修改優化而來。
TSM 存儲引擎主要由幾個部分組成: cache、wal、tsm file、compactor。
1)Shard
shard 並不能算是其中的一個組件,因為這是在 tsm 存儲引擎之上的一個概念。在 InfluxDB 中按照數據的時間戳所在的范圍,會去創建不同的 shard,每一個 shard 都有自己的 cache、wal、tsm file 以及 compactor,這樣做的目的就是為了可以通過時間來快速定位到要查詢數據的相關資源,加速查詢的過程,並且也讓之后的批量刪除數據的操作變得非常簡單且高效。
在 LSM Tree 中刪除數據是通過給指定 key 插入一個刪除標記的方式,數據並不立即刪除,需要等之后對文件進行壓縮合並時才會真正地將數據刪除,所以刪除大量數據在 LSM Tree 中是一個非常低效的操作。
而在 InfluxDB 中,通過 retention policy 設置數據的保留時間,當檢測到一個 shard 中的數據過期后,只需要將這個 shard 的資源釋放,相關文件刪除即可,這樣的做法使得刪除過期數據變得非常高效。
2)Cache
cache 相當於是 LSM Tree 中的 memtable,在內存中是一個簡單的 map 結構,這里的 key 為 seriesKey + 分隔符 + filedName,目前代碼中的分隔符為 #!~#,entry 相當於是一個按照時間排序的存放實際值的數組,具體結構如下:
type Cache struct { commit sync.Mutex mu sync.RWMutex store map[string]*entry size uint64 // 當前使用內存的大小 maxSize uint64 // 緩存最大值 // snapshots are the cache objects that are currently being written to tsm files // they're kept in memory while flushing so they can be queried along with the cache. // they are read only and should never be modified // memtable 快照,用於寫入 tsm 文件,只讀 snapshot *Cache snapshotSize uint64 snapshotting bool // This number is the number of pending or failed WriteSnaphot attempts since the last successful one. snapshotAttempts int stats *CacheStatistics lastSnapshot time.Time }
插入數據時,實際上是同時往 cache 與 wal 中寫入數據,可以認為 cache 是 wal 文件中的數據在內存中的緩存。當 InfluxDB 啟動時,會遍歷所有的 wal 文件,重新構造 cache,這樣即使系統出現故障,也不會導致數據的丟失。
cache 中的數據並不是無限增長的,有一個 maxSize 參數用於控制當 cache 中的數據占用多少內存后就會將數據寫入 tsm 文件。如果不配置的話,默認上限為 25MB,每當 cache 中的數據達到閥值后,會將當前的 cache 進行一次快照,之后清空當前 cache 中的內容,再創建一個新的 wal 文件用於寫入,剩下的 wal 文件最后會被刪除,快照中的數據會經過排序寫入一個新的 tsm 文件中。
目前的 cache 的設計有一個問題,當一個快照正在被寫入一個新的 tsm 文件時,當前的 cache 由於大量數據寫入,又達到了閥值,此時前一次快照還沒有完全寫入磁盤,InfluxDB 的做法是讓后續的寫入操作失敗,用戶需要自己處理,等待恢復后繼續寫入數據。
3)WAL
wal 文件的內容與內存中的 cache 相同,其作用就是為了持久化數據,當系統崩潰后可以通過 wal 文件恢復還沒有寫入到 tsm 文件中的數據。
由於數據是被順序插入到 wal 文件中,所以寫入效率非常高。但是如果寫入的數據沒有按照時間順序排列,而是以雜亂無章的方式寫入,數據將會根據時間路由到不同的 shard 中,每一個 shard 都有自己的 wal 文件,這樣就不再是完全的順序寫入,對性能會有一定影響。看到官方社區有說后續會進行優化,只使用一個 wal 文件,而不是為每一個 shard 創建 wal 文件。
wal 單個文件達到一定大小后會進行分片,創建一個新的 wal 分片文件用於寫入數據。
4)TSM file
單個 tsm file 大小最大為 2GB,用於存放數據。
TSM file 使用了自己設計的格式,對查詢性能以及壓縮方面進行了很多優化,在后面的章節會具體說明其文件結構。
5)Compactor
compactor 組件在后台持續運行,每隔 1 秒會檢查一次是否有需要壓縮合並的數據。
主要進行兩種操作,一種是 cache 中的數據大小達到閥值后,進行快照,之后轉存到一個新的 tsm 文件中。
另外一種就是合並當前的 tsm 文件,將多個小的 tsm 文件合並成一個,使每一個文件盡量達到單個文件的最大大小,減少文件的數量,並且一些數據的刪除操作也是在這個時候完成。
四、目錄與文件結構
InfluxDB 的數據存儲主要有三個目錄。
默認情況下是 meta, wal 以及 data 三個目錄。
meta 用於存儲數據庫的一些元數據,meta 目錄下有一個 meta.db 文件。
wal 目錄存放預寫日志文件,以 .wal 結尾。data 目錄存放實際存儲的數據文件,以 .tsm 結尾。這兩個目錄下的結構是相似的,其基本結構如下:
# wal 目錄結構 -- wal -- mydb -- autogen -- 1 -- _00001.wal -- 2 -- _00035.wal -- 2hours -- 1 -- _00001.wal # data 目錄結構 -- data -- mydb -- autogen -- 1 -- 000000001-000000003.tsm -- 2 -- 000000001-000000001.tsm -- 2hours -- 1 -- 000000002-000000002.tsm
其中 mydb 是數據庫名稱,autogen 和 2hours 是存儲策略名稱,再下一層目錄中的以數字命名的目錄是 shard 的 ID 值,比如 autogen 存儲策略下有兩個 shard,ID 分別為 1 和 2,shard 存儲了某一個時間段范圍內的數據。再下一級的目錄則為具體的文件,分別是 .wal 和 .tsm 結尾的文件。
1)WAL 文件
wal 文件中的一條數據,對應的是一個 key(measument + tags + fieldName) 下的所有 value 數據,按照時間排序。
- Type (1 byte): 表示這個條目中 value 的類型。
- Key Len (2 bytes): 指定下面一個字段 key 的長度。
- Key (N bytes): 這里的 key 為 measument + tags + fieldName。
- Count (4 bytes): 后面緊跟着的是同一個 key 下數據的個數。
- Time (8 bytes): 單個 value 的時間戳。
- Value (N bytes): value 的具體內容,其中 float64, int64, boolean 都是固定的字節數存儲比較簡單,通過 Type 字段知道這里 value 的字節數。string 類型比較特殊,對於 string 來說,N bytes 的 Value 部分,前面 4 字節用於存儲 string 的長度,剩下的部分才是 string 的實際內容。
2)TSM 文件
單個 tsm 文件的主要格式如下:
主要分為四個部分: Header, Blocks, Index, Footer。
其中 Index 部分的內容會被緩存在內存中,下面詳細說明一下每一個部分的數據結構。
Header
- MagicNumber (4 bytes): 用於區分是哪一個存儲引擎,目前使用的 tsm1 引擎,MagicNumber 為
0x16D116D1
。 - Version (1 byte): 目前是 tsm1 引擎,此值固定為
1
。
Blocks
Blocks 內部是一些連續的 Block,block 是 InfluxDB 中的最小讀取對象,每次讀取操作都會讀取一個 block。每一個 Block 分為 CRC32 值和 Data 兩部分,CRC32 值用於校驗 Data 的內容是否有問題。Data 的長度記錄在之后的 Index 部分中。
Data 中的內容根據數據類型的不同,在 InfluxDB 中會采用不同的壓縮方式,float 值采用了 Gorilla float compression,而 timestamp 因為是一個遞增的序列,所以實際上壓縮時只需要記錄時間的偏移量信息。string 類型的 value 采用了 snappy 算法進行壓縮。
Data 的數據解壓后的格式為 8 字節的時間戳以及緊跟着的 value,value 根據類型的不同,會占用不同大小的空間,其中 string 為不定長,會在數據開始處存放長度,這一點和 WAL 文件中的格式相同。
Index
Index 存放的是前面 Blocks 里內容的索引。索引條目的順序是先按照 key 的字典序排序,再按照 time 排序。InfluxDB 在做查詢操作時,可以根據 Index 的信息快速定位到 tsm file 中要查詢的 block 的位置。
這張圖只展示了其中一部分,用結構體來表示的話類似下面的代碼:
type BlockIndex struct { MinTime int64 MaxTime int64 Offset int64 Size uint32 } type KeyIndex struct { KeyLen uint16 Key string Type byte Count uint32 Blocks []*BlockIndex } type Index []*KeyIndex
Key Len (2 bytes): 下面一個字段 key 的長度。
Key (N bytes): 這里的 key 指的是 seriesKey + 分隔符 + fieldName。
Type (1 bytes): fieldName 所對應的 fieldValue 的類型,也就是 Block 中 Data 內的數據的類型。
Count (2 bytes): 后面緊跟着的 Blocks 索引的個數。
后面四個部分是 block 的索引信息,根據 Count 中的個數會重復出現,每個 block 索引固定為 28 字節,按照時間排序。
Min Time (8 bytes): block 中 value 的最小時間戳。
Max Time (8 bytes): block 中 value 的最大時間戳。
Offset (8 bytes): block 在整個 tsm file 中的偏移量。
Size (4 bytes): block 的大小。根據 Offset + Size 字段就可以快速讀取出一個 block 中的內容。
間接索引
間接索引只存在於內存中,是為了可以快速定位到一個 key 在詳細索引信息中的位置而創建的,可以被用於二分查找來實現快速檢索。
offsets 是一個數組,其中存儲的值為每一個 key 在 Index 表中的位置,由於 key 的長度固定為 2字節,所以根據這個位置就可以找到該位置上對應的 key 的內容。
當指定一個要查詢的 key 時,就可以通過二分查找,定位到其在 Index 表中的位置,再根據要查詢的數據的時間進行定位,由於 KeyIndex 中的 BlockIndex 結構是定長的,所以也可以進行一次二分查找,找到要查詢的數據所在的 BlockIndex 的內容,之后根據偏移量以及 block 長度就可以從 tsm 文件中快速讀取出一個 block 的內容。
Footer
tsm file 的最后8字節的內容存放了 Index 部分的起始位置在 tsm file 中的偏移量,方便將索引信息加載到內存中。
五、數據查詢與索引結構
由於 LSM Tree 的原理就是通過將大量的隨機寫轉換為順序寫,從而極大地提升了數據寫入的性能,與此同時犧牲了部分讀的性能。TSM 存儲引擎是基於 LSM Tree 開發的,所以情況類似。通常設計數據庫時會采用索引文件的方式(例如 LevelDB 中的 Mainfest 文件) 或者 Bloom filter 來對 LSM Tree 這樣的數據結構的讀取操作進行優化。
InfluxDB 中采用索引的方式進行優化,主要存在兩種類型的索引。
1)元數據索引
一個數據庫的元數據索引通過 DatabaseIndex 這個結構體來存儲,在數據庫啟動時,會進行初始化,從所有 shard 下的 tsm file 中加載 index 數據,獲取其中所有 Measurement 以及 Series 的信息並緩存到內存中。
type DatabaseIndex struct { measurements map[string]*Measurement // 該數據庫下所有 Measurement 對象 series map[string]*Series // 所有 Series 對象,SeriesKey = measurement + tags name string // 數據庫名 }
這個結構體中最主要存放的就是該數據下所有 Measurement 和 Series 的內容,其數據結構如下:
type Measurement struct { Name string `json:"name,omitempty"` fieldNames map[string]struct{} // 此 measurement 中的所有 filedNames // 內存中的索引信息 // id 以及其對應的 series 信息,主要是為了在 seriesByTagKeyValue 中存儲Id節約內存 seriesByID map[uint64]*Series // lookup table for series by their id // 根據 tagk 和 tagv 的雙重索引,保存排好序的 SeriesID 數組 // 這個 map 用於在查詢操作時,可以根據 tags 來快速過濾出要查詢的所有 SeriesID,之后根據 SeriesKey 以及時間范圍從文件中讀取相應內容 seriesByTagKeyValue map[string]map[string]SeriesIDs // map from tag key to value to sorted set of series ids // 此 measurement 中所有 series 的 id,按照 id 排序 seriesIDs SeriesIDs // sorted list of series IDs in this measurement } type Series struct { Key string // series key Tags map[string]string // tags id uint64 // id measurement *Measurement // 所屬 measurement // 在哪些 shard 中存在 shardIDs map[uint64]bool // shards that have this series defined }
元數據查詢
InfluxDB 支持一些特殊的查詢語句(支持正則表達式匹配),可以查詢一些 measurement 以及 tags 相關的數據,例如
SHOW MEASUREMENTS SHOW TAG KEYS FROM "measurement_name" SHOW TAG VALUES FROM "measurement_name" WITH KEY = "tag_key"
例如我們需要查詢 cpu_usage 這個 measurement 上傳數據的機器有哪些,一個可能的查詢語句為:
SHOW TAG VALUES FROM "cpu_usage" WITH KEY = "host"
首先根據 measurement 可以在 DatabaseIndex.measurements 中拿到 cpu_usage 所對應的 Measurement 對象。
通過 Measurement.seriesByTagKeyValue 獲取 tagk=host 所對應的以 tagv 為鍵的 map 對象。
遍歷這個 map 對象,所有的 key 則為我們需要獲取的數據。
普通數據查詢的定位
對於普通的數據查詢語句,則可以通過上述的元數據索引快速定位到要查詢的數據所包含的所有 seriesKey,fieldName 和時間范圍。
舉個例子,假設查詢語句為獲取 server01 這台機器上 cpu_usage 指標最近一小時的數據:
`SELECT value FROM "cpu_usage" WHERE host='server01' AND time > now() - 1h`
先根據 measurement=cpu_usage 從 DatabaseIndex.measurements 中獲取到 cpu_usage 對應的 Measurement 對象。
之后通過 DatabaseIndex.measurements["cpu_usage"].seriesByTagKeyValue["host"]["server01"] 獲取到所有匹配的 series 的 ID值,再通過 Measurement.seriesByID 這個 map 對象根據 series ID 獲取它們的實際對象。
注意這里雖然只指定了 host=server01,但不代表 cpu_usage 下只有這一個 series,可能還有其他的 tags 例如 user=1 以及 user=2,這樣獲取到的 series ID 實際上有兩個,獲取數據時需要獲取所有 series 下的數據。
在 Series 結構體中的 shardIDs 這個 map 變量存放了哪些 shard 中存在這個 series 的數據。而 Measurement.fieldNames 這個 map 可以幫助過濾掉 fieldName 不存在的情況。
至此,我們在 o(1) 的時間復雜度內,獲取到了所有符合要求的 series key、這些 series key 所存在的 shardID,要查詢數據的時間范圍,之后我們就可以創建數據迭代器從不同的 shard 中獲取每一個 series key 在指定時間范圍內的數據。后續的查詢則和 tsm file 中的 Index 的在內存中的緩存相關。
2)TSM File 索引
上文中對於 tsm file 中的 Index 部分會在內存中做間接索引,從而可以實現快速檢索的目的。這里看一下具體的數據結構:
type indirectIndex struct { b []byte // 下層詳細索引的字節流 offsets []int32 // 偏移量數組,記錄了一個 key 在 b 中的偏移量 minKey, maxKey string minTime, maxTime int64 // 此文件中的最小時間和最大時間,根據這個可以快速判斷要查詢的數據在此文件中是否存在,是否有必要讀取這個文件 tombstones map[string][]TimeRange // 用於記錄哪些 key 在指定范圍內的數據是已經被刪除的 }
b 直接對應着 tsm file 中的 Index 部分,通過對 offsets 進行二分查找,可以獲取到指定 key 的所有 block 的索引信息,之后根據 offset 和 size 信息可以取出一個指定的 block 中的所有數據。
type indexEntries struct { Type byte entries []IndexEntry } type IndexEntry struct { // 一個 block 中的 point 都在這個最小和最大的時間范圍內 MinTime, MaxTime int64 // block 在 tsm 文件中偏移量 Offset int64 // block 的具體大小 Size uint32 }
在上一節中說明了通過元數據索引可以獲取到所有 符合要求的 series key,它們對應的 shardID,時間范圍。通過 tsm file 索引,我們就可以根據 series key 和 時間范圍快速定位到數據在 tsm file 中的位置。
從 tsm file 中讀取數據
InfluxDB 中的所有數據讀取操作都通過 Iterator 來完成。
Iterator 是一個抽象概念,並且支持嵌套,一個 Iterator 可以從底層的其他 Iterator 中獲取數據並進行處理,之后再將結果傳遞給上層的 Iterator。
這部分的代碼邏輯比較復雜,這里不展開說明。實際上 Iterator 底層最主要的就是通過 cursor 來獲取數據。
type cursor interface { next() (t int64, v interface{}) } type floatCursor interface { cursor nextFloat() (t int64, v float64) } // 底層主要是 KeyCursor,每次讀取一個 block 的數據 type floatAscendingCursor struct { // 內存中的 value 對象 cache struct { values Values pos int } tsm struct { tdec TimeDecoder // 時間序列化對象 vdec FloatDecoder // value 序列化對象 buf []FloatValue values []FloatValue // 從 tsm 文件中讀取到的 FloatValue 的緩存 pos int keyCursor *KeyCursor } }
cursor 提供了一個 next() 方法用於獲取一個 value 值。每一種數據類型都有一個自己的 cursor 實現。
底層實現都是 KeyCursor,KeyCursor 會緩存每個 Block 的數據,通過 Next() 函數依次返回,當一個 Block 中的內容讀完后再通過 ReadBlock() 函數讀取下一個 Block 中的內容。
-