導讀:本文將描述 Apache Druid 的基本集群架構,說明架構中各進程的作用。並從數據寫入和數據查詢兩個角度來說明 Druid 架構的工作流程。
關注公眾號 MageByte,設置星標點「在看」是我們創造好文的動力。公眾號后台回復 “加群” 進入技術交流群獲更多技術成長。
Druid 是多進程架構,每種進程類型都可以獨立配置,獨立擴展。這樣可以為集群提供最大的靈活度。這種設計還提供了強失效容忍:一個失效的組件不會立即影響另外的組件。
下面我們來深入了解 Druid 有哪些進程類型,每種進程又在整個集群中扮演什么角色。
進程和服務(Process and Servers)
Druid 有多種進程類型,如下:
- Coordinator進程在集群中負責管理數據可用。
- Overlord進程控制數據攝入的資源負載分配。
- Broker進程處理外部客戶端的查詢。
- Router進程是可選的,它可以路由請求到 Brokers,Coordinator,和 Overlord。
- Historical進程存儲可查詢的數據。
- MiddleManager進程負責數據攝入。
你可以以任何方式來部署上面的進程。但是為了易於運維,官方建議以下面三種服務類型來組織進程:Master、Query 和 Data。
- Master:運行 Coordinator 和 Overlord 進程,管理數據可用和數據寫入。
- Query: 運行 Broker 和可選的 Router 進程,負責處理外部查詢請求。
- Data:運行 Historical 和 MiddleManager 進程,負責執行數據寫入任務並存儲可查詢的數據。
外部依賴(External dependencies)
除了內置的進程類型,Druid 還有三個外部依賴項。
Deep storage
共享文件存儲,只要配置成允許 Druid 訪問即可。在集群部署中,通常使用分布式存儲(如 S3 或 HDFS)或掛載網絡文件系統。在單機部署中,通常使用本地磁盤。Druid 使用 Deep Storage 存儲寫入集群的數據。
Druid 僅將 Deep Storage 用作數據的備份,並作為 Druid進程間在后台的數據傳輸方式。要響應查詢,Historical 進程並不從 Deep Storage 上讀取數據,在任何查詢之前,先從本地磁盤查詢已經存在的數據。這意味着,Druid 在查詢時並不需要訪問 Deep Storage,這樣就可以得到最優的查詢延遲。這也意味着,在 Deep Storage 和 Historical 進程間你必須有足夠的磁盤空間來存儲你計划加載的數據。
Deep Storage 是 Druid 彈性、容錯設計的重要組成部分。如果 Druid 單機進程本地數據丟失,可以從 Deep Storage 恢復數據。
Metadata storage
元數據存儲,存儲各種共享的系統元數據,如 segment 可用性信息和 task 信息。在集群部署中,通常使用傳統的 RDBMS,如 PostgreSQL 或 MySQL。在單機部署中,通常使用本地存儲,如 Apache Derby 數據庫。
Zookeeper
用來進行內部服務發現,協調,和主選舉。
架構圖(Architecture diagram)
下圖可以看出使用官方建議的 Master/Query/Data 服務部署方式,查詢和寫入數據是如何進行的:
存儲設計(Storage design)
Datasources and segments
Druid 數據存儲在"datasources"中,它就像 RDBMS 中的 table。每一個 datasources 通過時間分區,或通過其他屬性進行分區。每一個時間范圍稱之為"chunk"(比如,一天一個,如果你的 datasource 使用 day 分區)。在 chunk 中,數據被分區進一個或多個"segments"中。每一個 segment 是一個單獨的文件,通常包含數百萬行數據。一旦 segment 被存儲進 chunks,其組織方式將如以下時間線所示:
一個 datasource 也許只有一個,也可能有數十萬甚至上百萬個 segment。每個 segment 生命周期開始於 MiddleManager 創建時,剛被創建時,segment 是可變和未提交的。segment 構建過程包含以下幾步,旨在生成結構緊湊並支持快速查詢的數據文件。
- 轉換成列格式
- 使用 bitmap 創建索引
- 使用各種算法壓縮數據
- 為 String 列做字典編碼,用最小化 id 存儲
- 對 bitmap 索引做 bitmap 壓縮
- 對所有列做類型感知壓縮
segment 定時提交和發布。此時,數據被寫入 Deep Storage,並且再不可變,並從 MiddleManagers 進程遷移至 Historical 進程中。一個關於 segment 的 entry 將寫入 metadata storage。這個 entry 是關於 segment 的元數據的自描述信息,包含如 segment 的數據模式,大小,Deep Storage 地址等信息。這些信息讓 Coordinator 知道集群中哪些數據是可用的。
索引和移交(Indexing and handoff)
indexing 是每個 segment 創建的機制。handoff 是數據被發布並開始可以被 Historical 進程處理的機制。這機制在 indexing 側的工作順序如下:
- 啟動一個 indexing task 並構建一個新的 segment。在構建之前必須先確定其標識。對於一個追加任務(如 kafka 任務,或 append 模式任務)可以調用 Overlord 的"allocate"API 來將一個潛在的新分區加入到一個已經存在的 segment 中。對於一個覆寫任務(如 Hadoop 任務,或非 append 模式 index 任務) 將為 interval 創建新版本號和新 segment。
- 如果 indexing 任務是實時任務(如 Kafka 任務),此時 segment 可以立即被查詢。數據是可用的,但還是未發布狀態。
- 當 indexing 任務完成讀取 segment 數據時,它將數據推送到 Deep Storage 上,並通過向 metadata store 寫一個記錄來發布數據。
- 如果 indexing 任務是實時任務,此時,它將等待 Historical 進程加載這個 segment。如果 indexing 任務不是實時任務,就立即退出。
這機制在 Coordinator/Historical 側的工作如下:
- Coordinator 定期從 metadata storage 拉取已經發布的 segments(默認,每分鍾執行)。
- 當 Coordinate 發現已發布但不可用的 segment 時,它將選擇一個 Historical 進程去加載 segment,並指示 Historical 該做什么。
- Historical 加載 segment 並為其提供服務。
- 此時,如果 indexing 任務還在等待數據移交,就可以退出。
數據寫入(indexing)和移交(handoff):
段標識(Segment identifiers)
Segment 標識由下面四部分組成:
- Datasource 名稱。
- 時間間隔(segment 包含的時間間隔,對應數據攝入時
segmentGranularity
指定參數)。 - 版本號(通常是 ISO8601 時間戳,對應 segment 首次生成時的時間)。
- 分區號(整數,在 datasource+interval+version 中唯一,不一定是連續的)。
例如,這是 datasource 為clarity-cloud0
,時間段為2018-05-21T16:00:00.000Z/2018-05-21T17:00:00.000Z
,版本號為2018-05-21T15:56:09.909Z
,分區號為 1 的標識符:
clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z_1
分區號為 0(塊中的第一個分區)的 segment 省略了分區號,如以下示例所示,它是與前一個分區在同一時間塊中的 segment,但分區號為 0 而不是 1:
clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z
段版本控制(segment versioning)
你可能想知道上一節中描述的“版本號”是什么。
Druid 支持批處理模式覆寫。在 Driud 中,如果你要做的只是追加數據,那么每個時間塊只有一個版本。但是,當你覆蓋數據時,在幕后發生的事情是使用相同的數據源,相同的時間間隔,但版本號更高的方式創建了一組新的 segment。這向 Druid 系統的其余部分發出信號,表明應從群集中刪除較舊的版本,而應使用新版本替換它。
對於用戶而言,切換似乎是瞬間發生的,因為 Druid 通過先加載新數據(但不允許對其進行查詢)來處理此問題,然后在所有新數據加載完畢后,立即將新查詢切換到新 segment。然后,它在幾分鍾后刪除舊 segment。
段(segment)生命周期
每個 segment 的生命周期都涉及以下三個主要領域:
- 元數據存儲區:一旦構建完 segment,就將 segment 元數據(小的 JSON 數據,通常不超過幾個 KB)存儲在
元數據存儲區
中。將 segmnet 的記錄插入元數據存儲的操作稱為發布。然后將元數據中的use
布爾值設置成可用
。由實時任務創建的 segment 將在發布之前可用,因為它們僅在 segment 完成時才發布,並且不接受任何其他數據。 - 深度存儲:segment 數據構建完成后,並在將元數據發布到元數據存儲之前,立即將 segment 數據文件推送到深度存儲。
- 查詢的可用性:segment 可用於在某些 Druid 數據服務器上進行查詢,例如實時任務或Historical進程。
你可以使用 Druid SQL sys.segments
表檢查當前 segment 的狀態 。它包括以下標志:
is_published
:如果 segment 元數據已發布到存儲的元數據中,used
則為 true,此值也為 true。is_available
:如果該 segment 當前可用於實時任務或Historical查詢,則為 True。is_realtime
:如果 segment 在實時任務上可用,則為 true 。對於使用實時寫入的數據源,通常會先設置成true
,然后隨着 segment 的發布和移交而變成false
。is_overshadowed
:如果該 segment 已發布(used
設置為 true)並且被其他一些已發布的 segment 完全覆蓋,則為 true。通常,這是一個過渡狀態,處於此狀態的 segment 很快就會將其used
標志自動設置為 false。
查詢處理
查詢首先進入Broker
進程,Broker
將得出哪些 segment 具有與該查詢有關的數據(segment 列表始終按時間規划,也可以根據其他屬性來規划,這取決於數據源的分區方式),然后,Broker
將確定哪些 Historical
和 MiddleManager
正在為這些 segment 提供服務,並將重寫的子查詢發送給每個進程。Historical
/ MiddleManager
進程將接受查詢,對其進行處理並返回結果。Broker
接收結果並將它們合並在一起以得到最終答案,並將其返回給客戶端。
Broker
會分析每個請求,優化查詢,盡可能的減少每個查詢必須掃描的數據量。相比於 Broker 過濾器做的優化,每個 segment 內的索引結構允許 Druid 在查看任何數據行之前先找出哪些行(如果有)與過濾器集匹配。一旦 Druid 知道哪些行與特定查詢匹配,它就只會訪問該查詢所需的特定列。在這些列中,Druid 可以在行與行之間跳過,從而避免讀取與查詢過濾器不匹配的數據。
因此,Druid 使用三種不同的技術來優化查詢性能:
-
檢索每個查詢需訪問的 segment。
-
在每個 segment 中,使用索引來標識查詢的行。
-
在每個 segment 中,僅讀取與特定查詢相關的行和列。
系列推薦
Mysql:小主鍵,大問題
Mysql大數據量問題與解決
你應該知道一些其他存儲——列式存儲
時間序列數據庫(TSDB)初識與選擇(InfluxDB、OpenTSDB、Druid、Elasticsearch對比)
十分鍾了解Apache Druid(集數據倉庫、時間序列、全文檢索於一體的存儲方案)
Apache Druid 底層存儲設計(列存儲與全文檢索)
Apache Druid 的集群設計與工作流程
想了解更多數據存儲,時間序列,Druid 的知識,可關注我的公眾號。點「在看」是我們創造好文的動力