Druid 架構剖析
1. 前言
Druid 的目標是提供一個能夠在大資料集上做實時資料攝入與查詢的平臺,然而對於大多數系統而言,提供資料的快速攝入與提供快速查詢是難以同時實現的兩個指標。例如對於普通的RDBMS,如果想要獲取更快的查詢速度,就會因為建立索引而犧牲掉寫入的速度,如果想要更快的寫入速度,則索引的建立就會受到限制。而Druid卻可以完美的對兩者進行結合,本文將對Druid如何實現這種結合做一個簡單的介紹。
2. Druid資料流
下圖為Druid的資料流,包括資料攝入,元資料,查詢,三方面的流程
|
2.1資料攝入
資料可以通過實時節點以及批處理的方式進入Druid,對於實時節點而言,資料流的資料被實時節點消費後,當滿足條件後,實時節點會將收到的資料生成為Segment檔案並上傳到DeepStorage.批量資料經過Druid消費後,會被直接上傳到DeepStorage中。
2.2元資料
當資料檔案上傳到DeepStorage後,Coordinator節點會通知歷史節點將Segment的檔案從DeepStorage上下載到本地磁碟。
2.3查詢
在資料攝入的同事,Broker節點可以接受查詢請求,並將分別從實時節點與歷史節點的查詢的結果合併後返回
3. 架構設計
前文提到Druid在資料攝入與查詢的效能方面,做到了很好結合,本章節將詳細分析Druid是如何做到的。
3.1 常見的檔案組織方式
除了記憶體資料庫之外的大多數資料庫,資料基本都是存在磁碟上,而磁碟的訪問操作相對於記憶體操作而言是非常耗時的操作,提高資料庫效能的關鍵點之一就是減少對磁碟的訪問次數。
為了減少訪問次數,每個資料庫基本都有自己特殊的資料結構來幫助提高查詢效率(也可以叫索引),考慮到資料查詢一般都是一個範圍的資料,所以相關結構一般都不會考慮使用HASH結構,而會使用Tree結構,下面對幾種常見的Tree結構進行簡單的說明
二叉查詢樹(Binary Search Tree)
就是一顆二叉有序樹,保證左子樹上的所有節點都小於根節點,保證右子樹上的所有節點都大於根節點。優點是簡單,缺點是出現數據傾斜時,效率會很低
平衡二叉樹
針對二叉查詢樹的問題,平衡二叉樹出現了,該種樹結構的缺點是樹高為Log2N,樹的高度越高,查詢效率越低
B+樹
在傳統的關係資料庫中,B+樹以及其衍生樹是被用來作為索引資料結構最多的書,特點是能夠保持資料穩定有序,其插入與修改擁有較穩定的對數時間複雜度。詳細介紹請參考:<高效能MySql進化論(六):常見索引型別的原理及其特點的介紹> http://blog.csdn.net/eric_sunah/article/details/14045991:
B+樹的缺點來自於其優點,當儲存資料為億級別時,隨著插入操作的不斷產生,葉子節點會慢慢的分裂,可能導致原本連續存放的資料,存放在不同的物理磁碟塊位置上,做範圍查詢時,導致較高的磁碟IO,導致效能下降
日誌合併樹(LSM)
對於寫操作而言,順序寫的效率會遠遠大於隨機寫的速度,而上述的B+樹就違反了該原則。
1992年日誌結構(Log Structured)的新型索引結構產生了,該種類型的思想主要是將磁碟看成是一個大的日誌,每次資料都最佳到末端,從而提高了寫的效能。此時的日誌結構索引對於隨機讀取的效率很低。
1996年,日誌結構合併樹(Log structured Merge-Tree-LSM)的索引結構被提出。該種結構包含了LS的優點,同時又通過將資料檔案預排序來克服隨機查效能不足的問題。
LSM的名聲大噪歸功於HBASE以及Cassandra,隨著這兩個Apache頂級專案的發展,LSM技術也在不斷的發展
LSM-tree是由兩個或兩個以上儲存資料的結構組成的。最簡單的LSM-tree由兩個部件構成。一個部件常駐記憶體,稱為C0樹(或C0),可以為任何方便鍵值查詢的資料結構,另一個部件常駐硬碟之中,稱為C1樹(或C1),其資料結構與B-tree類似。C1中經常被訪問的結點也將會被快取在記憶體中。如下圖所示:
當插入一條新的資料條目時,首先會向日志文件中寫入插入操作的日誌,為以後的恢復做準備。然後將根據新條目的索引值將新條目插入到C0中。將新條目插入記憶體的C0中,不需要任何與硬碟的I/O操作,但記憶體的儲存代價比硬碟的要高上不少,因此當C0的大小達到某一閾值時,記憶體儲存的代價會比硬碟的I/O操作和儲存代價還高。故每當C0的大小接近其閾值時,將有一部分的條目從C0滾動合併到硬碟中的C1,以減少C0的大小,降低記憶體儲存資料的代價。
除了C0,C1結構,LSM Tree通常還會結合日誌檔案(commit log,在Hbase中叫Hlog)來為資料恢復做保障,C0,C1,commit log的協作順序大概為:
1.新插入的資料首先寫到commit log中,該操作叫WAL(Write Ahead LOG)
2.寫完commit log後,資料寫到C0
3.打到一定條件後,C0中的資料被Flush到C1,並刪除對應的commit log
4.C0,C1資料可同時提供查詢
5.當c0資料出問題了,可以使用commit log與c1中的內容回覆C0
LSM-Tree的結構非常有利於海量資料的寫入,但是在查詢方面還是存在不足,為了解決查詢效能問題,一般採用如下策略進行彌補:
1.定期的對C1上的小的檔案進行合併。
2..對C1使用布隆過濾器,以加速查詢資料是否在某個C1中的判定。
3.2 Druid中的LSM-Tree
LSM-Tree適合哪種寫操作要遠遠大於DELETE/UPDATE/QUERY的應用場景,這正好符合Druid的使用場景,所以Druid的檔案組織方式與LSM-Tree類似。
對於可以攝取實時資料的實時節點而言,涉及操作大致如下:
1. 實時資料首先會被載入到實時節點記憶體中的堆結構緩衝區
2. 當條件滿足時,緩衝區的資料會被flush到磁碟上變成一個數據塊
3. 將磁碟上的資料塊載入到記憶體中的非堆區
4. 查詢節點可以同時從堆緩衝區與非堆區進行資料查詢
上述描述,可以用下面的圖來進行表示:
對於已經落地到實時節點的磁碟的資料塊,還會進行如下處理:
1. 實時節點週期性的將統一時間段內的資料塊檔案合併成一個大的檔案
2. 生成好的大檔案會立即被上傳到Deep Storage
3. 協調節點感知到有新的資料塊檔案被上傳到DeepStorage後,會協調某個歷史節點對相關檔案進行下載
4. 歷史節點載入完相關資料後,會通過協調節點對外宣告對於該檔案內容的查詢,都由自己提供。產生該檔案的實時節點也會對外宣告,不再負責對應資料的查詢
從上述內容可以看出,Druid的類LSM-Tree結構有以下特點:
1. 類LSM-Tree的架構,保證了Druid的高效能寫入
2. 通過“查詢職責分離模式+不支援更新操作” 保證了元件職責的單一以及資料處理的簡單性,保證了查詢效能的高效性
3.3 資料儲存結構
Druid的高效能,除了來自類LSM-Tree的貢獻,其DataSource以及Segment的完美設計也功不可沒。
3.3.1 Datasource
Druid中的Datasource可以理解為RDBMS中的表,其包含下面三個重要的概念:
1. 時間列(Timestamp):每行資料的時間值,預設使用UTC時間格式,儲存到毫秒級別,本列是資料聚合以及範圍查詢的重要指標
2. 維度列(Dimension):標識資料行的列,可以是一列,也可以是多列
3. 指標列(Metric):用來做計算或是統計的列,可以是一列,也可以是多列
相對於其他資料庫,Druid Datasource最大的特點是在輸入儲存時,就可以對資料進行聚合操作,該特性不僅可以節省儲存的空間,而且可以提高聚合查詢的效率。
3.3.2 Segment
Segment為Druid中資料的物理儲存格式,Segment通過以下特性來支撐Druid的高效能:
資料的橫向切割:橫向切割主要只指站在時間範圍的角度,將不同時間段的資料儲存在不同的Segment檔案中(時間範圍可以通過segmentGranularity進行設定),查詢時只需要更具時間條件遍歷對應的Segment檔案即可。
資料的縱向切割:面向列進行進行資料壓縮
使用BitMap等技術對資料訪問進行優化
4. 元件介紹
4.1 實時節點
實時節點主要負責實時資料攝入,以及生成Segment檔案。
4.1.1 獲取資料與生成Segment檔案
實時節點通過Firehose來消費實時資料,Firehose是Druid中的消費實時資料模型,可以有不同的實現,Druid自帶了一個基於Kafka High Level API實現的對於Kafka的資料消費(druid-kafka-eight Firehose)。
除了Firehose,實時節點上還有一個重要的角色叫Plumber,主要負責按照指定的週期,對資料檔案進行合併。
實時節點提供Pull以及Push兩種方式對資料進行攝取,詳細介紹將在後續文中進行詳細描述。
4.1.2 高可用
當使用druid-kafka-eight從Kafka進行資料消費時,該Firehose可以讓實時節點具有很好的可擴充套件性。當啟動多個實時節點時,將使用Kafka Consumer Group的方式從Kafka進行資料獲取,通過Zookeeper來維護每個節點的offset情況,無論是增加節點,還是刪除節點,通過High API都可以保證Kafka的資料至少被Druid的叢集消費一次。Kafka Consumer Group的詳細說明,請參考:http://blog.csdn.net/eric_sunah/article/details/44243077
通過druid-kafka-eight保證的高可用,仔細分析可以發現會存在生成的segment檔案不能被傳到Deepstorage的缺陷,解決該問題可以通過兩個辦法
1.重啟實時節點
2.使用Tranquility+Index Service的方式對Kafka的資料進行精確的消費與備份。由於Tranquility可以通過Push的方式將制定的資料推到Druid叢集,一次它可以對同一個Partition資料建立多個副本,當某個資料消費任務失敗時,系統可以準確的使用另外一個相同任務所建立的Segment資料塊。
4.2 歷史節點
歷史節點的職責比較單一,主要是將segment資料檔案載入到記憶體以提供資料查詢,由於Druid不支援資料變更,因此歷史節點就是載入檔案與提供查詢。
4.2.1 記憶體方式提供查詢
Coordinator Nodes會定期(預設為1分鐘)去同步元資訊庫,感知新生成的Segment,將待載入的Segment資訊儲存在Zookeeper中,當Historical Node感知到需要載入新的Segment時,首先會去本地磁碟目錄下查詢該Segment是否已下載,如果沒有,則會從Zookeeper中下載待載入Segment的元資訊,此元資訊包括Segment儲存在何處、如何解壓以及如何如理該Segment。Historical Node使用記憶體檔案對映方式將index.zip中的XXXXX.smoosh檔案載入到記憶體中,並在Zookeeper中本節點的served segments目錄下宣告該Segment已被載入,從而該Segment可以被查詢。對於重新上線的Historical Node,在完成啟動後,也會掃描本地儲存路徑,將所有掃描到的Segment載入如記憶體,使其能夠被查詢。
==由於歷史節點提供的查詢服務依賴於記憶體,所以記憶體的大小直接影響到歷史節點的效能。==
4.2.2 資料分層(Tiers)
在面對海量資料儲存時,一般都會使用資料分層的儲存策略,常見策略如下:
1. 熱資料:經常被訪問,資料量不大,查詢延遲要求低
2. 溫資料:不經常被訪問,資料量較大,查詢延遲要求儘量低
3. 冷資料:偶爾被訪問,資料量佔比最大,查詢延遲不用很快
Druid中,歷史節點可以分成不同的層次,相同層次中的所有節點都採用相同的配置。 可以為每一層設定不同的效能和容錯引數。
4.2.3 高可用
歷史節點擁有較好的高可用特性,協調節點可以通過Zookeeper感知到歷史節點的增加或是刪除操作。當新增歷史節點時,協調可以自動分配Segment給新增的節點,當移除歷史節點時,協調節點會將該歷史節點上的資料分配給其他處於Active狀態的歷史節點。
歷史節點依賴於Zookeeper進行Segment資料的載入和解除安裝操作。如果Zookeeper變得不可用,歷史節點將不能再進行資料載入和解除安裝操作。但是因為查詢功能使用的是HTTP服務,所以Zookeeper出現異常後,不會影響歷史節點上對以載入資料的查詢。
4.3 查詢節點
查詢節點負責接收查詢請求,並將實時節點以及歷史節點的查詢結果合併後返回。
4.3.1 快取
Druid提供了兩類介質提供Cache功能:
1. 外部Cache,例如Memcache
2. 內部Cache,使用查詢節點或是歷史節點的記憶體
Broker Node預設使用LRU快取策略,查詢的時候會首先訪問Cache,如果Cache沒有命中,才會繼續訪問歷史/實時節點。
對於每次查詢的結果,Historical Node返回的結果,Broker Node認為是“可信的”,會快取下來,而Real-Time Node返回的實時資料,Broker Node認為是可變的,“不可信的”,所以不會快取。所以對每個查詢請求,如果涉及到實時節點,則該請求總是會轉到實時節點。
Cache也可以理解為對資料額外的備份,即使說有的歷史節點都掛了,還是有可能從Cache中查到對應的資料。
Cache的原理圖如下:
4.3.2 高可用
可以通過Nginx+(N*Broker Node)的方式達到查詢節點高可用的效果,該種部署模式下,無論查詢請求落到哪個查詢節點,返回的結果都是相同的。
4.4 協調節點
協調節點主要負責管理歷史節點的負載均衡以及根據規則管理資料的生命週期
4.4.1 歷史節點負載均衡
在典型的生產環境中,查詢通常會觸及幾十個甚至幾百個Segment 。由於每個歷史節點都資源有限,所以必須在叢集中均衡的分配Segment。均衡的策略主要基於成本的優化,例如考慮時間和大小,遠近等因素。
對於歷史節點而言,協調節點就是其Master節點,協調節點出問題時,歷史節點雖然還可以提供查詢功能,但不會再接收新的segment資料。
4.4.2 資料生命週期
Druid利用針對每個DataSource設定的Rule來載入或丟棄具體的資料檔案。規則用來表名錶明應該如何分配Segment到不同的歷史節點層,以及一個分段的在每個層應該有多少個副本等。規則還可以用來指定什麼時候應該刪除那些Segment。
可以對一個Datasource新增多條規則,對於某個Segment來說,協調節點會逐條檢查規則,當檢測到某個Segment符合某個規則時,就命令對應的歷史節點執行對應的操作。
4.4.3 Replication
Druid允許使用者指定某個Datasource的Segment副本數,預設為1,即對於某個datasource的某個segment,只會存在於單個歷史節點上。 為了防止某個歷史節點宕機時,部分segment的不可用,可以根據資源的情況增加segment的副本數。
4.4.4 高可用
可以通過部署多個協調節點來達到協調節點高可用的目的,如果叢集中存在多個Coordinator Node,則通過選舉演算法產生Leader,其他Follower作為備份。
4.5 索引服務(Indexing Service)
索引服務也可以產生Segment檔案,相對於實時節點,索引節點主要包括以下優點:
1. 除了支援Pull的方式攝取資料,還支援Push的方式
2. 可以通過API的方式定義任務配置
3. 可以更靈活的使用系統資源
4. 可以控制segment副本數量的控制
5. 可以靈活的完成和segment資料檔案相關的操作
6. 提供可擴充套件以及高可用的特性
4.5.1 主從結構的架構
Indexing Service是高可用、分散式、Master/Slave架構服務。主要由三類元件構成:負責執行索引任務(indexing task)的Peon,負責控制Peon的MiddleManager,負責任務分發給MiddleManager的Overlord;三者的關係可以解釋為:Overlord是MiddleManager的Master,而MiddleManager又是Peon的Master。其中,Overlord和MiddleManager可以分散式部署,但是Peon和MiddleManager預設在同一臺機器上,架構圖如下:
4.5.2 統治節點(Overload)
Overlord負責接受任務、協調任務的分配、建立任務鎖以及收集、返回任務執行狀態給呼叫者。當叢集中有多個Overlord時,則通過選舉演算法產生Leader,其他Follower作為備份。
Overlord可以執行在local(預設)和remote兩種模式下,如果執行在local模式下,則Overlord也負責Peon的建立與執行工作,當執行在remote模式下時,Overlord和MiddleManager各司其職,根據上圖所示,Overlord接受實時/批量資料流產生的索引任務,將任務資訊註冊到Zookeeper的/task目錄下所有線上的MiddleManager對應的目錄中,由MiddleManager去感知產生的新任務,同時每個索引任務的狀態又會由Peon定期同步到Zookeeper中/Status目錄,供Overlord感知當前所有索引任務的執行狀況。
Overlord對外提供視覺化介面,通過訪問http://:/console.html,我們可以觀察到叢集內目前正在執行的所有索引任務、可用的Peon以及近期Peon完成的所有成功或者失敗的索引任務。
4.5.3 MiddleManager
MiddleManager負責接收Overlord分配的索引任務,同時建立新的程序用於啟動Peon來執行索引任務,每一個MiddleManager可以執行多個Peon例項。
在執行MiddleManager例項的機器上,我們可以在${ java.io.tmpdir}目錄下觀察到以XXX_index_XXX開頭的目錄,每一個目錄都對應一個Peon例項;同時restore.json檔案中儲存著當前所有執行著的索引任務資訊,一方面用於記錄任務狀態,另一方面如果MiddleManager崩潰,可以利用該檔案重啟索引任務。
4.5.4 Peon
Peon是Indexing Service的最小工作單元,也是索引任務的具體執行者,所有當前正在執行的Peon任務都可以通過Overlord提供的web視覺化介面進行訪問