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

導讀:首先你將經過這篇文章瞭解到 Apache Druid 底層的數據存儲方式。其次將知道爲何 Apache Druid 兼具數據倉庫,全文檢索和時間序列的特色。最後將學習到一種優雅的底層數據文件結構。javascript

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

瞭解過 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 萬行)。ide

數據結構

下面將描述 segment 文件的內部數據結構,該結構本質上是列式的,每一列數據都放置在單獨的數據結構中。經過分別存儲每一個列,Druid 能夠經過僅掃描實際須要的那些列來減小查詢延遲。oop

Druid 共有三種基本列類型:時間戳列,維度列和指標列,以下圖所示:學習

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

timestampmetric列很簡單:在底層,它們都是由 LZ4 壓縮的 interger 或 float 的數組。一旦查詢知道須要選擇的行,它就簡單的解壓縮這些行,取出相關的行,而後應用所需的聚合操做。與全部列同樣,若是查詢不須要某一列,則該列的數據會被跳過。ui

維度列就有所不一樣,由於它們支持過濾和分組操做,因此每一個維度都須要下列三種數據結構:編碼

  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、文章開頭的問題,你是否已經有答案

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

2、知識擴展

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

*請持續關注,後期將爲你拓展更多知識。對 Druid 感興趣的同窗也能夠回顧我以前的系列文章。

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

MageByte

相關文章
相關標籤/搜索