1. 程式人生 > >Apache Druid 底層儲存設計(列儲存與全文檢索)

Apache Druid 底層儲存設計(列儲存與全文檢索)

> 導讀:首先你將通過這篇文章瞭解到 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 檔案,其中時間間隔可以通過`granularitySpec`的`segmentGranularity`引數配置。為了使 Druid 在繁重的查詢負載下正常執行,segment 的檔案大小應該在建議的 300mb-700mb 範圍內。如果你的 segment 檔案大於這個範圍,那麼可以考慮修改時間間隔粒度或是對資料分割槽,並調整`partitionSpec`的`targetPartitonSize`引數(這個引數的預設值是 500 萬行)。 ## 資料結構 下面將描述 segment 檔案的內部資料結構,該結構本質上是列式的,每一列資料都放置在單獨的資料結構中。通過分別儲存每個列,Druid 可以通過僅掃描實際需要的那些列來減少查詢延遲。 Druid 共有三種基本列型別:時間戳列,維度列和指標列,如下圖所示: ![](https://magebyte.oss-cn-shenzhen.aliyuncs.com/druid/druid-column-types.png) `timestamp`和`metric`列很簡單:在底層,它們都是由 LZ4 壓縮的 interger 或 float 的陣列。一旦查詢知道需要選擇的行,它就簡單的解壓縮這些行,取出相關的行,然後應用所需的聚合操作。與所有列一樣,如果查詢不需要某一列,則該列的資料會被跳過。 `維度列`就有所不同,因為它們支援過濾和分組操作,所以每個維度都需要下列三種資料結構: 1. 將值(始終被視為字串)對映成整數 ID 的**字典**, 2. 用 1 編碼的**列值列表**,以及 3. 對於列中每一個不同的值,用一個**bitmap**指示哪些行包含該值。 為什麼需要這三種資料結構?`字典`僅將字串對映成整數 id,以便可以緊湊的表示 2 和 3 中的值。3 中的 `bitmap`也稱為*反向索引*,允許快速過濾操作(特別是,點陣圖便於快速進行 AND 和 OR 操作)。最後,*group by*和*TopN*需要 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”主題。在這種情況下,這三個資料結構現在看起來如下: ```javascript 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 組成,例如: ```properties 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 線性增加。 ```properties 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。 ```properties 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 的索引)可確保每個間隔的原子更新。在我們的示例中,在將所有`v2`segment`2015-01-01/2015-01-02`都載入到 Druid 叢集中之前,查詢僅使用`v1`segment。一旦`v2`載入了所有 segment 並可以查詢,所有查詢將忽略`v1`segment 並切換到這些`v2`segment。之後不久,`v1`segment 將被叢集解除安裝。 請注意,跨越多個 segment 間隔的更新僅是每個間隔內具有原子性。在整個更新過程中,它們不是原子的。例如,當你具有以下 segment: ```properties 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`構建完並替換掉`v1`segment 這段時間期內,`v2`segment 將被載入進叢集之中。因此在完全載入`v2`segment 之前,群集中可能同時存在`v1`和`v2`segment。 ```properties 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`和和`v2`segment。 ## 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](https://magebyte.oss-cn-shenzhen.aliyuncs.com/wechat/Snip20200314