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 文件,其中時間間隔可以通過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 是如何定位到一條數據的?詳細流程是怎樣的?

系列推薦

Mysql:小主鍵,大問題
Mysql大數據量問題與解決
你應該知道一些其他存儲——列式存儲
時間序列數據庫(TSDB)初識與選擇(InfluxDB、OpenTSDB、Druid、Elasticsearch對比)
十分鍾了解Apache Druid(集數據倉庫、時間序列、全文檢索於一體的存儲方案)
Apache Druid 底層存儲設計(列存儲與全文檢索)
Apache Druid 的集群設計與工作流程

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

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

MageByte


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM