influxdb原理那些事

  • 2020 年 2 月 17 日
  • 筆記

戳藍字「TopCoder」關注我們哦!

編者註:InfluxDB是一個開源的時序數據庫,使用GO語言開發,特別適合用於處理和分析資源監控數據這種時序相關數據。而InfluxDB自帶的各種特殊函數如求標準差,隨機取樣數據,統計數據變化比等,使數據統計和實時分析變得十分方便。

在目前的APM和後續的IoT場景中,InfluxDB會發揮越來越重要的作用,那麼InfluxDB是如何保存數據並且高性能對外提供存取服務的呢?下面就一起來看下InfluxDB原理的那些事 ~

centos下influxdb默認配置路徑為/etc/influxdb/influxdb.conf,influxdb數據存儲主要有3個目錄,分別是meta、wal和data。meta主要存放元數據,該目錄下有一個meta.db文件;wal目錄存放預寫日誌,以.wal結尾;data目錄存放TSM文件,以.tsm文件結尾。

在同一個database中,retention policy、measurement、tag sets 完全相同的數據同屬於一個 series,從Index數據排列來看,同一個 series 的數據在物理上會按照時間順序排列存儲在一起,series 的 key 為 measurement + tags set 序列化字符串,series代碼中結構如下:

type Series struct {      mu          sync.RWMutex      Key         string              // series key      Tags        map[string]string   // tags      id          uint64              // id      measurement *Measurement        // measurement  }  

WAL就是一種寫優化且固定格式的預寫文件,允許寫入持久化但是不易查詢,對WAL的寫入就是append操作。influxdb的WAL就是一系列格式為 _00xxx.wal 的文件,文件號單調遞增,默認當超過10M時就會新寫一個WAL文件,每個WAL文件都會存儲經過壓縮的數據。當一個新的Point數據被寫入時,首先經過壓縮寫入到WAL中,在返回之前會寫入到內存的索引中,這意味着數據寫入後立馬可通過索引可見,同時批量寫入意味着更高效率。當WAL日誌對應的數據被寫入到TSM中後,WAL日誌就可以刪除了。WAL具體數據格式如下:

Cache就是WAL的內存表示,它在運行時可被查詢並且與TSM中保存的文件進行合併。當緩存大小超過 cache-snapshot-memory-size 時會觸發緩存數據寫入到TSM文件,並刪除對應的WAL段文件;當緩存大小超過 cache-max-memory-size 時會導致cache拒絕新的寫入,避免數據流量過大影響服務穩定性。除了內存的閾值限制之外,緩存還會在 cache-snapshot-write-cold-duration 配置的時間間隔定期將緩存數據寫入到TSM文件。通過重讀所有WAL文件,Influxdb可以在啟動時重建緩存。

我們知道Cache就是WAL的內存表示,相當於LSM中的memtable,在內存中它就是一個map結構,其key就是 series key+fieldName,當然key也包含有分隔符(目前是#!~#),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  }  

插入數據就是往WAL和Cache寫數據,當influxdb啟動時,會遍歷所有WAL文件構建Cache,這樣保證系統出現故障也不會造成數據丟失。

TSM文件是influxdb數據存儲的一系列只讀文件集合,這些文件結構類似於leveldb中的SSTable,一個TSM文件格式如下:

  • Header:頭部信息,4位magic字段+1位version字段;
  • Blocks:CRC+數據存儲字段,數據的長度在index字段存儲;
  • Index:索引順序按照先key後時間戳來,如果key相同則按照時間戳排序,key包括measurement+tag set+一個filed,如果一個point包含多個field,則包含多個索引條目;每個索引條目以key len和key開始,標準的TLV格式,然後是block類型(float,int,bool,string)以及該block的條目數,之後是block的最小、最大時間戳,最後是block所在的文件偏移量以及block大小 — 也就是說,包含該key的TSM文件中每個block都有一個索引block條目;
  • footer:存儲了索引開頭的offset。

從TSM文件結構來看,解析TSM需要讀取footer來確定Index和Blocks分界點,然後讀取Index信息來提取對應的Block,才能組成一個完整的TSM索引+數據信息。Block對應的數據也是經過壓縮的,以便減少存儲空間,block包含時間戳、series和field值,每個block都有1個位元組的header,之後是壓縮過的時間戳和值:

針對不同類型數據採用不同壓縮編碼,比如時間戳、整形、浮點數和字符串等,字符串使用Snappy壓縮進行編碼,每個字符串連續打包然後壓縮成一個較大的塊。

數據刪除通過向WAL寫入刪除條目然後更新Cache來完成刪除操作,同時Influxdb會為包含相關數據的TSM文件寫入一個tombstone文件,這些tombstone文件被用於在啟動時忽略相應的block,以及在compaction時期移除已刪除的數據,換句話說,數據刪除在influxdb中是一個低效率操作,特別是針對大數據量刪除來說,並且只有等待數據合併時才會真正刪除數據。

如果直接讀取TSM中的Index進行索引查詢無疑是低效的,因此會在內存中構建方便二分查詢,結構如下:

由於Index中各個key長度不定,因此使用offsets字段進行二分搜索,offsets數組中儲存的是對應Index中key數據偏移量。TSM中的Index字段也是定長的,也是可以執行二分查找,找到要查詢的數據的 BlockIndex 的內容,然後根據偏移量以及 block 長度就可以從 tsm 文件中快速讀取出一個 block 數據。

通過上面的Index+offsets數據和TSM文件的Index數據,可以通過某個key(measurement+tag set+一個filed)來進行數據查詢,但是我們一般的查詢都是通過某個tag的值來查找的,比如以下查詢代碼:

select * FROM cpu WHERE host='s01' AND time > now() - 10h  

那麼該如何使用上面所說的索引查找流程呢?是不是存在一個單個tag value和key的映射關係呢,對,確實存在這樣的結構:

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  }  

以命令 select * FROM cpu WHERE host='s01' AND time > now() – 10h 為例,通過 seriesByTagKeyValue['host']['s01'] 獲取到所有匹配的 series 的 ID值,然後再通過map結構seriesByID根據series id獲取到series對象,注意匹配到的某個tag value的可能不止一個series。這樣,我們就可以在O(1)時間複雜度內找到tag value對應的 series key,然後根據查詢請求的時間範圍,從不同shard中獲取每一個series在指定時間段內的數據,後續的查詢則和 tsm file 中的 Index 的在內存中的緩存相關了。