Druid 架構
本篇譯自 Druid 專案 白皮書 部分內容( https://github.com/apache/incubator-druid/tree/master/publications/whitepaper/druid.pdf),如果有興趣可看細看原 pdf【初次翻譯多多包涵】
一個 Druid 叢集包含多種特定功能的節點, 我們相信這種設計能夠分散業務並且簡化整個系統的複雜性。不同節點型別相互獨立得執行,它們之間有很小的干擾。因此,叢集內部的通訊故障對資料的可用性影響很小。
為了解決複雜資料分析的問題,不同型別的節點共同組成一個完整可用的系統。Druid 這個名字來自許多角色扮演遊戲中的德魯伊角色類:它是一個形狀移位器,能夠採用多種不同的形式來實現一組中的各種不同角色的組成和流動。Druid 叢集中的資料如圖所示:
1. 實時節點 Real-Time Nodes
實時節點封裝了用於攝取和查詢事件流的功能,事件經這些節點建立索引後可以立即用於查詢。節點僅關注一小段時間範圍內的事件,然後週期性的將這段小時間範圍內的不可變的一批事件交給 Druid 叢集內的其他專門用於處理一批批不可變事件的節點。實時節點利用 ZooKeeper 與其他節點進行協調,節點在 ZooKeeper 中宣告他們的線上狀態和他們提供的資料。
實時節點維護一個在記憶體中的索引緩衝用於所有即將發生的事件。這些索引隨著事件的攝取會增量式的增加,而且索引可以直接用於查詢。Druid 在 JVM 的堆快取中查詢時表現為一個行儲存,為了避免堆溢位,實時節點會週期性的或者在行數超過最大限制後將記憶體中的索引持久化到磁碟中。這個持久化程式會將記憶體中的緩衝資料轉換成一個面向列儲存的資料格式。每一個持久化後的索引都是不可變的然後實時節點載入持久化後的資料到非堆記憶體中這樣資料仍然可以在實時節點中被查詢到。
每一個實時節點都會週期性的排程一個後臺任務去查詢所有持久化到本地的索引,然後把一段時間內實時節點生成的索引合併到一起形成一個包含所有被合併索引的所有事件的塊,我們把這種塊稱作 段(segment)
。實時節點在將資料交出階段會將 segment 上傳到被稱作 deep storage
的永久備份儲存中,一般是一個分散式的儲存系統比如 Amazon S3 或者 HDFS。
上圖展示了實時節點的操作順序,節點在 13:37 時啟動然後只接受當前小時內和下個小時的事件資料。當事件被攝取後節點就宣告它正在提供一個 13:00 到 14:00 的 segment,每隔10分鐘(持久化週期可配置)節點都會將記憶體中的索引沖刷並持久化到磁碟上。在接近當前小時的結尾時節點可能會收到 14:00 到 15:00 的資料,當此事發生時節點會準備提供下一個小時的資料並建立一個新的記憶體索引,然後節點會宣告它又提供了一個 14:00 到 15:00 的 segment。此時,節點不會立即把 13:00 到 14:00 的索引給合併,而是會等待一個可配置的視窗期來接收比較慢的那些 13:00 到 14:00 的資料。這視窗週期最小化了延遲傳遞過來的資料的丟失的風險。在視窗週期的末期,實時節點合併所有的 13:00 到 14:00 的持久化的索引檔案到一個不可以變的 segment 中然後交給其他節點,當該 segment 在 Druid 叢集的其他節點被載入和可被查詢後,實時節點將清掉節點中儲存的該 segment 的資訊並且宣告不再提供此資料。
可用性和擴充套件性
實時節點作為資料的消費者,需要對應的生產者來提供資料流。通常,為了資料的穩定性我們在資料生產者和實時節點中間架設一個訊息匯流排如 Kafka, 實時節點通過從訊息匯流排讀取事件來攝取資料,事件從生成到消費所用的時間一般會在毫秒級。
訊息匯流排的用途一般分為兩個方面。首先,訊息匯流排為即將到來的事件作為一個緩衝。像 Kafka 這樣,一個訊息匯流排維護一個 offset 來表示一個消費者(實時節點)消費了多少的事件資料流,實時節點每次在將資料持久化到磁碟上時更新 offset。當遇到失敗或者恢復這種場景時,如果節點沒有丟失自己的資料它就可以重新從磁碟載入所有的持久化的索引然後從上次的提交的 offset 那裡開始繼續讀取資料。從上次提交的 offset 那裡開始攝取資料這種方式大大減少了一個節點恢復的時間,在實踐中我們看到一個節點從類似的失敗中恢復只用了幾秒。
訊息匯流排用途的第二方面就是扮演多個實時節可以讀取事件資料的端點服務。多個實時節點可以通過訊息匯流排獲取同樣的資料用來建立一個事件副本,當遇到一個節點硬碟壞掉以至於完全壞掉時,事件副本可以保障沒有資料丟失。一個攝取節點支援資料流分割槽這樣多個實時節點每個節點可以攝取資料流的一個分割槽,還可以非常簡單的新增額外的實時節點,在實踐中該模式允許一個最大的 Druid 叢集能夠以大約 500 MB/s (150,000 events/s or 2 TB/hour) 的速度消費原始資料。
2. 歷史節點 Historical Nodes
歷史節點封裝了載入和提供不可變 segment 的查詢功能。在許多真實工作流程中,多數載入進 Druid 的資料都是不可變的,因此,歷史節點是是 Druid 叢集裡面典型的主工作節點。歷史節點採用了無共享架構,多個節點之間沒有單點競爭,每個節點都不用感知其他節點的儲存而且操作簡單,他們只知道如何載入、解除安裝和提供不可變 segment。
和實時節點一樣,歷史節點也在 ZooKeeper 中宣告它們的上線狀態和提供的資料。歷史節點載入和解除安裝資料的指令也是通過 ZooKeeper 來發送,指令資訊中包含了 segment 在 deep storage 的位置以及如何解壓和處理 segment。在一個歷史節點下載一個特定的 segment 之前它首先會檢查本地快取檢視已載入的 segment,如果要載入的 segment 不在快取中,歷史節點將會去 deep storage 中下載 segment。一旦過程完畢,segment 就會被在 ZooKeeper 中宣告出來然後 segment 就可被查詢。本地快取也使得歷史節點可以快速的更新和重啟,歷史節點在重啟時會檢查本地快取然後立即提供所有它找到的資料。
歷史節點支援讀一致性因為他們只處理只讀的資料,不可變資料也使得一個簡單的併發模型成為可能:歷史節點可以併發的掃描和聚合不可變的塊而不被阻塞。
層 (tiers)
歷史節點可以通過一個有識別性的 tier 進行分組,可以為每個 tier 分配不同效能和錯誤容忍性的引數。層級化後的節點用途可以使得不同高低優先順序的 segment 根據重要程式被分佈到不同的節點。例如,我們可以給一個擁有多核心和大記憶體容量的節點貼上 "hot" 級,然後 "hot" 級的節點叢集就可以被配置為下載那些更頻繁訪問的資料。一個並行的 "cold" 節點叢集就可以使用效能略差一些的硬體來搭建,用來儲存不經常被訪問的 segments。
可用性
歷史節點依賴 ZooKeeper 來執行 segment 的載入和解除安裝指令,如果 ZooKeeper 變為不可用狀態,歷史節點便不能再提供新資料或者丟棄掉舊資料了。但是由於歷史節點的查詢服務是通過 HTTP 提供的,所以歷史節點依然可以響應那些他們當前還在提供服務的資料的查詢請求,這意味著 ZooKeeper 的停機不會影響歷史節點當前服務的資料的可用性。
3. 代理節點 Broker Nodes
Broker 節點扮演一個在歷史節點和實時節點之上的路由角色,它們能夠解析 ZooKeeper 中釋出的元資料資訊,其中包含了哪些 segment 是可以被查詢的以及對應的位置。Broker 節點負責將查詢路由到正確的歷史節點或實時節點上,然後還會把歷史節點和實時節點返回的部分資料合併成最終的整理後的結果返回給呼叫者。
快取 caching
Broker 節點包含一個帶 LRU 失效策略的快取,該快取能夠使用本地的堆記憶體或者外部的分散式的 key/value 鍵值儲存,例如 Memcached。Broker 節點每次收到查詢請求時,它首先會把查詢對映到系列的 segment 上,某些 segment 上的查詢結果可能已經在快取中存在了所以不用重新計算。對於那些在快取中沒有的結果,Broker 節點會直接去對應的歷史節點和實時節點中計算,當歷史節點返回結果之後 Broker 節點會把結果按照 segment 進行快取方便將來使用。實時節點的查詢結果永遠不會被快取因此查詢實時節點資料的請求會一直到達實時節點,實時節點的資料一直在改變,如果快取查詢結果的話該結果是不可信的。
快取也充當一個數據可靠性的附加層,當歷史節點都掛掉後在快取中存在的查詢結果依然可以被查詢到。
可用性 Availability
在 ZooKeeper 停機後,資料依然可以被查詢。當 Broker 節點無法與 ZooKeeper 通訊時,Broker 節點會使用它們上次記錄的叢集的資訊來繼續把查詢傳送給實時節點和歷史節點。Broker 節點會假設 ZooKeeper 停機前後叢集的結構是一樣的。在實踐中,這種可用性模式使我們的叢集在我們診斷出 ZooKeepr 停機後仍然為查詢提供了相當長時間的服務。
4. 協調節點 Coordinator Nodes
Druid 協調器節點主要負責歷史節點上的資料管理和分發。協調節點告知歷史節點載入新資料、刪除過期資料、複製資料和移動資料來達到負載均衡。Druid 使用多版本併發控制交換協議來管理不可變的 segments 從而維護一個穩定的檢視。如果一個不可變 segments 包含的資料在一個新的 segments 中被標記刪除,這個過時的 segment 會被叢集丟棄。協調節點進行一個領導選舉來決定哪個節點行使協調功能,剩餘的節點用於冗餘備份。
一個協調節點週期性的執行來斷定當前的叢集狀態,它通過對比當前時間叢集的狀態和期望狀態來做決定。與 Druid 所有節點一樣,協調節點維護一個 ZooKeeper 連線用於獲取當前叢集的資訊,協調節點也會維護一個 包含額外操作引數和配置的SQL/">MySQL 資料庫的連線。MySQL 資料庫中儲存的重要資訊中其中一個就是包含了所有應該被歷史節點處理的 segments 列表的資料庫表,該表可以被任何生成 segments 的服務(如實時節點)所更新。MySQL 資料庫同樣包含一個用於管理如何建立、銷燬和做副本的規則表。
規則 Rules
Rules 管理著如何從叢集載入、丟棄歷史節點的 segments,指示著 segments 應該如何被分配到不同的歷史節點的 tier 中以及一個 tier 中應該儲存多少個 segment 的副本,甚至 Rules 還指示著 segments 何時會被整體從叢集丟棄掉。一般情況下 Rules 會被用在一段時間內,例如,一個使用者可以使用 rules 來把一個月內的 segments 載入到 "hot" 叢集中,把一年內的 segments 載入到 "cold" 叢集中,然後把更老的資料丟棄掉。
協調節點從 MySQL 資料庫的 rule 表中載入一系列的規則,資料來源可以關聯一些特定或者預設的規則。協調節點會遍歷所有可用的 segment 然後匹配每一個 segment 的第一個規則。
負載均衡 Load Balancing
在典型的生產環境中,一個查詢經常會命中數十個甚至上百個 segment,限於每個歷史節點的資源限制,segments 非常必要在叢集中分發以保證叢集的負載不至於太過於傾斜確定。最佳負載分佈需要一些有關查詢模式 和速度的知識。通常情況下,查詢會覆蓋跨越單個數據源的最近連續時間間隔的段,平均而言,訪問更小 segme-nt 的查詢會更快一些。
這種查詢模式建議以更高的速率複製最近的歷史片段(segment),分散時間上接近的大片段(segment) 到不同的歷史節點,並共同定位來自不同資料來源的段 (segments)。為了在群集中最佳地分配和平衡段,我們開發了基於成本的優化過程,該過程考慮了段資料來源,新近度和大小,該演算法的確切細節超出了本文範圍可能會在未來的文獻中討論。
副本 Replication
協調節點可以告訴不同的歷史節點載入同一段的副本,歷史節點的每個層(tier)中的副本數是完全可配置的,需要高級別容錯的設定可以配置為具有大量副本,副本段的處理方式與原始段相同,並遵循相同的負載分配演算法。通過複製段,單個歷史節點故障在Druid叢集中是透明的,我們使用此屬性進行軟體升級,我們可以無縫地使歷史節點離線更新它和進行備份,併為群集中的每個歷史節點重複此過程。 在過去兩年中,我們從未在 Druid 叢集中更新軟體而佔用停機時間。
可用性 Availability
Druid 協調節點含有 ZooKeeper 和 MySQL 的額外依賴,協調節點依賴 ZooKeeper 來確定叢集中都已經存在了哪些歷史節點,如果 ZooKeeper 變為不可用協調節點便不能再發送指派、平衡和丟棄 segment 的指令了。無論如何,這些操作都根本不會影響到資料的可用性。
響應 MySQL 和 ZooKeeper 失敗的設計原則是相同的:如果負責協調的外部依賴失敗,則叢集維持現狀。 Druid 使用 MySQL 儲存操作管理資訊並分析有關群集中應存在哪些段的元資料資訊。 如果 MySQL 發生故障,協調器節點將無法使用此資訊。 但是,這並不意味著資料本身不可用。 如果協調節點無法與 MySQL 通訊,它們將停止分配新的段以及刪除過時的段等操作。 在MySQL中斷期間,代理,歷史和實時節點仍可查詢。