Influxdb原理詳解

 

 
本文屬於《 InfluxDB系列教程》文章系列,該系列共包括如下 15 部分:
  1. InfluxDB學習之InfluxDB的安裝和簡介
  2. InfluxDB學習之InfluxDB的基本概念
  3. InfluxDB學習之InfluxDB的基本操做
  4. InfluxDB學習之InfluxDB的HTTP API寫入操做
  5. InfluxDB學習之InfluxDB數據保留策略(Retention Policies)
  6. InfluxDB學習之InfluxDB連續查詢(Continuous Queries)
  7. InfluxDB學習之InfluxDB的HTTP API查詢操做
  8. InfluxDB學習之InfluxDB的關鍵概念
  9. InfluxDB學習之InfluxDB經常使用函數(一)聚合類函數
  10. InfluxDB學習之InfluxDB經常使用函數(二)選擇類函數
  11. InfluxDB學習之InfluxDB經常使用函數(三)變換類函數
  12. InfluxDB學習之再說連續查詢
  13. Influxdb原理詳解
  14. InfluxDB安裝後web頁面沒法訪問的解決方案
  15. InfluxDB數據備份和恢復方法,支持本地和遠程備份

系列詳情請看:《InfluxDB系列教程php

 

InfluxDB是一款Go語言寫的時序數據庫,本文主要介紹下Influxdb的架構和基本原理。html

更多InfluxDB詳細教程請看:InfluxDB系列學習教程目錄linux

InfluxDB技術交流羣:580487672(點擊加入)web

1、InfluxDB特色

 

  • 能夠設置metric的保存時間。
  • 支持經過條件過濾以及正則表達式刪除數據。
  • 支持相似 sql 的語法。
  • 能夠設置數據在集羣中的副本數。
  • 支持按期採樣數據,寫入另外的measurement,方便分粒度存儲數據。

2、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 請求的相應字段中指定。sql

  • 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 的數據在物理上會按照時間順序排列存儲在一塊兒。json

    series 的 key 爲 measurement + 全部 tags 的序列化字符串,這個 key 在以後會常常用到。api

    代碼中的結構以下:

    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 中。

    3、存儲引擎 - 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

    Influxdb原理詳解

    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 文件合併成一個,使每個文件儘可能達到單個文件的最大大小,減小文件的數量,而且一些數據的刪除操做也是在這個時候完成。

    4、目錄與文件結構

    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 文件

    Influxdb原理詳解

    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 文件的主要格式以下:

    Influxdb原理詳解

     

    主要分爲四個部分: Header, Blocks, Index, Footer

    其中 Index 部分的內容會被緩存在內存中,下面詳細說明一下每個部分的數據結構。

    Header

    Influxdb原理詳解

  • MagicNumber (4 bytes): 用於區分是哪個存儲引擎,目前使用的 tsm1 引擎,MagicNumber 爲 0x16D116D1
  • Version (1 byte): 目前是 tsm1 引擎,此值固定爲 1
    Blocks

    Influxdb原理詳解

    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 文件中的格式相同。

    IndexInfluxdb原理詳解

    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 在詳細索引信息中的位置而建立的,能夠被用於二分查找來實現快速檢索。

    Influxdb原理詳解

    Influxdb原理詳解

    offsets 是一個數組,其中存儲的值爲每個 key 在 Index 表中的位置,因爲 key 的長度固定爲 2字節,因此根據這個位置就能夠找到該位置上對應的 key 的內容。

    當指定一個要查詢的 key 時,就能夠經過二分查找,定位到其在 Index 表中的位置,再根據要查詢的數據的時間進行定位,因爲 KeyIndex  中的 BlockIndex 結構是定長的,因此也能夠進行一次二分查找,找到要查詢的數據所在的 BlockIndex 的內容,以後根據偏移量以及 block 長度就能夠從 tsm 文件中快速讀取出一個 block 的內容。

    Influxdb原理詳解

    tsm file 的最後8字節的內容存放了 Index 部分的起始位置在 tsm file 中的偏移量,方便將索引信息加載到內存中。

    5、數據查詢與索引結構

    因爲 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 中的內容。

相關文章
相關標籤/搜索