開源時序數據庫解析(三):InfluxDB

InfluxDB

InfluxDB在DB-Engines的時序數據庫類別裡排名第一,實至名歸,從它的功能豐富性、易用性以及底層實現來看,都有很多的亮點,值得大篇幅來分析。

首先簡單歸納下它的幾個比較重要的特性:

  1. 極簡架構:單機版的InfluxDB只需要安裝一個binary,即可運行使用,完全沒有任何的外部依賴。相比來看幾個反面例子,OpenTSDB底層是HBase,拖家帶口就得帶上ZooKeeper、HDFS等,如果你不熟悉Hadoop技術棧,一般運維起來是有一定的難度,這也是其被人抱怨最多的一個點。KairosDB稍微好點,它依賴Cassandra和ZooKeeper,單機測試可以使用H2。總的來說,依賴一個外部的分佈式數據庫的TSDB,在架構上會比完全自包含的TSDB複雜一點,畢竟一個成熟的分佈式數據庫本身就很複雜,當然這一點在雲計算這個時代已經完全消除。
  2. TSM Engine:底層採用自研的TSM存儲引擎,TSM也是基於LSM的思想,提供極強的寫能力以及高壓縮率,在後面的章節會對其做一個比較詳細的分析。
  3. InfluxQL:提供SQL-Like的查詢語言,極大的方便了使用,數據庫在易用性上演進的終極目標都是提供Query Language。
  4. Continuous Queries: 通過CQ能夠支持auto-rollup和pre-aggregation,對常見的查詢操作可以通過CQ來預計算加速查詢。
  5. TimeSeries Index: 對Tags會進行索引,提供高效的檢索。這一項功能,對比OpenTSDB和KairosDB等,在Tags檢索的效率上提升了不少。OpenTSDB在Tags檢索上做了不少的查詢優化,但是受限於HBase的功能和數據模型,所以然並卵。不過目前穩定版中的實現採用的是memory-based index的實現方式,這種方案在實現上比較簡單,查詢上效率最高,但是帶來了不少的問題,在下面的章節會詳細描述。
  6. Plugin Support: 支持自定義插件,能夠擴展到兼容多種協議,如Graphite、collectd和OpenTSDB。

在下面的章節,會主要對其基本概念、TSM存儲引擎、Continuous Queries以及TimeSeries Index做詳細的解析。

基本概念

先來了解下InfluxDB中的幾個基本概念,看下具體的例子:

 INSERT machine_metric,cluster=Cluster-A,hostname=host-a cpu=10 1501554197019201823

上面是一條向InfluxDB中寫入一條數據的命令行,來看下這條數據由哪幾個部分組成:

  • Measurement:Measurement的概念與OpenTSDB的Metric類似,代表數據所屬監控指標的名稱。例如上述例子是對機器指標的監控,所以其measurement命名為machine_metric。
  • Tags:與OpenTSDB的Tags概念類似,用於描述主體的不同的維度,允許存在一個或多個Tag,每個Tag也是由TagKey和TagValue構成。
  • Field:在OpenTSDB的邏輯數據模型中,一行metric數據對應一個value。而在InfluxDB中,一行measurement數據可以對應多個value,每個value根據Field來區分。
  • Timestamp: 時序數據的必備屬性,代表該條數據所屬的時間點,可以看到InfluxDB的時間精度能夠精確到納秒。
  • TimeSeries:Measurement+Tags的組合,在InfluxDB中被稱為TimeSeries。TimeSeries就是時間線,根據時間能夠定位到某個時間點,所以TimeSeries+Field+Timestamp能夠定位到某個Value。這個概念比較重要,在後續的章節中都會提到。

最終在邏輯上每個Measurement內的數據會組織成一張大的數據表,如下圖所示:

開源時序數據庫解析(三):InfluxDB

在查詢時,InfluxDB支持在Measurement內任意維度的條件查詢,你可以指定任意某個Tag或者Filed的條件做查詢。接著上面的數據案例,你可以構造以下查詢條件:

SELECT * FROM "machine_metric" WHERE time > now() - 1h; 
SELECT * FROM "machine_metric" WHERE "cluster" = "Cluster-A" AND time > now() - 1h;
SELECT * FROM "machine_metric" WHERE "cluster" = "Cluster-A" AND cpu > 5 AND time > now() - 1h;

從數據模型以及查詢的條件上看,Tag和Field沒有任何區別。從語義上來看,Tag用於描述Measurement,而Field用於描述Value。從內部實現來上看,Tag會被全索引,而Filed不會,所以根據Tag來進行條件查詢會比根據Filed來查詢效率高很多。

TSM

InfluxDB底層的存儲引擎經歷了從LevelDB到BlotDB,再到選擇自研TSM的過程,整個選擇轉變的思考可以在其官網文檔裡看到。整個思考過程很值得借鑑,對技術選型和轉變的思考總是比平白的描述某個產品特性讓人印象深刻的多。

我簡單總結下它的整個存儲引擎選型轉變的過程,第一階段是LevelDB,選型LevelDB的主要原因是其底層數據結構採用LSM,對寫入很友好,能夠提供很高的寫入吞吐量,比較符合時序數據的特性。在LevelDB內,數據是採用KeyValue的方式存儲且按Key排序,InfluxDB使用的Key設計是SeriesKey+Timestamp的組合,所以相同SeriesKey的數據是按timestamp來排序存儲的,能夠提供很高效的按時間範圍的掃描。

不過使用LevelDB的一個最大的問題是,InfluxDB支持歷史數據自動刪除(Retention Policy),在時序數據場景下數據自動刪除通常是大塊的連續時間段的歷史數據刪除。LevelDB不支持Range delete也不支持TTL,所以要刪除只能是一個一個key的刪除,會造成大量的刪除流量壓力,且在LSM這種數據結構下,真正的物理刪除不是即時的,在compaction時才會生效。各類TSDB實現數據刪除的做法大致分為兩類:

  1. 數據分區:按不同的時間範圍劃分為不同的分區(Shard),因為時序數據寫入都是按時間線性產生的,所以分區的產生也是按時間線性增長的,寫入通常是在最新的分區,而不會散列到多個分區。分區的優點是數據回收的物理刪除非常簡單,直接把整個分區刪除即可。缺點是數據回收的精細度比較大,為整個分區,而回收的時間精度取決於分區的時間跨度。分區的實現可以是在應用層提供,也可以是存儲引擎層提供,例如可以利用RocksDB的column family來作為數據分區。InfluxDB採用這種模式,默認的Retention Policy下數據會以7天時間跨度組成為一個分區。
  2. TTL:底層數據引擎直接提供數據自動過期的功能,可以為每條數據設定存儲時間(time to live),當數據存活時間到達後存儲引擎會自動對數據進行物理刪除。這種方式的優點是數據回收的精細度很高,精細到秒級及行級的數據回收。缺點是LSM的實現上,物理刪除發生在compaction的時候,比較不及時。RocksDB、HBase、Cassandra和阿里雲表格存儲都提供數據TTL的功能。

InfluxDB採用的是第一種策略,會按7天一個週期,將數據分為多個不同的Shard,每個Shard都是一個獨立的數據庫實例。隨著運行時間的增長,shard的個數會越來越多。而由於每個shard都是一個獨立的數據庫實例,底層都是一套獨立的LevelDB存儲引擎,這時帶來的問題是,每個存儲引擎都會打開比較多的文件,隨著shard的增多,最終進程打開的文件句柄會很快觸及到上限。LevelDB底層採用level compaction策略,是文件數多的原因之一。實際上level compaction策略不適合時序數據這種寫入模式,這點原因InfluxDB沒有提及。

由於遇到大量的客戶反饋文件句柄過多的問題,InfluxDB在新版本的存儲引擎選型中選擇了BoltDB替換LevelDB。BoltDB底層數據結構是mmap B+樹,其給出的選型理由是:1.與LevelDB相同語義的API;2.純Go實現,便於集成和跨平臺;3.單個數據庫只使用一個文件,解決了文件句柄消耗過多的問題,這條是他們選型BoltDB的最主要理由。但是BoltDB的B+樹結構與LSM相比,在寫入能力上是一個弱勢,B+樹會產生大量的隨機寫。所以InfluxDB在使用BoltDB之後,很快遇到了IOPS的問題,當數據庫大小達到幾個GB後,會經常遇到IOPS的瓶頸,極大影響寫入能力。雖然InfluxDB後續也採用了一些寫入優化措施,例如在BoltDB之前加了一層WAL,數據寫入先寫WAL,WAL能保證數據是順序寫盤,但是最終寫入BoltDB還是會帶來比較大的IOPS資源消耗。

InfluxDB在經歷了幾個小版本的BoltDB後,最終決定自研TSM,TSM的設計目標一是解決LevelDB的文件句柄過多問題,二是解決BoltDB的寫入性能問題。TSM全稱是Time-Structured Merge Tree,思想類似LSM,不過是基於時序數據的特性做了一些特殊的優化。來看下TSM的一些重要組件:

  1. Write Ahead Log(WAL) : 數據會先寫入WAL,後進入memory-index和cache,寫入WAL會同步刷盤,保證數據持久化。Cache內數據會異步刷入TSM File,在Cache內數據未持久化到TSM File之前若遇到進程crash,則會通過WAL內的數據來恢復cache內的數據,這個行為與LSM是完全類似的。
  2. Cache: TSM的Cache與LSM的MemoryTable類似,其內部的數據為WAL中未持久化到TSM File的數據。若進程發生failover,則cache中的數據會根據WAL中的數據進行重建。Cache內數據保存在一個SortedMap中,Map的Key為TimeSeries+Timestamp的組成。所以可以看到,在內存中數據是按TimeSeries組織的,TimeSeries中的數據按時間順序存放。
  3. TSM Files: TSM File與LSM的SSTable類似,TSM File由四個部分組成,分別為:header, blocks, index和footer。其中最重要的部分是blocks和index:
  • Block:每個block內存儲的是某個TimeSeries的一段時間範圍內的值,即某個時間段下某個measurement的某組tag set對應的某個field的所有值,Block內部會根據field的不同的值的類型採取不同的壓縮策略,以達到最優的壓縮效率。
  • Index:文件內的索引信息保存了每個TimeSeries下所有的數據Block的位置信息,索引數據按TimeSeries的Key的字典序排序。在內存中不會把完整的index數據加載進去,這樣會很大,而是隻對部分Key做索引,稱之為indirectIndex。indirectIndex中會有一些輔助定位的信息,例如該文件中的最小最大時間以及最小最大Key等,最重要的是保存了部分Key以及其Index數據的文件offset信息。若想要定位某個TimeSeries的Index數據,會先根據內存中的部分Key信息找到與其最相近的Index Offset,之後從該起點開始順序掃描文件內容再精確定位到該Key的Index數據位置。
  1. Compaction: compaction是一個將write-optimized的數據存儲格式優化為read-optimized的數據存儲格式的一個過程,是LSM結構存儲引擎做存儲和查詢優化很重要的一個功能,compaction的策略和算法的優劣決定了存儲引擎的質量。在時序數據的場景下,基本很少發生update或者delete,數據都是按時間順序生成的,所以基本不會有overlap,Compaction起到的作用主要在於壓縮和索引優化。
  • LevelCompaction: InfluxDB將TSM文件分為4個層級(Level 1-4),compaction只會發生在同層級文件內,同層級的文件compaction後會晉升到下一層級。從這個規則看,根據時序數據的產生特性,level越高數據生成時間越舊,訪問熱度越低。由Cache數據初次生成的TSM文件稱為Snapshot,多個Snapshot文件compaction後產生Level1的TSM文件,Level1的文件compaction後生成level2的文件,依次類推。低Level和高Level的compaction會採用不同的算法,低level文件的compaction採用低CPU消耗的做法,例如不會做解壓縮和block合併,而高level文件的compaction則會做block解壓縮以及block合併,以進一步提高壓縮率。我理解這種設計是一種權衡,compaction通常在後臺工作,為了不影響實時的數據寫入,對compaction消耗的資源是有嚴格的控制,資源受限的情況下必然會影響compaction的速度。而level越低的數據越新,熱度也越高,需要有一種更快的加速查詢的compaction,所以InfluxDB在低level採用低資源消耗的compaction策略,這完全是貼合時序數據的寫入和查詢特性來設計的。
  • IndexOptimizationCompaction: 當Level4的文件積攢到一定個數後,index會變得很大,查詢效率會變的比較低。影響查詢效率低的因素主要在於同一個TimeSeries數據會被多個TSM文件所包含,所以查詢不可避免的需要跨多個文件進行數據整合。所以IndexOptimizationCompaction的主要作用就是將同一TimeSeries下的數據合併到同一個TSM文件中,儘量減少不同TSM文件間的TimeSeries重合度。
  • FullCompaction: InfluxDB在判斷某個Shard長時間內不會再有數據寫入之後,會對數據做一次FullCompaction。FullCompaction是LevelCompaction和IndexOptimization的整合,在做完一次FullCompaction之後,這個Shard不會再做任何的compaction,除非有新的數據寫入或者刪除發生。這個策略是對冷數據的一個規整,主要目的在於提高壓縮率。

Continuous Queries

對InfluxDB內的數據做預聚合和降精度有兩種推薦的策略,一種是使用InfluxData內的數據計算引擎Kapacitor,另一種是使用InfluxDB自帶的Continuous Queries。

CREATE CONTINUOUS QUERY "mean_cpu" ON "machine_metric_db"
BEGIN
SELECT mean("cpu") INTO "average_machine_cpu_5m" FROM "machine_metric" GROUP BY time(5m),cluster,hostname
END

如上是一個簡單的配置Continuous Queries的CQL,所起的作用是能夠讓InfluxDB啟動一個定時任務,每隔5分鐘將『machine_metric』這個measurement下的所有數據按cluster+hostname這個維度進行聚合,計算cpu這個Field的平均值,最終結果寫入average_machine_cpu_5m這個新的measurement內。

InfluxDB的Continuous Queries與KairosDB的auto-rollup功能類似,都是單節點調度,數據的聚合是滯後而非實時的流計算,在計算時對存儲會產生較大的讀壓力。

TimeSeries Index

時序數據庫除了支撐時序數據的存儲和計算外,還需要能夠提供多維度查詢。InfluxDB為了提供更快速的多維查詢,對TimeSeries進行了索引。關於數據和索引,InfluxDB是這麼描述自己的:

InfluxDB actually looks like two databases in one: a time series data store and an inverted index for the measurement, tag, and field metadata.

在InfluxDB 1.3之前,TimeSeries Index(下面簡稱為TSI)只支持Memory-based的方式,即所有的TimeSeries的索引都是放在內存內,這種方式有好處但是也會帶來很多的問題。而在最新發布的InfluxDB 1.3版本上,提供了另外一種方式的索引可供選擇,新的索引方式會把索引存儲在磁盤上,效率上相比內存索引差一點,但是解決了內存索引存在的不少問題。

Memory-based Index

 // Measurement represents a collection of time series in a database. It also
// contains in memory structures for indexing tags. Exported functions are
// goroutine safe while un-exported functions assume the caller will use the
// appropriate locks.
type Measurement struct {
database string
Name string `json:"name,omitempty"`
name []byte // cached version as []byte
mu sync.RWMutex
fieldNames map[string]struct{}
// in-memory index fields
seriesByID map[uint64]*Series // lookup table for series by their id

seriesByTagKeyValue map[string]map[string]SeriesIDs // map from tag key to value to sorted set of series ids
// lazyily created sorted series IDs
sortedSeriesIDs SeriesIDs // sorted list of series IDs in this measurement
}
// Series belong to a Measurement and represent unique time series in a database.
type Series struct {
mu sync.RWMutex
Key string
tags models.Tags
ID uint64
measurement *Measurement
shardIDs map[uint64]struct{} // shards that have this series defined
}

如上是InfluxDB 1.3的源碼中對內存索引數據結構的定義,主要有兩個重要的數據結構體:

Series: 對應某個TimeSeries,其內存儲TimeSeries相關的一些基本屬性以及它所屬的Shard

  • Key:對應measurement + tags序列化後的字符串。
  • tags: 該TimeSeries下所有的TagKey和TagValue
  • ID: 用於唯一區分的整數ID。
  • measurement: 所屬的measurement。
  • shardIDs: 所有包含該Series的ShardID列表。

Measurement: 每個measurement在內存中都會對應一個Measurement結構,其內部主要是一些索引來加速查詢。

  • seriesByID:通過SeriesID查詢Series的一個Map。
  • seriesByTagKeyValue:雙層Map,第一層是TagKey對應其所有的TagValue,第二層是TagValue對應的所有Series的ID。可以看到,當TimeSeries的基數變得很大,這個map所佔的內存會相當多。
  • sortedSeriesIDs:一個排序的SeriesID列表。

全內存索引結構帶來的好處是能夠提供非常高效的多維查詢,但是相應的也會存在一些問題:

  • 能夠支持的TimeSeries基數有限,主要受限於內存的大小。若TimeSeries個數超過上限,則整個數據庫會處於不可服務的狀態。這類問題一般由用戶錯誤的設計TagKey引發,例如某個TagKey是一個隨機的ID。一旦遇到這個問題的話,也很難恢復,往往只能通過手動刪數據。
  • 若進程重啟,恢復數據的時間會比較長,因為需要從所有的TSM文件中加載全量的TimeSeries信息來在內存中構建索引。

Disk-based Index

針對全內存索引存在的這些問題,InfluxDB在最新的1.3版本中提供了另外一種索引的實現。得益於代碼設計上良好的擴展性,索引模塊和存儲引擎模塊都是插件化的,用戶可以在配置中自由選擇使用哪種索引。

開源時序數據庫解析(三):InfluxDB

InfluxDB實現了一個特殊的存儲引擎來做索引數據的存儲,其結構也與LSM類似,如上圖就是一個Disk-based Index的結構圖,詳細的說明可以參見設計文檔。

索引數據會先寫入Write-Ahead-Log,WAL中的數據按LogEntry組織,每個LogEntry對應一個TimeSeries,包含Measurement、Tags以及checksum信息。寫入WAL成功後,數據會進入一個內存索引結構內。當WAL積攢到一定大小後,LogFile會Flush成IndexFile。IndexFile的邏輯結構與內存索引的結構一致,表示的也是Measurement到TagKey,TagKey到TagValue,TagValue到TimeSeries的Map結構。InfluxDB會使用mmap來訪問文件,同時文件中對每個Map都會保存HashIndex來加速查詢。

當IndexFile積攢到一定數量後,InfluxDB也提供compaction的機制,將多個IndexFile合併為一個,節省存儲空間以及加速查詢。

總結

InfluxDB內所有的組件全部採取自研,自研的好處是每個組件都可以貼合時序數據的特性來做設計,將性能發揮到極致。整個社區也是非常活躍,但是動不動就會有一次大的功能升級,例如改個存儲格式換個索引實現啥的,對於用戶來說就比較折騰了。總的來說,我還是比較看好InfluxDB的發展,不過可惜的是集群版沒有開源。


分享到:


相關文章: