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的協作順序大概為:
- 新插入的數據首先寫到commit log中,該操作叫WAL(Write Ahead LOG)
- 寫完commit log后,數據寫到C0
- 打到一定條件后,C0中的數據被Flush到C1,並刪除對應的commit log
- C0,C1數據可同時提供查詢
- 當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實現的高可用機制,可用下圖進行表示:
通過druid-kafka-eight保證的高可用,仔細分析可以發現會存在生成的segment文件不能被傳到Deepstorage的缺陷,解決該問題可以通過兩個辦法
- 重啟實時節點
- 使用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加載如內存,使其能夠被查詢。
無論何種查詢,歷史節點都會先將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可視化界面進行訪問