導讀:首先你將經過這篇文章瞭解到 Apache Druid 底層的數據存儲方式。其次將知道爲何 Apache Druid 兼具數據倉庫,全文檢索和時間序列的特色。最後將學習到一種優雅的底層數據文件結構。javascript
今日格言:優秀的軟件,從模仿開始的原創。java
瞭解過 Apache Druid 或以前看過本系列前期文章的同窗應該都知道 Druid 兼具數據倉庫,全文檢索和時間序列的能力。那麼爲何其能夠具備這些能力,Druid 在實現這些能力時作了怎樣的設計和努力?算法
Druid 的底層數據存儲方式就是其能夠實現這些能力的關鍵。本篇文章將爲你詳細講解 Druid 底層文件 Segment 的組織方式。數組
帶着問題閱讀:數據結構
Druid 將數據存儲在 segment 文件中,segment 文件按時間分區。在基本配置中,將爲每個時間間隔建立一個 segment 文件,其中時間間隔能夠經過granularitySpec
的segmentGranularity
參數配置。爲了使 Druid 在繁重的查詢負載下正常運行,segment 的文件大小應該在建議的 300mb-700mb 範圍內。若是你的 segment 文件大於這個範圍,那麼能夠考慮修改時間間隔粒度或是對數據分區,並調整partitionSpec
的targetPartitonSize
參數(這個參數的默認值是 500 萬行)。ide
下面將描述 segment 文件的內部數據結構,該結構本質上是列式的,每一列數據都放置在單獨的數據結構中。經過分別存儲每一個列,Druid 能夠經過僅掃描實際須要的那些列來減小查詢延遲。oop
Druid 共有三種基本列類型:時間戳列,維度列和指標列,以下圖所示:學習
timestamp
和metric
列很簡單:在底層,它們都是由 LZ4 壓縮的 interger 或 float 的數組。一旦查詢知道須要選擇的行,它就簡單的解壓縮這些行,取出相關的行,而後應用所需的聚合操做。與全部列同樣,若是查詢不須要某一列,則該列的數據會被跳過。ui
維度列
就有所不一樣,由於它們支持過濾和分組操做,因此每一個維度都須要下列三種數據結構:編碼
爲何須要這三種數據結構?字典
僅將字符串映射成整數 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」主題。在這種狀況下,這三個數據結構如今看起來以下:
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 由下面幾個文件組成:
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
。
每列存儲爲兩部分:
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 的結果。
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 的索引)可確保每一個間隔的原子更新。在咱們的示例中,在將全部v2
segment2015-01-01/2015-01-02
都加載到 Druid 集羣中以前,查詢僅使用v1
segment。一旦v2
加載了全部 segment 並能夠查詢,全部查詢將忽略v1
segment 並切換到這些v2
segment。以後不久,v1
segment 將被集羣卸載。
請注意,跨越多個 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
構建完並替換掉v1
segment 這段時間期內,v2
segment 將被加載進集羣之中。所以在徹底加載v2
segment 以前,羣集中可能同時存在v1
和v2
segment。
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 可能具備不一樣的 schema。若是一個 segment 中存在一個字符串列(維),但另外一個 segment 中不存在,則涉及這兩個 segment 的查詢仍然有效。缺乏維的 segment 查詢將表現得好像維只有空值。一樣,若是一個 segment 包含一個數字列(指標),而另外一部分則沒有,則對缺乏該指標的 segment 的查詢一般會「作正確的事」。缺乏該指標的聚合的行爲就好像該指標缺失。
roaring bitmap compressing
壓縮算法。*請持續關注,後期將爲你拓展更多知識。對 Druid 感興趣的同窗也能夠回顧我以前的系列文章。
關注公衆號 MageByte,設置星標點「在看」是咱們創造好文的動力。後臺回覆 「加羣」 進入技術交流羣獲更多技術成長。