Druid.io系列4:索引過程分析

大樹葉發表於2018-04-30

Druid底層不儲存原始資料,而是借鑑了Apache Lucene、Apache Solr以及ElasticSearch等檢索引擎的基本做法,對資料按列建立索引,最終轉化為Segment,用於儲存、查詢與分析。

首先,無論是實時資料還是批量資料在進入Druid前都需要經過Indexing Service這個過程。在Indexing Service階段,Druid主要做三件事:第一,將每條記錄轉換為列式(columnar format);第二,為每列資料建立點陣圖索引;第三,使用不同的壓縮演算法進行壓縮,其中預設使用LZ4,對於字元型別列採用字典編碼(Dictionary encoding)進行壓縮,對於點陣圖索引採用Concise/Roaring bitmap進行編碼壓縮。最終的輸出結果也就是Segment。

下面,我們先講解Druid的索引過程中的幾個基本概念,再介紹實時索引的基本原理,最後結合我們在生產環境中使用過的兩種索引模式加深對原理的理解。

1 Segment粒度與時間視窗

Segment粒度(SegmentGranularity)表示每一個實時索引任務中產生的Segment所涵蓋的時間範圍。比如設定{”SegmentGranularity” : “HOUR”},表示每個Segment任務週期為1小時。

時間視窗(WindowPeriod)表示當前實時任務的時間跨度,對於落在時間視窗內的資料,Druid會將其“加工”成Segment,而任何早於或者晚於該時間視窗的資料都會被丟棄。

Segment粒度與時間視窗都是DruidReal-Time中重要的概念與配置項,因為它們既影響每個索引任務的存活時間,又影響資料停留在Real-TimeNode上的時長。所以,每個索引任務“加工”Segment的最長週期 =SegmentGranularity+WindowPeriod,在實際使用中,官方建議WindowPeriod<= SegmentGranularity,以避免建立大量的實時索引任務。

2 實時索引原理

Druid實時索引過程有三個主要特性:

主要面向流式資料(Event Stream)的攝取(ingest)與查詢,資料進入Real-TimeNode後可進行即席查詢。

實時索引面向一個小的時間視窗,落在視窗內的原始資料會被攝取,視窗外的原始資料則會被丟棄,已完成的Segments會被Handoff到HistoricalNode。

雖然Druid叢集內的節點是彼此獨立的,但是整個實時索引過程通過Zookeeper進行協同工作。

實時索引過程可以劃分為以下四個階段:

Ingest階段
Real-TimeNode對於實時流資料,採用LSM-Tree(Log-Structured Merge-Tree )將資料持有在記憶體中(JVM堆中),優化資料的寫入效能。圖3.29中,Real-TimeNodes在13:37申明服務13:00-14:00這一小時內的所有資料。

Persist階段
當到達一定閾值(0.9.0版本前,閾值是500萬行或10分鐘,為預防OOM,0.9.0版本後,閾值改為75000行或10分鐘)後,記憶體中的資料會被轉換為列式儲存物化到磁碟上,為了保證實時視窗內已物化的Smoosh檔案依然可以被查詢,Druid使用記憶體檔案對映方式(mmap)將Smoosh檔案載入到直接記憶體 中,優化讀取效能。如圖3.29中所示,13:47、13:57、14:07都是Real-TimeNodes物化資料的時間點。
這裡寫圖片描述

圖3.28描述了Ingest階段與Persist階段內資料流走向以及記憶體情況。Druid對實時視窗內資料讀寫都做了大量優化,從而保證了實時海量資料的即席可查。

Merge階段
對於Persist階段,會出現很多Smoosh碎片,小的碎片檔案會嚴重影響後期的資料查詢工作,所以在實時索引任務週期的末尾(略少於SegmentGranularity+WindowPeriod時長),每個Real-TimeNode會產生back-groundtask,一方面是等待時間視窗內“掉隊”的資料,另一方面搜尋本地磁碟所有已物化的Smoosh檔案,並將其拼成Segment,也就是我們最後看到的index.zip。圖3.29中,當到達索引任務末期14:10分時,Real-TimeNodes開始merge磁碟上的所有檔案,生成Segment,準備Handoff。<喎�"/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxzdHJvbmc+SGFuZG9mZr3Xts48L3N0cm9uZz48YnIgLz4NCrG+vde2ztb30qrTyUNvb3JkaW5hdG9yTm9kZXO4utTwo6xDb29yZGluYXRvck5vZGVzu+G9q9LRzeqzybXEU2VnbWVudNDFz6LXorLhtb3UqtDFz6K/4qGiyc+0q0RlZXBTdG9yYWdlo6yyos2o1qq8r8i6xNpIaXN0b3JpY2FsTm9kZcilvNPU2LjDU2VnbWVudKOszazKscO/uPTSu7aoyrG85LzkuPQoxKzIzzG31tbTKbzssulIYW5kb2Zm17TMrKOsyOe5+7PJuaajrFJlYWwtVGltZU5vZGW74dTaWm9va2VlcGVy1tDJ6sP30tGyu7f+zvG4w1NlZ21lbnSjrLKi1rTQ0M/C0ru49sqxvOS0sL/axNq1xMv30v3Izs7xo7vI57n7yqew3KOsQ29vcmRpbmF0b3JOb2Rlc7vhvfjQ0Le0uLSzosrUoaPNvDMuMjnW0KOsMTQ6MTG31s3qs8lIYW5kb2ZmuaTX97rzo6y4w1JlYWwtVGltZU5vZGXJ6sP3srvU2c6qtMvKsbzktLC/2sTatcTK/b7dt/7O8aOsv6rKvM/C0ru49sqxvOS0sL/axNq1xMv30v3Izs7xoaM8L3A+DQo8cD48aW1nIGFsdD0="這裡寫圖片描述" src="/uploadfile/Collfiles/20161029/20161029091328113.png" title="\" />

下面,我們介紹Tranquility-Kafka索引過程與0.9.1.1版本中最新的Kafkaindexingservice索引過程。

3 Tranquility-Kafka

Tranquility是託管在GitHub上的開源Scala Library,主要負責協調實時索引任務中建立Indexing Service Tasks、處理partition、副本、服務發現以及更新schema。在叢集中,我們可以啟動多個Tranquility-Kafka例項,所有例項均通過Zookeeper協同處理Indexing Service Tasks。

Tranquility的出現主要是因為Indexing Service API更偏向底層(low-level),就如同Kafka Producer和Consumer在low-level API(Scala API)的基礎上又封裝了high-level API(Java API),供開發者使用。

任務生命週期

Tranquility會為時間視窗內的每一個Segment啟動一個Indexing Service Task,其中Tranquility將資料以POST請求 的方式提交給EventReceiverFirehose(Firehose實現類,預設丟棄所有時間視窗外的資料),當到達任務最大時長(SegmentGranularity+WindowPeriod)時,TimedShutoffFirehose會自動關閉Firehose,此時Segment會進行合併、註冊元資訊、儲存到Deep Storage中並等待Handoff,當某個Historical Node宣告自己已載入該Segment後,Indexing Service Task會正常退出。所以,每個Indexing Service Task的生命週期包括SegmentGranularity + WindowPeriod+push to Deep Storage + wait forHandoff。

Schema更新
Schema更新表示我們增加或減少了原始資料中的維度數或度量數。Tranquility可以自動檢測Schema更新,並儲存新老兩份Schema,對於先前建立的任務依然使用老Schema,當到達新的SegmentGranularity時,Tranquility則會使用新Schema攝取資料。

高可用性
Tranquility的所有操作都是盡最大努力(best-effort),我們可以通過配置多個任務副本保證資料不丟失,但是在某些情況下,資料可能會丟失或出現重複:

早於或晚於時間視窗,資料一定會被丟棄。

失敗的Middle Manager數目多於配置的任務副本數,部分資料可能會丟失。

IndexingService內部(Overlord、MiddleManager、Peon)通訊長時間丟失,同時重試次數超過最大上限,或者執行週期已經超過了時間視窗,這種情況部分資料也會被丟棄。

Tranquility一直未收到IndexingService的確認請求,那麼Tranquility會切換到批量載入模式,資料可能會出現重複。

所以,Druid官方建議,如果使用Tranquility作為Real-TimeNodes,那麼可以採用如下解決方案減少資料丟失或者重複的風險,從而保證Druid中資料的exactly-oncesemantics:

將資料備份到S3或者HDFS等儲存中;

晚間對備份資料執行Hadoopbatchindexingjob,從而對白天的資料重做Segment。

4 Kafka-Indexing-Service

Druid 0.9.1.1版本中新增了Experimental Features:KafkaIndexingService。之所以會增加這個新的特性,根據Druid官方部落格:將Kafka整合進Druid,不僅是看重Kafka的高吞吐量以及高可靠性,同時也因為Kafka可以使流資料下游系統,也就是KafkaConsumer端能夠更好地實現exactly-oncesemantics。我們在使用過Tranquility-Kafka後可知,資料丟失可能不僅是因為叢集節點問題,同樣可能是因為資料延遲從而造成沒有落在時間視窗內而“被丟失”。

採用Kafkaindexingservice主要有以下幾方面的考慮:

每一個進入Kafka的message都是有序、不變的,同時可以通過partition+offset的方式定位,而Druid作為Kafka的Consumer,可以通過該方式rewind到Kafka已存在的buffer中的任意一條message;

Message是由Consumer端,也就是Druid自主地pull進入,而不是被KafkaBrokerpush進叢集,push的方式我們知道,接收端無法控制接收速率,容易造成資料過載,而pull的方式Consumer端可以控制ingest速率,從而保證資料有序、穩定地進入Druid;

Message中都包含了partition+offset標籤,這就保證了作為Consumer的Druid可以通過確認機制保證每一條message都被讀取,不會“被丟失”或“被重讀”。

所以,在Kafkaindexingservice中,每一個IndexingServiceTask都對應當前topic的一個partition,每一個partition都有對應的起止offset,那麼Druid只需要按照offset順序遍歷讀取該partition中所有的資料即可。同時,在讀取過程中,Druid收到的每條message都會被確認,從而保證所有資料都被有序的讀取,作索引,“加工成”Segment。當到達SegmentGranularity時,當前partition被讀過的offset會被更新到元資訊庫的druid_dataSource表中。

KafkaSupervisor
KafkaSupervisor作為Kafkaindexingservice的監督者,執行在Overlord中,管理Kafka中某個topic對應的Druid中所有Kafkaindexingservicetasks生命週期。在生產環境中,我們通過構造Kafka Supervisor對應的spec檔案,以JSON-OVER-HTTP 的方式傳送給Overlord節點,Overlord啟動KafkaSupervisor,監控對應的Kafkaindexingservicetasks。

這裡寫圖片描述

圖3.30給出了KafkaIndexingService中的資料流以及控制流。我們總結Supervisor的特性如下:

Supervisor啟動後,會啟動最多不超過目標topic中最大partition數目的IndexingServiceTasks;

負責管理所有Indexing Service Tasks的生命週期,包括每個task的執行狀態、已執行時長(以秒為單位),剩餘時長等;

重新建立失敗任務以及協調下一個SegmentGranularity內新任務的建立工作等;

Overlord的重啟或Leader切換並不會影響Supervisor的工作;

對於Schema更新,Supervisor首先會自動停止所有以老Schema執行中的任務,釋出Segment;然後使用新Schema重新建立Indexing Service Tasks,保證在此過程中沒有messages會被丟失或者重複讀取。

Kafka indexing service在生產環境中的使用說明
這裡寫圖片描述

這裡寫圖片描述

在圖3.32中我們可以看到,該目錄下有眾多Kafkaindexingservicetasks子目錄,只有在restore.json檔案中記錄的才是目前正在執行中的任務,而剩餘的子目錄可能是因為各種原因(任務失敗、例項重啟)而未被刪除的資料夾。我們以第一個任務(index_kafka_XXX_0030af53edaf0a1_mephilnh)為例。

圖3.33中lock表示當前tasklock,task.json是當前索引任務的描述檔案,log是當前peon日誌資訊。圖3.34中,work目錄下只有一層子目錄persist,表示當前索引任務已物化的“Segment碎片”。圖3.35展示了persist目錄下的具體情況,進入persist/${dataSource}_${intervalStart}_${intervalEnd}_${segment_generate_time}/目錄下,我們可以看到四個以數字命名的資料夾,這些資料夾是當前索引任務在實時時間視窗內按照一定規則(時間閾值10分鐘或者行數閾值75000行)物化的索引檔案,每個資料夾內都由meta.smoosh、XXXXX.smoosh以及version.bin這三個檔案構成,當索引任務執行到SegmentGranularity+WindowPeriod左右,當前資料夾下會生成一個以“merged”命名的資料夾,將所有以數字命名的資料夾下的檔案合併歸一為descriptor.json和index.zip,也就是我們所說的Segment,等待publish和handoff。

這裡寫圖片描述
這裡寫圖片描述

相關文章