分散式時序資料庫QTSDB的設計與實現

HULK一線技術雜談發表於2019-06-19

現有的開源時序資料庫influxdb只支援單機執行,在面臨大量資料寫入時,會出現查詢慢,機器負載高,單機容量的限制。

為了解決這一問題,360基礎架構團隊在單機influxdb的基礎上,開發了叢集版——QTSDB。

QTSDB 簡述

QTSDB是一個分散式時間序列資料庫,用於處理海量資料寫入與查詢。實現上,是基於開源單機時序資料庫influxdb 1.7開發的分散式版本,除了具有influxdb本身的特性之外,還有容量擴充套件、副本容錯等叢集功能。

主要特點如下:

  • 為時間序列資料專門編寫的高效能資料儲存, 兼顧寫入效能和磁碟空間佔用;

  • 類sql查詢語句,支援多種統計聚合函式;

  • 自動清理過期資料;

  • 內建連續查詢,自動完成使用者預設的聚合操作;

  • Golang編寫,沒有其它的依賴, 部署運維簡單;

  • 節點動態水平擴充套件,支援海量資料儲存;

  • 副本冗餘設計,自動故障轉移,支援高可用;

  • 最佳化資料寫入,支援高吞吐量;

系統架構

邏輯儲存層次結構

分散式時序資料庫QTSDB的設計與實現

influxdb架構層次最高是database,database下邊根據資料保留時長不同分成了不同的retension policy,形成了database下面的多個儲存容器,因為時序資料庫與時間維度關聯,所以將相同保留時長的內容存放到一起,便於到期刪除。除此之外,在retension policy之下,將retension policy的保留時長繼續細分,每個時間段的資料儲存在一個shard group中,這樣當某個分段的shard group到期之後,會將其整個刪掉,避免從儲存引擎內部摳出部分資料。例如,在database之下的資料,可能是30天保留時長,可能是7天保留時長,他們將存放在不同的retension policy之下。假設將7天的資料繼續按1天進行劃分,就將他們分別存放到7個shard group中,當第8天的資料生成時,會新建一個shard group寫入,並將第 1天的shard group整個刪除。

到此為止,同一個retension policy下,發來的當下時序資料只會落在當下的時間段,也就是隻有最新的shard group有資料寫入,為了提高併發量,一個shard group又分成了多個shard,這些shard全域性唯一,分佈於所有物理節點上,每個shard對應一個tsm儲存引擎,負責儲存資料。

在請求訪問資料時,透過請求的資訊可以鎖定某個database和retension policy,然後根據請求中的時間段資訊,鎖定某個(些)shard group。對於寫入的情況,每條寫入的資料都對應一個serieskey(這個概念後面會介紹),透過對serieskey進行雜湊取模就能鎖定一個shard,進行寫入。而shard是有副本的,在寫入的時候會採用無主多寫的策略同時寫入到每個副本中。查詢時,由於查詢請求中沒有serieskey的資訊,所以只能將shard group內的shard都查詢一遍,針對一個shard,會在其副本中選擇一個可用的物理節點進行訪問。

那麼一個shard group要有多少shard呢,為了達到最大併發量,又不過分干擾資料整體的有序性,在物理節點數和副本數確定後,一個shard group內的shard數量是機器數除以副本數,保障了當下的資料可以均勻寫入到所有的物理節點之上,也不至於因為shard過多影響查詢效率。例如,圖上data叢集有6個物理節點,使用者指定雙副本,那麼就有3個shard。

叢集結構

分散式時序資料庫QTSDB的設計與實現

整個系統分成三個部分:proxy、meta叢集、data叢集。proxy負責接收請求,無狀態,其前可接lvs支援水平擴充套件。meta叢集儲存上面提到的邏輯儲存層次及其與物理節點的對應關係,透過raft協議保障後設資料的強一致,這裡meta資訊儲存在記憶體中,日誌和快照會持久化到磁碟。data叢集是真正的資料儲存節點,資料以shard為單位儲存於其上,每個shard都對應一個tsm儲存引擎。

請求到來的時候,經過lvs鎖定一臺proxy,proxy先根據database、retension policy和時間段到meta叢集查詢meta資訊,最終得到一個shard到物理節點的對映,然後將這個對映關係轉換為物理節點到shard的對映返回給proxy,最後根據這個對映關係,到data叢集指定的物理節點中訪問具體的shard,至於shard之下的資料訪問後邊會介紹。

資料訪問

語法格式

分散式時序資料庫QTSDB的設計與實現

influxdb的查詢提供類似於關聯式資料庫的查詢方式,展示出來類似一個關係表:measurement,時序資料庫的時間作為一個永恆的列,除此之外的列分成兩類:

1、field

一類是field,他們是時序資料最關鍵的資料部分,其值會隨著時間的流動源源不斷的追加,例如兩臺機器之間在每個時間點上的延遲。

2、tag

另一類是tag,他們是一個field值的一些標記,所以都是字串型別,並且取值範圍很有限。例如某個時間點的延遲field值是2ms,對應有兩個標記屬性,從哪臺機器到哪臺機器的延遲,因此可以設計兩個tag:from、to。

measurement展示出來第一行是key,剩下的可以看成value,這樣tag有tagkey,tagvalue,field有fieldkey和fieldvalue。

資料讀寫

分散式時序資料庫QTSDB的設計與實現

當收到一行寫入資料時,會轉化為如下的格式:

measurement+tagkey1+tagvalue1+tagkey2+tagvalue2+fieldkey+fieldvalue+time。

如果一行中存在多個field就會劃分成多條這樣的資料儲存。influxdb的儲存引擎可以理解為一個map,從measurement到fieldkey作為儲存key,後邊的fieldvalue和time是儲存value,這些值會源源不斷追加的,在儲存引擎中,這些值會作為一列儲存到一起,因為是隨時間漸變的資料,將他們儲存到一起可以提升壓縮的效果。另外將儲存key去掉fieldkey之後剩餘部分就是上邊提到的serieskey。

上邊提到,訪問請求在叢集中如何鎖定shard,這裡介紹在一個shard內的訪問。

分散式時序資料庫QTSDB的設計與實現

influxdb的查詢類似於sql語法,但是跟sql語句的零散資訊無法直接查詢儲存引擎,所以需要一些策略將sql語句轉換成儲存key。influxdb透過構建倒排索引來將where後的tag資訊轉換為所有相關的serieskey的集合,然後將每個serieskey拼接上select後邊的fieldkey就組成了儲存key,這樣就可以按列取出對應的資料了。

透過對tsm儲存引擎中儲存key內serieskey的分析,能夠構建出倒排索引,新版本influxdb將倒排索引持久化到每個shard中,與儲存資料的tsm儲存引擎對應,叫做tsi儲存引擎。倒排索引相當於一個三層的map,map的key是measurment,值是一個二層的map,這個二層的map的key是tagkey,對應的值是一個一層的map,這個一層map的key是tagval,對應的值是一個serieskey的集合,這個集合中的每個serieskey字串都包含了map索引路徑上的measurement、tagkey和tagval。

這樣可以分析查詢sql,用from後的measurement查詢倒排索引三級map獲得一個二級map,然後再分析where之後多個過濾邏輯單元,以tagkey1=tagval1為例,將這兩個資訊作為二層map的key,查到最終的值:serieskey的集合,這個集合的每個serieskey字串都包含了measurment、tagkey1和tagval1,他們是滿足當下過濾邏輯單元的serieskey。根據這些邏輯單元的與或邏輯,將其對應的serieskey的集合進行交併運算,最終根據sql的語義過濾出所有的符合其邏輯的serieskey的集合,然後將這些serieskey與select後邊的fieldkey拼接起來,得到最終的儲存·key,就可以讀取資料了。

分散式時序資料庫QTSDB的設計與實現

不帶聚合函式的查詢:如圖,對於一個serieskey,需要拼接眾多的fieldkey,進而取出多個列的資料,他們出來後面臨的問題是怎麼組合為一行的資料,influxdb行列約束比較鬆散,不能單純按照列內偏移確定行。Influxdb把serieskey和time作為判斷列資料為一行的依據,每一個serieskey對應的多列就彙集為一個以多行為粒度的資料流,多個serieskey對應的資料流按照一定順序彙集為一個資料流,作為最終的結果集返回到客戶端。

分散式時序資料庫QTSDB的設計與實現

帶聚合函式的查詢:這種方式與上邊的查詢正好相反,這裡是針對聚合函式引數field,拼接上眾多的serieskey,當然最終目的都是一樣,得到儲存key,多個儲存key可以讀取多個資料流,這些資料流面臨兩種處理,先將他們按照一定的順序彙集為一個資料流,然後按照一定的策略圈定這個資料流內相鄰的一些資料進行聚合計算,進而得到最終聚合後的值。這裡的順序和策略來自於sql語句中group by後的聚合方式。

多資料流的合併聚合方式,也同樣適用於shard之上的查詢結果。

對於寫入就比較簡單了,直接更新資料儲存引擎和倒排索引就可以了。

整個流程

對於訪問的整個流程上邊都已經提到了,這裡整體梳理一下:分成兩個階段,在shard之上的查詢,在shard之下的查詢。

首先訪問請求透過lvs鎖定到某個proxy,proxy到meta叢集中查詢meta資訊,根據請求資訊,鎖定database,retension policy和shard group,進而得到眾多的shard。

對於寫入操作,根據寫入時的serieskey,鎖定一個shard進行寫入,由於shard存在多副本,需要同時將資料寫入到多個副本。對於查詢,無法透過請求資訊得到serieskey,因此需要查詢所有的shard,針對每個shard選擇一個可用的副本,進行訪問。

經過上邊的處理就獲得shard到物理節點的對映,然後將其反轉為物理節點到shard的對映,返回給proxy,proxy就可以在data叢集的某個節點訪問對應的shard了。

在shard之下的寫入訪問,需要拆解insert語句,組合為儲存鍵值對存入tsm儲存引擎,然後根據組合的serieskey更新倒排索引。

在shard之下的查詢訪問,分析sql語句,查詢倒排索引,獲取其相關的serieskey集合,將其拼接field,形成最終的儲存key,進行資料訪問。然後將眾多資料在data節點上進行shard之上的合併聚合,在proxy上進行data之上的合併聚合。

最終proxy將訪問結果返回給客戶端。

故障處理

策略

上邊提到influxdb針對shard提供副本容錯,當寫入資料傳送到proxy,proxy將資料以無主多寫的形式傳送到所有的shard副本。meta叢集以心跳的形式監控data節點是否線上,在讀取的時候,針對同一shard會在線上的data節點中隨機選擇一個讀取節點進行讀取。

在寫入時如果一個data節點不可用,則會寫入到proxy的一個臨時檔案中,等網路恢復正常會將這些暫存的資料傳送到指定節點。

處理

data叢集擴容

當有全新節點加入data叢集,目前還不支援自動將現有資料進行遷移,不過也做了些努力,為了使當下寫入資料儘快應用到新的節點,在新加入節點的時候,會將當下時間作為當下shard group的結尾時間,然後按照全新的data節點數量新建一個shard group,這樣當下資料量馬上就能均分到各個data節點,而每個shard group相關的meta資訊都儲存在meta叢集裡,因此不會對之前資料的讀取造成干擾。

data節點短暫不可用

如果data節點處於短期不可用狀態,包括短暫的網路故障後自恢復,或者硬體故障後運維人員干預,最終data節點還存有掉線前的資料,那麼就可以以原來的身份加入到data叢集。對於寫入來說,不可用期間proxy會臨時存放此data節點的資料,在data加入叢集時會將這部分資料再次傳送到data節點,保障資料最終一致。

data節點長期不可用

如果data節點由於一些原因,不能或者不需要以原來的身份加入到叢集,需要運維人員手動將原來不可用的data節點下線,那麼這臺機器可用時,可以以全新的data身份加入到叢集中,這等同於叢集的擴容。

 總 結 

QTSDB叢集實現為:寫入時根據serieskey將資料寫到指定shard,而讀取時無法預知serieskey,因此需要查詢每個shard。將整個讀取過程切分為兩個階段:在data節點上進行儲存引擎的讀取以及節點內部多shard的合併聚合,在proxy節點將多個data節點的資料彙總,進行後期的合併聚合,形成最終的結果集返回到客戶端。

QTSDB現有的叢集功能還有不完善的地方,會在之後的使用中不斷完善。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555491/viewspace-2648130/,如需轉載,請註明出處,否則將追究法律責任。

相關文章