Druid高效架構

我們知道Druid能夠同時提供對大數據集的實時攝入和高效復雜查詢的性能,主要原因就是它獨到的架構設計和基于Datasource與Segment的數據存儲結構。接下來我們會分別從數據存儲和系統節點架構兩方面來深入了解一下Druid的架構。

數據存儲

Druid將數據組織成Read-Optimized的結構,而這也是Druid能夠支持交互式查詢的關鍵。Druid中的數據存儲在被稱為datasource中,類似RDMS中的table。每個datasource按照時間劃分,如果你有需求也可以進一步按其它屬性劃分。每個時間范圍稱為一個chunk(比如你按天分區,則一個chunk為一天)。在chunk中數據由被分為一個或多個segment(segment是數據實際存儲結構,Datasource、Chunk只是一個邏輯概念),每個segment都是一個單獨的文件,通常包含幾百萬行數據,這些segment是按照時間組織成的chunk,所以在按照時間查詢數據時,效率非常高。

image

數據分區

任何分布式存儲/計算系統,都需要對數據進行合理的分區,從而實現存儲和計算的均衡,以及數據并行化。而Druid本身處理的是事件數據,每條數據都會帶有一個時間戳,所以很自然的就可以使用時間進行分區。比如上圖,我們指定了分區粒度為為天,那么每天的數據都會被單獨存儲和查詢(一個分區下有多個Segment的原因往下看)。
使用時間分區我們很容易會想到一個問題,就是很可能每個時間段的數據量是不均衡的(想一想我們的業務場景),而Duid為了解決這種問題,提供了“二級分區”,每一個二級分區稱為一個Shard(這才是物理分區)。通過設置每個Shard的所能存儲的目標值和Shard策略,來完成shard的分區。Druid目前支持兩種Shard策略:Hash(基于維值的Hash)和Range(基于某個維度的取值范圍)。上圖中,2000-01-01和2000-01-03的每個分區都是一個Shard,由于2000-01-02的數據量比較多,所以有兩個Shard。

Segment

Shard經過持久化之后就稱為了Segment,Segment是數據存儲、復制、均衡(Historical的負載均衡)和計算的基本單元了。Segment具有不可變性,一個Segment一旦創建完成后(MiddleManager節點發布后)就無法被修改,只能通過生成一個新的Segment來代替舊版本的Segment。

Segment內部存儲結構

接下來我們可以看下Segment文件的內部存儲結構。因為Druid采用列式存儲,所以每列數據都是在獨立的結構中存儲(并不是獨立的文件,是獨立的數據結構,因為所有列都會存儲在一個文件中)。Segment中的數據類型主要分為三種:時間戳、維度列和指標列。

image

對于時間戳列和指標列,實際存儲是一個數組,Druid采用LZ4壓縮每列的整數或浮點數。當收到查詢請求后,會拉出所需的行數據(對于不需要的列不會拉出來),并且對其進行解壓縮。解壓縮完之后,在應用具體的聚合函數。
對于維度列不會像指標列和時間戳這么簡單,因為它需要支持filter和group by,所以Druid使用了字典編碼(Dictionary Encoding)和位圖索引(Bitmap Index)來存儲每個維度列。每個維度列需要三個數據結構:

  1. 需要一個字典數據結構,將維值(維度列值都會被認為是字符串類型)映射成一個整數ID。
  2. 使用上面的字典編碼,將該列所有維值放在一個列表中。
  3. 對于列中不同的值,使用bitmap數據結構標識哪些行包含這些值。

Druid針對維度列之所以使用這三個數據結構,是因為:

  1. 使用字典將字符串映射成整數ID,可以緊湊的表示結構2和結構3中的值。
  2. 使用Bitmap位圖索引可以執行快速過濾操作(找到符合條件的行號,以減少讀取的數據量),因為Bitmap可以快速執行AND和OR操作。
  3. 對于group by和TopN操作需要使用結構2中的列值列表。

我們以上面"Page"維度列為例,可以具體看下Druid是如何使用這三種數據結構存儲維度列:

1. 使用字典將列值映射為整數
{
"Justin Bieher":0,
"ke$ha":1
}
2. 使用1中的編碼,將列值放到一個列表中
[0,0,1,1]
3. 使用bitmap來標識不同列值
value = 0: [1,1,0,0] //1代表該行含有該值,0標識不含有
value = 1: [0,0,1,1]

下圖是以advertiser列為例,描述了advertiser列的實際存儲結構:

image

前兩種存儲結構在最壞情況下會根據數據量增長而成線性增長(列數據中的每行都不相同),而第三種由于使用Bitmap存儲(本身是一個稀疏矩陣),所以對它進行壓縮,可以得到非常客觀的壓縮比。Druid而且運用了Roaring Bitmap(http://roaringbitmap.org/)能夠對壓縮后的位圖直接進行布爾運算,可以大大提高查詢效率和存儲效率(不需要解壓縮)。

Segment命名

高效的數據查詢,不僅僅體現在文件內容的存儲結構上,還有一點很重要,就是文件的命名上。試想一下,如果一個Datasource下有幾百萬個Segment文件,我們又如何快速找出我們所需要的文件呢?答案就是通過文件名稱快速索引查找。
Segment的命名包含四部分:數據源(Datasource)、時間間隔(包含開始時間和結束時間兩部分)、版本號和分區(Segment有分片的情況下才會有)。

test-datasource_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T16:00:00.000Z_1
數據源名稱_開始時間_結束時間_版本號_分區

分片號是從0開始,如果分區號為0,則可以省略:test-datasource_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T16:00:00.000Z
還需要注意如果一個時間間隔segment由多個分片組成,則在查詢該segment的時候,需要等到所有分片都被加載完成后,才能夠查詢(除非使用線性分片規范(linear shard spec),允許在未加載完成時查詢)。

字段 是否必須 描述
datasource segment所在的Datasource
開始時間 該Segment所存儲最早的數據,時間格式是ISO 8601。開始時間和結束時間是通過segmentGranularity設置的時間間隔
結束時間 該segment所存儲最晚的數據,時間格式是ISO 8601
版本號 因為Druid支持批量覆蓋操作,當批量攝入與之前相同數據源、相同時間間隔數據時,數據就會被覆蓋,這時候版本號就會被更新。Druid系統的其它部分感知到這個信號后,就會把就舊數據刪除,使用新版本的數據(這個切換很快)。版本號也是是用的ISO 8601時間戳,但是這個時間戳代表首次啟動的時間
分區號 segment如果采用分區,才會有該標識

Segment物理存儲實例

下面我們以一個實例來看下Segment到底以什么形式存儲的,我們以本地導入方式將下面數據導入到Druid中。

{"time": "2018-11-01T00:47:29.913Z","city": "beijing","sex": "man","gmv": 20000}
{"time": "2018-11-01T00:47:33.004Z","city": "beijing","sex": "woman","gmv": 50000}
{"time": "2018-11-01T00:50:33.004Z","city": "shanghai","sex": "man","gmv": 10000}

我們以單機形式運行Druid,這樣Druid生成的Segment文件都在${DRUID_HOME}/var/druid/segments 目錄下。

image

segment通過datasource_beginTime_endTime_version_shard用于唯一標識,在實際存儲中是以目錄的形式表現的。

image

可以看到Segment中包含了Segment描述文件(descriptor.json)和壓縮后的索引數據文件(index.zip),我們主要看的也是index.zip這個文件,對其進行解壓縮。

image

首先看下factory.json這個文件,這個文件并不是segment具體存儲段數據的文件。因為Druid通過使用MMap(一種內存映射文件的方式)的方式訪問Segment文件,通過查看這個文件內容來看,貌似是用于MMap讀取文件所使用的(不太了解MMap)?

#factory.json文件內容
{"type":"mMapSegmentFactory"}

Druid實際存儲Segment數據文件是:version.bin、meta.smoosh和xxxxx.smoosh這三個文件,下面分別看下這三個文件的內容。
version.bin是一個存儲了4個字節的二進制文件,它是Segment內部版本號(隨著Druid發展,Segment的格式也在發展),目前是V9,以Sublime打開該文件可以看到:

0000 0009 

meta.smoosh里面存儲了關于其它smoosh文件(xxxxx.smoosh)的元數據,里面記錄了每一列對應文件和在文件的偏移量。除了列信息外,smoosh文件還包含了index.drd和metadata.drd,這部分是關于Segment的一些額外元數據信息。

#版本號,該文件所能存儲的最大值(2G),smooth文件數
v1,2147483647,1
# 列名,文件名,起始偏移量,結束偏移量
__time,0,0,154
city,0,306,577
gmv,0,154,306
index.drd,0,841,956
metadata.drd,0,956,1175
sex,0,577,841

再看00000.smoosh文件前,我們先想一下為什么這個文件被命名為這種樣式?因為Druid為了最小化減少打開文件的句柄數,它會將一個Segment的所有列數據都存儲在一個smoosh文件中,也就是xxxxx.smoosh這個文件。但是由于Druid使用MMap來讀取Segment文件,而MMap需要保證每個文件大小不能超過2G(Java中的MMapByteBuffer限制),所以當一個smoosh文件大于2G時,Druid會將新數據寫入到下一個smoosh文件中。這也就是為什么這些文件命名是這樣的,這里也對應上了meta文件中為什么還要標識列所在的文件名。
通過meta.smoosh的偏移量也能看出,00000.smoosh文件中數據是按列進行存儲的,從上到下分別存儲的是時間列、指標列、維度列。對于每列主要包會含兩部分信息:ColumnDescriptor和binary數據。columnDescriptor是一個使用Jackson序列化的對象,它包含了該列的一些元數據信息,比如數據類型、是否是多值等。而binary則是根據不同數據類型進行壓縮存儲的二進制數據。

^@^@^@d{"valueType":"LONG","hasMultipleValues":false,"parts":[{"type":"long","byteOrder":"LITTLE_ENDIAN"}]}^B^@^@^@^C^@^@ ^@^A^A^@^@^@^@"^@^@^@^A^@^@^@^Z^@^@^@^@¢yL?ìf^A^@^@<8c>X^H^@<80>?^Wàìf^A^@^@^@^@^@d{"valueType":"LONG","hasMultipleValues":false,"parts":[{"type":"long","byteOrder":"LITTLE_ENDIAN"}]}^B^@^@^@^C^@^@ ^@^A^A^@^@^@^@ ^@^@^@^A^@^@^@^X^@^@^@^@1 N^@^A^@"P?^H^@<80>^P'^@^@^@^@^@^@^@^@^@<9a>{"valueType":"STRING","hasMultipleValues":false,"parts":[{"type":"stringDictionary","bitmapSerdeFactory":{"type":"concise"},"byteOrder":"LITTLE_ENDIAN"}]}^B^@^@^@^@^A^A^@^@^@#^@^@^@^B^@^@^@^K^@^@^@^W^@^@^@^@beijing^@^@^@^@shanghai^B^A^@^@^@^C^@^A^@^@^A^A^@^@^@^@^P^@^@^@^A^@^@^@^H^@^@^@^@0^@^@^A^A^@^@^@^@^\^@^@^@^B^@^@^@^H^@^@^@^P^@^@^@^@<80>^@^@^C^@^@^@^@<80>^@^@^D^@^@^@<9a>{"valueType":"STRING","hasMultipleValues":false,"parts":[{"type":"stringDictionary","bitmapSerdeFactory":{"type":"concise"},"byteOrder":"LITTLE_ENDIAN"}]}^B^@^@^@^@^A^A^@^@^@^\^@^@^@^B^@^@^@^G^@^@^@^P^@^@^@^@man^@^@^@^@woman^B^A^@^@^@^C^@^A^@^@^A^A^@^@^@^@^P^@^@^@^A^@^@^@^H^@^@^@^@0^@^A^@^A^@^@^@^@^\^@^@^@^B^@^@^@^H^@^@^@^P^@^@^@^@<80>^@^@^E^@^@^@^@<80>^@^@^B^A^@^@^@^@&^@^@^@^C^@^@^@^G^@^@^@^O^@^@^@^V^@^@^@^@gmv^@^@^@^@city^@^@^@^@sex^A^A^@^@^@^[^@^@^@^B^@^@^@^H^@^@^@^O^@^@^@^@city^@^@^@^@sex^@^@^Afì<91>D^@^@^@^Af??,^@^@^@^@^R{"type":"concise"}{"container":{},"aggregators":[{"type":"longSum","name":"gmv","fieldName":"gmv","expression":null}],"timestampSpec":{"column":"time","format":"auto","missingValue":null},"queryGranularity":{"type":"none"},"rollup":true}

smooth文件中的binary數據經過LZ4或Bitmap壓縮,所以無法看到數據原始內容。

在smooth文件最后還包含了兩部分數據,分別是index.drd和metadata.drd。其中index.drd中包含了Segment中包含哪些度量、維度、時間范圍、以及使用哪種bitmap。metadata.drd中存儲了指標聚合函數、查詢粒度、時間戳配置等(上面內容的最后部分)。
下圖是物理存儲結構圖,存儲未壓縮和編碼的數據就是最右邊的內容。

image

Segment創建

Segment都是在MiddleManager節點中創建的,并且處在MiddleManager中的Segment在狀態上都是可變的并且未提交的(提交到DeepStorage之后,數據就不可改變)。
Segment從在MiddleManager中創建到傳播到Historical中,會經歷以下幾個步驟:

  1. MiddleManager中創建Segment文件,并將其發布到Deep Storage。
  2. Segment相關的元數據信息被存儲到MetaStore中。
  3. Coordinator進程根據MetaStore中得知Segment相關的元數據信息后,根據規則的設置分配給復合條件的Historical節點。
  4. Historical節點得到Coordinator指令后,自動從DeepStorage中拉取Segment數據文件,并通過Zookeeper向集群聲明負責提供該Segment數據相關的查詢服務。
  5. MiddleManager在得知Historical負責該Segment后,會丟棄該Segment文件,并向集群聲明不在負責該Segment相關的查詢。

如何配置分區

可以通過granularitySpec中的segmentGranularity設置segment的時間間隔(http://druid.io/docs/latest/ingestion/ingestion-spec.html#granularityspec)。為了保證Druid的查詢效率,每個Segment文件的大小建議在300MB~700MB之間。如果超過這個范圍,可以修改時間間隔或者使用分區來進行優化(配置partitioningSpec中的targetPartitionSize,官方建議設置500萬行以上;http://druid.io/docs/latest/ingestion/hadoop.html#partitioning-specification)。

系統架構詳解

我們知道Druid節點類型有五種:Overload、MiddleManager、Coordinator、Historical和Broker。

image

Overload和MiddleManager主要負責數據攝入(對于沒有發布的Segment,MiddleManager也提供查詢服務);Coordinator和Historical主要負責歷史數據的查詢;Broker節點主要負責接收Client查詢請求,拆分子查詢給MiddleManager和Historical節點,然后合并查詢結果返回給Client。其中Overload是MiddleManager的master節點,Coordinator是Historical的master節點。

索引服務

Druid提供一組支持索引服務(Indexing Service)的組件,也就是Overload和MiddleManager節點。索引服務是一種高可用的分布式服務,用于運行跟索引相關的任務,索引服務是數據攝入創建和銷毀Segment的主要方式(還有一種是采用實時節點的方式,但是現在已經廢棄了)。索引服務支持以pull或push的方式攝入外部數據。
索引服務采用的是主從架構,Overload為主節點,MiddleManager是從節點。索引服務架構圖如下圖所示:

image

索引服務由三部分組件組成:用于執行任務的Peon(勞工)組件、用于管理Peon的MiddleManager組件和分配任務給MiddleManager的Overload組件。MiddleManager和Overload組件可以部署在相同節點也可以跨節點部署,但是Peon和MiddleManager是部署在同一個節點上的。
索引服務架構和Yarn的架構很像:

  • Overlaod節點相當于Yarn的ResourceManager,負責集群資源管理和任務分配。
  • MiddleManager節點相當于Yarn的NodeManager,負責接受任務和管理本節點的資源。
  • Peon節點相當于Yarn的Container,執行節點上具體的任務。

Overload節點

Overload作為索引服務的主節點,對外負責接受索引任務,對內負責將任務分解并下發給MiddleManager。Overload有兩種運行模式:

  • 本地模式(Local Mode):默認模式。本地模式下的Overload不僅負責任務協調工作,還會負責啟動一些peon來完成具體的任務。
  • 遠程模式(Remote Mode):該模式下,Overload和MiddleManager運行在不同的節點上,它僅負責任務的協調工作,不負責完成具體的任務。

Overload提供了一個UI客戶端,可以用于查看任務、運行任務和終止任務等。

http://<OVERLORD_IP>:<port>/console.html

Overload提供了RESETful的訪問形式,所以客戶端可以通過HTTP POST形式向請求節點提交任務。

http://<OVERLORD_IP>:<port>/druid/indexer/v1/task //提交任務
http://<OVERLORD_IP>:<port>/druid/indexer/v1/task/{task_id}/shutdown //殺死任務

MiddleManager節點

MiddleManager是執行任務的工作節點,MiddleManager會將任務單獨發給每個單獨JVM運行的Peon(因為要把資源和日志進行隔離),每個Peon一次只能運行一個任務。

Peon節點

Peon在單個JVM中運行單個任務,MiddleManager負責為任務創建Peon。

Coordinator節點

Coordinator是Historical的mater節點,它主要負責管理和分發Segment。具體工作就是:告知Historical加載或刪除Segment、管理Segment副本以及負載Segment在Historical上的均衡。
Coordinator是定期運行的,并且運行間隔可以通過配置參數配置。每次Coordinator運行都會通過Zookeeper獲取當前集群狀態,通過評估集群狀態來采取適當的操作(比如均衡負載Segment)。Coordinator會連接數據庫(MetaStore),數據庫中存儲了Segment信息和規則(Rule)。Segment表中列出了需要加載到集群中的所有Segment,Coordinator每次運行都會從Segment表來拉取Segment列表并與當前集群的Segment對比,如果發現數據庫中不存在的Segment,但是在集群中還有,就會把它從集群刪掉;規則表定義了如何處理Segment,規則的作用就是我們可以通過配置一組規則,來操作集群加載Segment或刪除Segment。關于如何配置規則,可以查看:http://druid.io/docs/latest/operations/rule-configuration.html

Historical節點加載Segment前,會進行容量排序,哪個Historical節點的Segment最少,則它就具有最高的加載權。Coordinator不會直接Historical節點通信,而是將Segment信息放到一個隊列中,Historical節點去隊列取Segment描述信息,并且加載該Segment到本節點。
Coordinator提供了一UI界面,用于顯示集群信息和規則配置:

http://<COORDINATOR_IP>:<COORDINATOR_PORT>

Historical節點

Historical節點負責管理歷史Segment,Historical節點通過Zookeeper監聽指定的路徑來發現是否有新的Segment需要加載(Coordinator通過分配算法指定具體的Historical)。
上面通過Coordinator知道,當有新的Segment需要加載的時候,Coordinator會將其放到一個隊列中。當Historical節點收到有新的Segment時候,就會檢測本地cache和磁盤,查看是否有該Segment信息。如果沒有Historical節點會從Zookeeper中拉取該Segment相關的信息,然后進行下載。

image

Broker

Broker節點是負責轉發Client查詢請求的,Broker通過zookeeper能夠知道哪個Segment在哪些節點上,Broker會將查詢轉發給相應節點。所有節點返回數據后,Broker會將所有節點的數據進行合并,然后返回給Client。
Broker會有一個LRU(高速緩存失效策略),來緩存每Segment的結果。這個緩存可以是本地緩存,也可以借助外部緩存系統(比如memcached),第三方緩存可以在所有broker中共享Segment結果。當Borker接收到查詢請求后,會首先查看本地是否有對應的查詢數據,對于不存在的Segment數據,會將請求轉發給Historical節點。

轉載http://www.lxweimin.com/p/7a26d9153455

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 我們知道Druid能夠同時提供對大數據集的實時攝入和高效復雜查詢的性能,主要原因就是它獨到的架構設計和基于Data...
    零度沸騰_yjz閱讀 21,557評論 3 17
  • Druid io總體設計 1.Druid模塊架構 1.1 Druid簡介 最新版本的Druid采用了位圖索引、字典...
    小武大講堂閱讀 1,848評論 0 2
  • Druid.io(以下簡稱Druid)是面向海量數據的、用于實時查詢與分析的OLAP存儲系統。Druid的四大關鍵...
    大詩兄_zl閱讀 6,482評論 0 9
  • #refer1:http://www.cnblogs.com/xd502djj/p/6408979.html#re...
    liuzx32閱讀 1,929評論 0 1
  • 0. Overview 后面將寫幾篇文章介紹一下 OLAP 的大數據系統架構。這里的 Druid 不是阿里巴巴的連...
    legendtkl閱讀 4,167評論 1 3