Apache Druid底層儲存設計

MageByte發表於2020-03-31

導讀:首先你將通過這篇文章瞭解到 Apache Druid 底層的資料儲存方式。其次將知道為什麼 Apache Druid 兼具資料倉儲,全文檢索和時間序列的特點。最後將學習到一種優雅的底層資料檔案結構。

今日格言:優秀的軟體,從模仿開始的原創。

瞭解過 Apache Druid 或之前看過本系列前期文章的同學應該都知道 Druid 兼具資料倉儲,全文檢索和時間序列的能力。那麼為什麼其可以具有這些能力,Druid 在實現這些能力時做了怎樣的設計和努力?

Druid 的底層資料儲存方式就是其可以實現這些能力的關鍵。本篇文章將為你詳細講解 Druid 底層檔案 Segment 的組織方式。

帶著問題閱讀:

  1. Druid 的資料模型是怎樣的?
  2. Druid 維度列的三種儲存資料結構如何?各自的作用?
  3. Segment 檔案標識組成部分?
  4. Segment 如何分片儲存資料?
  5. Segment 新老版本資料怎麼生效?

Segment 檔案

Druid 將資料儲存在 segment 檔案中,segment 檔案按時間分割槽。在基本配置中,將為每一個時間間隔建立一個 segment 檔案,其中時間間隔可以通過granularitySpecsegmentGranularity引數配置。為了使 Druid 在繁重的查詢負載下正常執行,segment 的檔案大小應該在建議的 300mb-700mb 範圍內。如果你的 segment 檔案大於這個範圍,那麼可以考慮修改時間間隔粒度或是對資料分割槽,並調整partitionSpectargetPartitonSize引數(這個引數的預設值是 500 萬行)。

資料結構

下面將描述 segment 檔案的內部資料結構,該結構本質上是列式的,每一列資料都放置在單獨的資料結構中。通過分別儲存每個列,Druid 可以通過僅掃描實際需要的那些列來減少查詢延遲。

Druid 共有三種基本列型別:時間戳列,維度列和指標列,如下圖所示:

timestampmetric列很簡單:在底層,它們都是由 LZ4 壓縮的 interger 或 float 的陣列。一旦查詢知道需要選擇的行,它就簡單的解壓縮這些行,取出相關的行,然後應用所需的聚合操作。與所有列一樣,如果查詢不需要某一列,則該列的資料會被跳過。

維度列就有所不同,因為它們支援過濾和分組操作,所以每個維度都需要下列三種資料結構:

  1. 將值(始終被視為字串)對映成整數 ID 的字典
  2. 用 1 編碼的列值列表,以及
  3. 對於列中每一個不同的值,用一個bitmap指示哪些行包含該值。

為什麼需要這三種資料結構?字典僅將字串對映成整數 id,以便可以緊湊的表示 2 和 3 中的值。3 中的

bitmap也稱為反向索引,允許快速過濾操作(特別是,點陣圖便於快速進行 AND 和 OR 操作)。最後,group byTopN需要 2 中的值列表,換句話說,僅基於過濾器彙總的查詢無需查詢儲存在其中的維度值列表

為了具體瞭解這些資料結構,考慮上面示例中的“page”列,下圖說明了表示該維度的三個資料結構。

1: 編碼列值的字典
{
"Justin Bieber": 0,
"Ke$ha": 1
}

2: 列資料
[0,0,1,1]

3: Bitmaps - 每個列唯一值對應一個
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,0,1,1]
複製程式碼

注意bitmap和前兩種資料結構不同:前兩種在資料大小上呈線性增長(在最壞的情況下),而 bitmap 部分的大小則是資料大小和列基數的乘積。壓縮將在這裡為我們提供幫助,因為我們知道,對於“列資料”中的每一行,只有一個點陣圖具有非零的條目。這意味著高基數列將具有極為稀疏的可壓縮高度點陣圖。Druid 使用特別適合點陣圖的壓縮演算法來壓縮 bitmap,如roaring bitmap compressing(有興趣的同學可以深入去了解一下)。

如果資料來源使用多值列,則 segment 檔案中的資料結構看起來會有所不同。假設在上面的示例中,第二行同時標記了“ Ke $ ha” 和 “ Justin Bieber”主題。在這種情況下,這三個資料結構現在看起來如下:

1: 編碼列值的欄位
{
"Justin Bieber": 0,
"Ke$ha": 1
}

2: 列資料
[0,
[0,1], <--Row value of multi-value column can have array of values
1,
1]

3: Bitmaps - one for each unique value
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,1,1,1]
^
|
|
Multi-value column has multiple non-zero entries
複製程式碼

注意列資料和Ke$ha點陣圖中第二行的更改,如果一行的一個列有多個值,則其在“列資料“中的輸入是一組值。此外,在”列資料“中具有 n 個值的行在點陣圖中將具有 n 個非零值條目。

命名約定

segment 標識通常由資料來源間隔開始時間(ISO 8601 format),間隔結束時間(ISO 8601 format)和版本號構成。如果資料因為超出時間範圍被分片,則 segment 識別符號還將包含分割槽號。如下: segment identifier=datasource_intervalStart_intervalEnd_version_partitionNum

Segment 檔案組成

在底層,一個 segment 由下面幾個檔案組成:

  • version.bin 4 個位元組,以整數表示當前 segment 的版本。例如,對於 v9 segment,版本為 0x0, 0x0, 0x0, 0x9。

  • meta.smoosh 儲存關於其他 smooth 檔案的後設資料(檔名和偏移量)。

  • XXXXX.smooth

    這些檔案中儲存著一系列二進位制資料。

    這些smoosh檔案代表一起被“ smooshed”的多個檔案,分成多個檔案可以減少必須開啟的檔案描述符的數量。它們的大小最大 2GB(以匹配 Java 中記憶體對映的 ByteBuffer 的限制)。這些smoosh檔案包含資料中每個列的單獨檔案,以及index.drd帶有有關該 segment 的額外後設資料的檔案。

    還有一個特殊的列,稱為__time,是該 segment 的時間列。

在程式碼庫中,segment 具有內部格式版本。當前的 segment 格式版本為v9

列格式

每列儲存為兩部分:

  1. Jackson 序列化的 ColumnDescriptor
  2. 該列的其餘二進位制檔案

ColumnDescriptor 本質上是一個物件。它由一些有關該列的後設資料組成(它是什麼型別,它是否是多值的,等等),然後是可以反序列化其餘二進位制數的序列化/反序列化 list。

分片資料

分片

對於同一資料來源,在相同的時間間隔內可能存在多個 segment。這些 segment 形成一個block間隔。根據shardSpec來配置分片資料,僅當block完成時,Druid 查詢才可能完成。也就是說,如果一個塊由 3 個 segment 組成,例如:

sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_0
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_1
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_2
複製程式碼

在對時間間隔的查詢2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z完成之前,必須裝入所有 3 個 segment。

該規則的例外是使用線性分片規範。線性分片規範不會強制“完整性”,即使分片未載入到系統中,查詢也可以完成。例如,如果你的實時攝取建立了 3 個使用線性分片規範進行分片的 segment,並且系統中僅載入了兩個 segment,則查詢將僅返回這 2 個 segment 的結果。

模式變更

替換 segment

Druid 使用 datasource,interval,version 和 partition number 唯一地標識 segment。如果在一段時間內建立了多個 segment,則分割槽號僅在 segment ID 中可見。例如,如果你有一個一小時時間範圍的 segment,但是一個小時內的資料量超過單個 segment 所能容納的時間,則可以在同一小時內建立多個 segment。這些 segment 將共享相同的 datasource,interval 和 version,但 partition number 線性增加。

foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-01/2015-01-02_v1_1
foo_2015-01-01/2015-01-02_v1_2
複製程式碼

在上面的示例 segment 中,dataSource = foo,interval = 2015-01-01 / 2015-01-02,version = v1,partitionNum =0。如果在以後的某個時間點,你使用新的模式重新索引資料,新建立的 segment 將具有更高的版本 ID。

foo_2015-01-01/2015-01-02_v2_0
foo_2015-01-01/2015-01-02_v2_1
foo_2015-01-01/2015-01-02_v2_2
複製程式碼

Druid 批量索引(基於 Hadoop 或基於 IndexTask 的索引)可確保每個間隔的原子更新。在我們的示例中,在將所有v2segment2015-01-01/2015-01-02都載入到 Druid 叢集中之前,查詢僅使用v1segment。一旦v2載入了所有 segment 並可以查詢,所有查詢將忽略v1segment 並切換到這些v2segment。之後不久,v1segment 將被叢集解除安裝。

請注意,跨越多個 segment 間隔的更新僅是每個間隔內具有原子性。在整個更新過程中,它們不是原子的。例如,當你具有以下 segment:

foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-02/2015-01-03_v1_1
foo_2015-01-03/2015-01-04_v1_2
複製程式碼

v2構建完並替換掉v1segment 這段時間期內,v2segment 將被載入進叢集之中。因此在完全載入v2segment 之前,群集中可能同時存在v1v2segment。

foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-02/2015-01-03_v2_1
foo_2015-01-03/2015-01-04_v1_2
複製程式碼

在這種情況下,查詢可能會同時出現v1和和v2segment。

segment 多個不同模式

同一資料來源的 segment 可能具有不同的 schema。如果一個 segment 中存在一個字串列(維),但另一個 segment 中不存在,則涉及這兩個 segment 的查詢仍然有效。缺少維的 segment 查詢將表現得好像維只有空值。同樣,如果一個 segment 包含一個數字列(指標),而另一部分則沒有,則對缺少該指標的 segment 的查詢通常會“做正確的事”。缺少該指標的聚合的行為就好像該指標缺失。

最後

一、文章開頭的問題,你是否已經有答案

  1. Druid 的資料模型是怎樣的?(時間戳列,維度列和指標列)
  2. Druid 維度列的三種儲存資料結構如何?各自的作用?(編碼對映表、列值列表、Bitmap)
  3. Segment 檔案標識組成部分?(datasource,interval,version 和 partition numbe)
  4. Segment 如何分片儲存資料?
  5. Segment 新老版本資料怎麼生效?

二、知識擴充套件

  1. 什麼是列儲存?列儲存和行儲存的區別是什麼?
  2. 你瞭解 Bitmap 資料結構嗎?
  3. 深入瞭解roaring bitmap compressing壓縮演算法。
  4. Druid 是如何定位到一條資料的?詳細流程是怎樣的?

*請持續關注,後期將為你擴充更多知識。對 Druid 感興趣的同學也可以回顧我之前的系列文章。

關注公眾號 MageByte,設定星標點「在看」是我們創造好文的動力。後臺回覆 “加群” 進入技術交流群獲更多技術成長。

MageByte
MageByte

相關文章