Druid的segment

Druid把它的索引存儲在segment的文件中,segment是以時間進行分區的。在基本的設置中,segment文件是根據一定的時間間隔創建的,通過granularitySpec中的 segmentGranularity參數來配置時間間隔。爲了使Druid在繁重的查詢壓力下正常運行,設置segment文件的大小在300MB~700MB之間是非常重要的。如果我們的segment文件大於這個範圍,需要考慮改變時間間隔的粒度或者對數據進行分區並在我們的partitionsSpec裏面調整targetPartitionSize參數(這個參數的比較好的起始點是5百萬行)。詳細信息查看下面的分片部分和 Batch ingestion 的 分區說明部分
segment文件的核心數據結構
我們描述一下segment文件的內部結構,它本質上是列式存儲:每一列的數據都在不同的數據結構中佈局。通過分別存儲每一列,Druid可以通過只掃描查詢實際需要的列來減少查詢延遲。有三種基本的列類型:時間戳列,維度列,指標列,例如:

時間戳和指標列非常簡單:在幕後,每一個都是用LZ4壓縮的整數或浮點值數組。一旦一個查詢知道哪些行需要被查詢,它只需要進行簡單的解壓,拉取相關的行,然後應用期望的聚合操作。如果查詢不需要某一個列,則這個列的數據就會被跳過。
維度列不一樣的原因是它們需要支持過濾和分組操作,因此每個維度都需要以下三種數據結構:
1、需要一個字典來映射值(這些值被當做字符串對待)到整形的ID
2、使用字典進行編碼的列值的一個列表
3、對於列的不同的值,使用bitmap來表明哪一行包含這個值

爲什麼需要這三種數據結構呢?通過字典來把字符串映射到整形,從而能緊湊的表達2和3;3中的bitmap非常方面的應用於 and 和 or 操作。數據結構2中的列表的值用來做分組和topN查詢的。換句話說,基於過濾器的聚合指標查詢不需要涉及到數據結構2中存儲的維度值。

我們使用上面例子中的page列來直觀的感受一下這些數據結構,這個維度列的三種數據結構展示如下:
1、經過字典編碼的列數據:

"Justin Bieber": 0, 
"Ke$ha": 1 
}

2、列數據:
[0,
 0, 
1, 
1]
3、bitmap:列的每個唯一值對應一行
value="Justin Bieber": [1,1,0,0] --第一行和第二行存在,第三行和第三行不存在這個值。
value="Ke$ha": [0,0,1,1]

需要注意的是,bitmap與前兩個數據結構不同,前兩個數據結構在數據大小上呈線性增長(在最壞的情況下),而bitmap的大小是數據的大小乘以列的基數。不過呢,我們可以採用壓縮算法,我們知道對於每一行的列數據,只有一個非零項的bitmap。這意味着高基數列將具有非常稀疏的,高度可壓縮的位圖。Druid使用特別適合位圖的壓縮算法( roaring bitmap壓縮)來利用這一點。

多值列
如果一個DataSource使用了多值列,那麼segment 文件的數據結構看起來會有一點不同。讓我們想象一下上面的例子,如果第二行同時擁有'Ke$ha' 和 'Justin Bieber' 值,在這種情況下,這三種數據結構看起來就像下面展示的:
1、經過字典編碼的列數據:
{ "Justin Bieber": 0, "Ke$ha": 1 }

2、列數據:
[0,
 [0,1], <--多值列在行裏面存儲多個值的數組
1, 
1]

3、bitmap 每一個代表唯一值
value="Justin Bieber": [1,1,0,0] 
value="Ke$ha": [0,1,1,1] ——多值列有多個非零項

注意:主要的不一樣的地方就是第二行的列數據 和Ke$ha的bitmap。如果行裏面存在多餘一個值的列,它將會以數組的形式保存在列數據裏面,另外,行裏面的列數據存在N個值將會有N個非零項在bitmap裏面。

SQL兼容的null處理
默認情況下,druid的字符串維度列中''和null是可以互換使用的,數值列和指標列不能出現null,如果出現會強制轉換成0.然而druid提供了SQL兼容的null處理模式,它必須通過配置項druid.generic.useDefaultValueForNull在系統級別進行開啓。當它設置爲false的時候,druid在進行攝入的時候創建的segment會對字符列的''和null進行區分,同樣數字列可以使用null而不是0.
在這種模式下,字符串維度列不包含額外的列結構,而只是爲null保留額外的字典條目。數值型列需要在segment添加額外的bitmap來表示存在null的行。這會稍微增加segment的大小。SQL兼容的null處理也會在查詢的時候帶來性能損耗,因爲需要檢查null的bitmap。當然這個性能損耗只會發生在列實際包含null的值的時候。

命名約定
segment的標識由segment的DataSource、間隔的開始時間(iso 8601格式)、間隔的結束時間(ISO 8601格式)和一個版本號組成。如果數據需要根據時間範圍分片的話,segment的標識還會包含一個分片數字。
segment的標識類似這樣:datasource_intervalStart_intervalEnd_version_partitionNum

segment的組件
在幕後,一個segment有好幾個文件組成,清單如下:
1、version.bin
使用4字節的整數來表示當前segment的版本,比如v9 segment,版本就是0x0, 0x0, 0x0, 0x9
2、meta.smoosh
元數據文件,存儲了其他smoosh文件的內容(文件名和偏移量)
3、xxxxx.smoosh
一系列連續的二進制文件
這些smoosh文件表示多個文件順序的放在一起以最小化文件描述符(用來打開存儲的數據)的數量。這些文件最大是2GB(匹配java的bytebuffer的內存限制)。對於每個列,smoosh文件都是使用單獨的文件進行數據的存儲,以及一個index.drd文件,其中包含有關該段的額外元數據。
還有一個稱爲“__time間”的特殊列,它引用段的時間列。隨着代碼的發展,這可能會變得越來越不特別,但現在它就像我媽媽一直告訴我的那樣特別。
在代碼庫中,segment具有內部格式版本。當前段格式版本是v9。

列的格式
每個列都會存儲兩部分內容:
1、json的列描述
2、剩下的就是列的二進制數據
列描述本質上是一個對象,它允許我們使用json多樣化的反序列化來最少化的代碼影響去添加新的和有趣的序列化方法。它包含一些列的元數據(它的類型,是不是多值……)和可以反序列化剩餘的二進制數據的序列化和反序列化的邏輯

創建segment的時候分片
分片
同一個DataSource在相同的時間間隔可能會存在多個segment。這些segment在時間間隔裏形成了一個塊。依據shardSpec的類型來進行數據的切分,只有當一個塊完成時,Druid才能進行查詢。這就是說,假如一個塊包含三個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的數據,所有這三個segment都必須加載完成。

例外的是線性分片聲明,線性分片聲明不需要強制塊必須完成,並且在分片沒有加載到系統中時也能完成查詢。例如:如果我們的實時攝入創建了三個線性分片聲明的segment,並且只有兩個segment加載到系統裏面,查詢的時候會返回這兩個segment的數據。

模式修改
替換segment
druid使用DataSource,時間間隔,版本和分區號唯一標識segment,分區號只有在一個segment 標識中可見是當某些時間粒度的時候存在多個segment。例如:如果我們有一個按小時的segment,但是我們的數據太多導致一個segment沒法存儲,我們就可以在相同的時間創建多個segment。這些segment擁有相同的DataSource,時間間隔和版本,但是會線性的增加分區好。
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
在上面的示例中,DataSource= foo,時間間隔=2015-01-01/2015-01-02,version=v1,分區號=0.如果在後續的某一個時間點,我們使用新的模式從新索引了數據,則新創建的segment將會使用一個高的版本
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要麼基於索引任務)保證基於時間間隔的原子性更新。在我們的例子中,除非有所有的2015-01-01/2015-01-02的v2 segment全部加載到druid的集羣中,否則查詢就使用v1的segment。一旦所有的v2segment加載完成並可查詢,所有的查詢都會忽略v1segment並切換到v2 segment。稍後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
v2segment將在構建後立即加載到集羣中,並在segment重疊的時間段內替換v1段。在完全加載v2段之前,集羣可能混合了v1和v2段。
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會存在不同的模式
同一數據源的Druid段可能有不同的模式。如果一個字符串列(維度)存在於一個segment中而不是另一個segment中,則涉及這兩個segment的查詢仍然有效。對缺少維度的segment的查詢將表現爲該維度只有空值。類似地,如果一個段有一個數值列(metric),而另一個沒有,那麼查詢缺少metric的段通常會看似在“做正確的事情”。在缺少的度量上的聚合的行爲類似於該度量丟失。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章