時間序列數據的存儲和計算 - 開源時序資料庫解析

開源時序資料庫

如圖是17年6月在db-engines上時序資料庫的排名,我會挑選開源的、分散式的時序資料庫做詳細的解析。前十的排名中,RRD是一個老牌的單機存儲引擎,Graphite底層是Whisper,可以認為是一個優化的更強大的RRD資料庫。kdb+、eXtremeDB和Axibase都未開源,不做解析。InfluxDB開源版和Prometheus的底層都是基於levelDB自研的單機的存儲引擎,InfluxDB的商業版支持分散式,Prometheus的roadmap上也規划了分散式存儲引擎的支持計劃。

綜合看下來,我會選擇OpenTSDB、KairosDB和InfluxDB做一個詳細的解析。我對OpenTSDB比較熟悉,研究過它的源碼,所以對OpenTSDB會描述的格外詳細,而對其他時序資料庫了解的沒那麼深入,如果有描述錯的地方,歡迎指正。

一、OpenTSDB

OpenTSDB是一個分散式、可伸縮的時序資料庫,支持高達每秒百萬級的寫入能力,支持毫秒級精度的數據存儲,不需要降精度也可以永久保存數據。其優越的寫性能和存儲能力,得益於其底層依賴的HBase,HBase採用LSM樹結構存儲引擎加上分散式的架構,提供了優越的寫入能力,底層依賴的完全水平擴展的HDFS提供了優越的存儲能力。OpenTSDB對HBase深度依賴,並且根據HBase底層存儲結構的特性,做了很多巧妙的優化。關於存儲的優化,我在這篇文章中有詳細的解析。在最新的版本中,還擴展了對BigTable和Cassandra的支持。

架構

如圖是OpenTSDB的架構,核心組成部分就是TSD和HBase。TSD是一組無狀態的節點,可以任意的擴展,除了依賴HBase外沒有其他的依賴。TSD對外暴露HTTP和Telnet的介面,支持數據的寫入和查詢。TSD本身的部署和運維是很簡單的,得益於它無狀態的設計,不過HBase的運維就沒那麼簡單了,這也是擴展支持BigTable和Cassandra的原因之一吧。

數據模型

OpenTSDB採用按指標建模的方式,一個數據點會包含以下組成部分:

  • metric:時序數據指標的名稱,例如sys.cpu.user,stock.quote等。
  • timestamp:秒級或毫秒級的Unix時間戳,代表該時間點的具體時間。
  • tags:一個或多個標籤,也就是描述主體的不同的維度。Tag由TagKey和TagValue組成,TagKey就是維度,TagValue就是該維度的值。
  • value:該指標的值,目前只支持數值類型的值。

存儲模型

OpenTSDB底層存儲的優化思想,可以參考這篇文章,簡單總結就是以下這幾個關鍵的優化思路:

  • 對數據的優化:為Metric、TagKey和TagValue分配UniqueID,建立原始值與UniqueID的索引,數據表存儲Metric、TagKey和TagValue對應的UniqueID而不是原始值。
  • 對KeyValue數的優化:如果對HBase底層存儲模型十分了解的話,就知道行中的每一列在存儲時對應一個KeyValue,減少行數和列數,能極大的節省存儲空間以及提升查詢效率。
  • 對查詢的優化:利用HBase的Server Side Filter來優化多維查詢,利用Pre-aggregation和Rollup來優化GroupBy和降精度查詢。

UIDTable

接下來看一下OpenTSDB在HBase上的幾個關鍵的表結構的設計,首先是tsdb-uid表,結構如下:

Metric、TagKey和TagValue都會被分配一個相同的固定長度的UniqueID,默認是三個位元組。tsdb-uid表使用兩個ColumnFamily,存儲了Metric、TagKey和TagValue與UniqueID的映射和反向映射,總共是6個Map的數據。

從圖中的例子可以解讀出:

  • TagKey為host,對應的UniqueID為001
  • TagValue為static,對應的UniqueId為001
  • Metric為proc.loadavg.1m,對應的UniqueID為052

為每一個Metric、TagKey和TagValue都分配UniqueID的好處,一是大大降低了存儲空間和傳輸數據量,每個值都只需要3個位元組就可以表示,這個壓縮率是很客觀的;二是採用固定長度的位元組,可以很方便的從row key中解析出所需要的值,並且能夠大大減少Java堆內的內存佔用(bytes相比String能節省很多的內存佔用),降低GC的壓力。

不過採用固定位元組的UID編碼後,對於UID的個數是有上限要求的,3個位元組最多只允許有16777216個不同的值,不過在大部分場景下都是夠用的。當然這個長度是可以調整的,不過不支持動態更改。

DataTable

第二張關鍵的表是數據表,結構如下:

該表中,同一個小時內的數據會存儲在同一行,行中的每一列代表一個數據點。如果是秒級精度,那一行最多會有3600個點,如果是毫秒級精度,那一行最多會有3600000個點。

這張表設計的精妙之處在於row key和qualifier(列名)的設計,以及對整行數據的compaction策略。row key格式為:

其中metric、tagk和tagv都是用uid來表示,由於uid固定位元組長度的特性,所以在解析row key的時候,可以很方便的通過位元組偏移來提取對應的值。Qualifier的取值為數據點的時間戳在這個小時的時間偏差,例如如果你是秒級精度數據,第30秒的數據對應的時間偏差就是30,所以列名取值就是30。列名採用時間偏差值的好處,主要在於能大大節省存儲空間,秒級精度的數據只要佔用2個位元組,毫秒精度的數據只要佔用4個位元組,而若存儲完整時間戳則要6個位元組。整行數據寫入後,OpenTSDB還會採取compaction的策略,將一行內的所有列合併成一列,這樣做的主要目的是減少KeyValue數目。

查詢優化

HBase僅提供簡單的查詢操作,包括單行查詢和範圍查詢。單行查詢必須提供完整的RowKey,範圍查詢必須提供RowKey的範圍,掃描獲得該範圍下的所有數據。通常來說,單行查詢的速度是很快的,而範圍查詢則是取決於掃描範圍的大小,掃描個幾千幾萬行問題不大,但是若掃描個十萬上百萬行,那讀取的延遲就會高很多。

OpenTSDB提供豐富的查詢功能,支持任意TagKey上的過濾,支持GroupBy以及降精度。TagKey的過濾屬於查詢的一部分,GroupBy和降精度屬於對查詢後的結果的計算部分。在查詢條件中,主要的參數會包括:metric名稱、tag key過濾條件以及時間範圍。上面一章中指出,數據表的rowkey的格式為:

從查詢的參數上可以看到,metric名稱和時間範圍確定的話,我們至少能確定row key的一個掃描範圍。但是這個掃描範圍,會把包含相同metric名稱和時間範圍內的所有的tag key的組合全部查詢出來,如果你的tag key的組合有很多,那你的掃描範圍是不可控的,可能會很大,這樣查詢的效率基本是不能接受的。

我們具體看一下OpenTSDB對查詢的優化措施:

  • Server side filter

    HBase提供了豐富和可擴展的filter,filter的工作原理是在server端掃描得到數據後,先經過filter的過濾後再將結果返回給客戶端。Server side filter的優化策略無法減少掃描的數據量,但是可以大大減少傳輸的數據量。OpenTSDB會將某些條件的tag key filter轉換為底層HBase的server side filter,不過該優化帶來的效果有限,因為影響查詢最關鍵的因素還是底層範圍掃描的效率而不是傳輸的效率。
  • 減少範圍查詢內掃描的數據量

    要想真正提高查詢效率,還是得從根本上減少範圍掃描的數據量。注意這裡不是減小查詢的範圍,而是減少該範圍內掃描的數據量。這裡用到了HBase一個很關鍵的filter,即FuzzyRowFilter,FuzzyRowFilter能夠根據指定的條件,在執行範圍掃描時,動態的跳過一定數據量。但不是所有OpenTSDB提供的查詢條件都能夠應用該優化,需要符合一定的條件,具體要符合哪些條件就不在這裡說明了,有興趣的可以去了解下FuzzyRowFilter的原理。
  • 範圍查詢優化成單行查詢

    這個優化相比上一條,更加的極端。優化思路非常好理解,如果我能夠知道要查詢的所有數據對應的row key,那就不需要範圍掃描了,而是單行查詢就行了。這裡也不是所有OpenTSDB提供的查詢條件都能夠應用該優化,同樣需要符合一定的條件。單行查詢要求給定確定的row key,而數據表中row key的組成部分包括metric名稱、timestamp以及tags,metric名稱和timestamp是能夠確定的,如果tags也能夠確定,那我們就能拼出完整的row key。所以很簡單,如果要能夠應用此優化,你必須提供所有tag key對應的tag value才行。

以上就是OpenTSDB對HBase查詢的一些優化措施,但是除了查詢,對查詢後的數據還需要進行GroupBy和降精度。GroupBy和降精度的計算開銷也是非常可觀的,取決於查詢後的結果的數量級。對GroupBy和降精度的計算的優化,幾乎所有的時序資料庫都採用了同樣的優化措施,那就是pre-aggregation和auto-rollup。思路就是預先進行計算,而不是查詢後計算。不過OpenTSDB在已發布的最新版本中,還未支持pre-aggregation和rollup。而在開發中的2.4版本中,也只提供了半吊子的方案,它只提供了一個新的介面支持將pre-aggregation和rollup的結果進行寫入,但是對數據的pre-aggregation和rollup的計算還需要用戶自己在外層實現。

總結

OpenTSDB的優勢在於數據的寫入和存儲能力,得益於底層依賴的HBase所提供的能力。劣勢在於數據查詢和分析的能力上的不足,雖然在查詢上已經做了很多的優化,但是不是所有的查詢場景都能適用。可以說,OpenTSDB在TagValue過濾查詢優化,是這次要對比的幾個時序資料庫中,優化的最差的。在GroupBy和Downsampling的查詢上,也未提供Pre-aggregation和Auto-rollup的支持。不過在功能豐富程度上,OpenTSDB的API是支持最豐富的,這也讓OpenTSDB的API成為了一個標杆。

二、KairosDB

KairosDB最初是從OpenTSDB 1.x版本fork出來的一個分支,目的是在OpenTSDB的代碼基礎上進行二次開發來滿足新的功能需求。其改造之一就是支持可插拔式的存儲引擎,例如支持H2可以方便本地開發和測試,而不是像OpenTSDB一樣與HBase強耦合。在其最初的幾個版本中,HBase也是作為其主要的存儲引擎。但是在之後的存儲優化中,慢慢使用Cassandra替換了HBase,它也是第一個基於Cassandra開發的時序資料庫。在最新的幾個版本中,已不再支持HBase,因為其存儲優化使用了Cassandra所特有而HBase沒有的一些特性。

在整體架構上,和OpenTSDB比較類似,都是採用了一個比較成熟的資料庫來作為底層存儲引擎。自己的主要邏輯僅僅是在存儲引擎層之上很薄的一個邏輯層,這層邏輯層的部署架構是一個無狀態的組件,可以很容易的水平擴展。

在功能差異性上,它在OpenTSDB 1.x上做二次開發,也是為了對OpenTSDB的一些功能做優化,或做出一些OpenTSDB所沒有的功能。我大概羅列下我看到的主要的功能差異:

  1. 可插拔式的存儲引擎:OpenTSDB在早期與HBase強耦合,為了追求極致的性能,甚至自研了一個非同步的HBase Client(現在作為獨立的一個開源項目輸出:AsyncHBase)。這樣也導致其整個代碼都是採用非同步驅動的模式編寫,不光增加了代碼的複雜度和降低可閱讀性,也加大了支持多種存儲引擎的難度。KairosDB嚴格定義了存儲層的API Interface,整體邏輯與存儲層耦合度較低,能比較容易的擴展多種存儲引擎。當然現在最新版的OpenTSDB也能夠額外支持Cassandra和BigTable,但是從整體的架構上,還不能說是一個支持可插拔式存儲引擎的架構。
  2. 支持多種數據類型及自定義類型的值:OpenTSDB只支持numeric的值,而KairosDB支持numeric、string類型的值,也支持自定義數值類型。在某些場景下,metric value不是一個簡單的數值,例如你要統計這個時間點的TopN,對應的metric value可能是一組string值。可擴展的類型,讓未來的需求擴展會變得容易。從第一第二點差異可以看出,KairosDB基於OpenTSDB的第一大改造就是將OpenTSDB的功能模型和代碼架構變得更加靈活。
  3. 支持Auto-rollup:目前大部分TSDB都在朝著支持pre-aggregation和auto-rollup的方向發展,OpenTSDB是少數的不支持該feature的TSDB,在最新發布的OpenTSDB版本中,甚至都不支持多精度數據的存儲。不過現在KairosDB支持的auto-rollup功能,採取的還是一個比較原始的實現方式,在下面的章節會詳細講解。
  4. 不同的存儲模型:存儲是TSDB核心中的核心,OpenTSDB在存儲模型上使用了UID的壓縮優化,來優化查詢和存儲。KairosDB採取了一個不同的思路,利用了Cassandra寬表的特性,這也是它從HBase轉向Cassandra的一個最重要的原因,在下面的章節會詳細講解。

存儲模型

OpenTSDB的存儲模型細節,可以參考這篇文章。其主要設計特點是採用了UID編碼,大大節省了存儲空間,並且利用UID編碼的固定位元組數的特性,利用HBase的Filter做了很多查詢的優化。但是採用UID編碼後也帶來了很多的缺陷,一是需要維護metric/tagKey/tagValue到UID的映射表,所有data point的寫入和讀取都需要經過映射表的轉換,映射表通常會緩存在TSD或者client,增加了額外的內存消耗;二是由於採用了UID編碼,導致metric/tagKey/tagValue的基數是有上限的,取決於UID使用的位元組數,並且在UID的分配上會有衝突,會影響寫入。

本質上,OpenTSDB存儲模型採用的UID編碼優化,主要解決的就兩個問題:

  1. 存儲空間優化:UID編碼解決重複的row key存儲造成的冗餘的存儲空間問題。
  2. 查詢優化:利用UID編碼後TagKey和TagValue固定位元組長度的特性,利用HBase的FuzzyRowFilter做特定場景的查詢優化。

KairosDB在解決這兩個問題上,採取了另外一種不同的方式,使其不需要使用UID編碼,也不存在使用UID編碼後遺留的問題。先看下KairosDB的存儲模型是怎樣的,它主要由以下三張表構成:

  1. DataPoints: 存儲所有原始數據點,每個數據點也是由metric、tags、timestamp和value構成。該表中一行數據的時間跨度是三周,也就是說三周內的所有數據點都存儲在同一行,而OpenTSDB內的行的時間跨度只有一個小時。RowKey的組成與OpenTSDB類似,結構為<metric><timestamp><tagk1><tagv1><tagk2>tagv2>...<tagkn><tagvn>,不同的是metric, tag key和tag value都存儲原始值,而不是UID。
  2. RowKeyIndex: 該表存儲所有metric對應DataPoints表內所有row key的映射,也就是說同一個metric上寫入的所有的row key,都會存儲在同一行內,並且按時間排序。該表主要被用於查詢,在根據tag key或者tag value做過濾時,會先從這張表過濾出要查詢的時間段內所有符合條件的row key,後在DataPoints表內查詢數據。
  3. StringIndex: 該表就三行數據,每一行分別存儲所有的metric、tag key和tag value。

KairosDB採取的存儲模型,是利用了Cassandra寬表的特性。HBase的底層文件存儲格式中,每一列會對應一個KeyValue,Key為該行的RowKey,所以HBase中一行中的每一列,都會重複的存儲相同的RowKey,這也是為何採用了UID編碼後能大大節省存儲空間的主要原因,也是為何有了UID編碼後還能採用compaction策略(將一行中所有列合併為一列)來進一步壓縮存儲空間的原因。而Cassandra的底層文件存儲格式與HBase不同,它一行數據不會為每一列都重複的存儲RowKey,所以它不需要使用UID編碼。Cassandra內降低存儲空間的一個優化方案就是縮減行數,這也是為何它一行存儲三周數據而不是一個小時數據的原因。要進一步了解兩種設計方案的原因,可以看下HBase文件格式以及Cassandra文件格式。

利用Cassandra的寬表特性,即使不採用UID編碼,存儲空間上相比採用UID編碼的OpenTSDB,也不會差太多。可以看下官方的解釋:

在查詢優化上,採取的也是和OpenTSDB不一樣的優化方式。先看下KairosDB內查詢的整個流程:

1. 根據查詢條件,找出所有DataPoints表裡的row key

  • 如果有自定義的plugin,則從plugin中獲取要查詢的所有row key。(通過Plugin可以擴展使用外部索引系統來對row key進行索引,例如使用ElasticSearch)
  • 如果沒有自定義的plugin,則在RowKeyIndex表裡根據metric和時間範圍,找出所有的row key。(根據列名的範圍來縮小查詢範圍,列名的範圍是(metric+startTime, metric+endTime))

2. 根據row key,從DataPoints表裡找出所有的數據

相比OpenTSDB直接在數據表上進行掃描來過濾row key的方式,KairosDB利用索引表無疑會大大減少掃描的數據量。在metric下tagKey和tagValue組合有限的情況下,會大大的提高查詢效率。並且KairosDB還提供了QueryPlugin的方式,能夠擴展利用外部組件來對row key進行索引,例如可以利用ElasticSearch,或者其他的索引系統,畢竟通過索引的方式,才是最優的查詢方案,這也是Heroic相比KairosDB最大的一個改進的地方。

Auto-rollup

KairosDB的官方文檔中有關於auto-rollup如何配置的章節,但是在討論組內,其關於auto-rollup的說明如下:

總結來說,目前KairosDB提供的auto-rollup方案,還是比較簡單的實現。就是一個可配置的單機組件,能夠定時啟動,把已經寫入的數據讀出後進行aggregation後再次寫入,確實非常的原始,可用性和性能都比較低。

但是有總比沒有好,支持auto-rollup一定是所有TSDB的趨勢,也是能拉開功能差異和提高核心競爭力的關鍵功能。

BlueFlood

上面主要分析了KairosDB,第一個基於Cassandra構建的TSDB,那乾脆繼續分析下其他基於Cassandra構建的TSDB。

BlueFlood也是一個基於Cassandra構建的TSDB,從這個PPT介紹上可以看到整體架構上核心組成部分主要有三個:

  • Ingest module: 處理數據寫入。
  • Rollup module: 做自動的預聚合和降精度。
  • Query module: 處理數據查詢。

相比KairosDB,其在數據模型上與其他的TSDB有略微差異,主要在:

  • 引入了租戶的維度:這是一個創新,如果你是做一個服務化的TSDB,那租戶這個維度是必需的。
  • 不支持Tag:這一點上,是比較讓我差異的地方。在大多數TSDB都基本上把Tag作為模型的不可缺少部分的情況下,BlueFlood在模型上居然不支持Tag。不過這有可能是其沒有想好如何優化Tag維度查詢的一種取捨,既然沒想好怎麼優化,那乾脆就先不支持,反正未來再去擴展Tag是可以完全兼容的。BlueFlood當前已經利用ElasticSearch去構建metric的索引,我相信它未來的方案,應該也是基於ElasticSearch去構建Tag的索引,在這個方案完全支持好後,應該才會去引入Tag。

模型上的不足,BlueFlood不需要去考慮Tag查詢如何優化,把精力都投入到了其他功能的優化上,例如auto-rollup。它在auto-rollup的功能支持上,甩了KairosDB和OpenTSDB幾條街。來看看它的Auto-rollup功能的特點:

  • 僅支持固定的Interval:5min,20min,60min,4hour,1day。
  • 提供分散式的Rollup Service:rollup任務可以分散式的調度,rollup的數據是通過離線的批量掃描獲取。

從它14年的介紹PPT上,還可以看到它在未來規劃的幾個功能點:

  • ElasticSearch Indexer and discovery: 目前這個已經實現,但是僅支持metric的索引,未來引入Tag後,可能也會用於Tag的索引。
  • Cloud files exporter for rollups: 這種方式對離線計算更加優化,rollup的大批量歷史數據讀取就不會影響在線的業務。
  • Apache Kafka exporter for rollups: 這種方式相比離線計算更進一步,rollup可以用流計算來做,實時性更加高。

總結來說,如果你不需要Tag的支持,並且對Rollup有強需求,那BlueFlood相比KairosDB會是一個更好的選擇,反之還是選擇KairosDB。

Heroic

第三個要分析的基於Cassandra的TSDB是Heroic,它在DB-Engines上的排名是第19,雖然比BlueFlood和KairosDB都落後,但是我認為它的設計實現卻是最好的一個。關於它的起源,可以看下這篇文章或者這個PPT,都是寶貴的經驗教訓。

Spotify在決定研發Heroic之前,在OpenTSDB、InfluxDB、KairosDB等TSDB中選用KairosDB來替換他們老的監控系統的底層。但是很快就遇到了KairosDB在查詢方面的問題,最主要還是KairosDB對metric和tag沒有索引,在metric和tag基數達到一定數量級後,查詢會變的很慢。所以Spotify研發Heroic的最大動機就是解決KairosDB的查詢問題,採用的解決方案是使用ElasticSearch來作為索引優化查詢引擎,而數據的寫入和數據表的Schema則完全與KairosDB一致。

簡單總結下它的特點:

  1. 完整的數據模型,完全遵循metric2.0的規範。
  2. 數據存儲模型與KairosDB一致,使用ElasticSearch優化查詢引擎。(這是除了InfluxDB外,其他TSDB如KairosDB、OpenTSDB、BlueFlood等現存最大的問題,是其核心競爭力之一)
  3. 不支持auto-rollup,這是它的缺陷之一。

如果你需要TSDB支持完整的數據模型,且希望得到高效的索引查詢,那Heroic會是你的選擇。

三、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中的幾個基本概念,看下具體的例子:

上面是一條向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支持在Measurement內任意維度的條件查詢,你可以指定任意某個Tag或者Filed的條件做查詢。接著上面的數據案例,你可以構造以下查詢條件:

從數據模型以及查詢的條件上看,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數據位置。

4. 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。

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

Memory-based Index

如上是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實現了一個特殊的存儲引擎來做索引數據的存儲,其結構也與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的發展,不過可惜的是集群版沒有開源。

更多技術乾貨敬請關注云棲社區知乎機構號:阿里云云棲社區 - 知乎

推薦閱讀:

分散式關係型資料庫 TiDB 正式發布 RC2 版
原諒我這麼幼稚,所以才會喜歡你這麼久 #MySQL#
MySQL學習筆記(一)表類型的選擇
PebblesDB讀後感

TAG:分布式系统 | 数据库 | 云存储 |