Druid.io(以下簡稱Druid)是面向海量數據的、用于實時查詢與分析的OLAP存儲系統。Druid的四大關鍵特性總結如下:
亞秒級的OLAP查詢分析。Druid采用了列式存儲、倒排索引、位圖索引等關鍵技術,能夠在亞秒級別內完成海量數據的過濾、聚合以及多維分析等操作。
實時流數據分析。區別于傳統分析型數據庫采用的批量導入數據進行分析的方式,Druid提供了實時流數據分析,采用LSM(Long structure merge)-Tree結構使Druid擁有極高的實時寫入性能;同時實現了實時數據在亞秒級內的可視化。
豐富的數據分析功能。針對不同用戶群體,Druid提供了友好的可視化界面、類SQL查詢語言以及REST 查詢接口。
高可用性與高可拓展性。Druid采用分布式、SN(share-nothing)架構,管理類節點可配置HA,工作節點功能單一,不相互依賴,這些特性都使得Druid集群在管理、容錯、災備、擴容等方面變得十分簡單。
1 為什么會有Druid
大數據技術從最早的Hadoop項目開始已經有十多年的歷史了,而Druid是在2013年年底才開源的,雖然目前還不是Apache頂級項目,但是作為后起之秀,依然吸引了大量用戶的目光,社區也非?;钴S。那么,為什么會有Druid,而Druid又解決了傳統大數據處理框架下的哪些“痛點”問題,下面我們來一一解答。
大數據時代,如何從海量數據中提取有價值的信息,是一個亟待解決的難題。針對這個問題,IT巨頭們已經開發了大量的數據存儲與分析類產品,比如IBM Netezza、HP Vertica、EMC GreenPlum等,但是他們大多是昂貴的商業付費類產品,業內使用者寥寥。
而受益于近年來高漲的開源精神,業內出現了眾多優秀的開源項目,其中最有名的當屬Apache Hadoop生態圈。時至今日,Hadoop已經成為了大數據的“標準”解決方案,但是,人們在享受Hadoop便捷數據分析的同時,也必須要忍受Hadoop在設計上的許多“痛點”,下面就羅列三方面的問題:
何時能進行數據查詢?對于Hadoop使用的Map/Reduce批處理框架,數據何時能夠查詢沒有性能保證。
隨機IO問題。Map/Reduce批處理框架所處理的數據需要存儲在HDFS上,而HDFS是一個以集群硬盤作為存儲資源池的分布式文件系統,那么在海量數據的處理過程中,必然會引起大量的讀寫操作,此時隨機IO就成為了高并發場景下的性能瓶頸。
數據可視化問題。HDFS是一個優秀的分布式文件系統,但是對于數據分析以及數據的即席查詢,HDFS并不是最優的選擇。
傳統的大數據處理架構Hadoop更傾向于一種“后臺批處理的數據倉庫系統”,其作為海量歷史數據保存、冷數據分析,確實是一個優秀的通用解決方案,但是如何保證高并發環境下海量數據的查詢分析性能,以及如何實現海量實時數據的查詢分析與可視化,Hadoop確實顯得有些無能為力。
2 Druid直面痛點
Druid的母公司MetaMarket在2011年以前也是Hadoop的擁躉者,但是在高并發環境下,Hadoop并不能對數據可用性以及查詢性能給出產品級別的保證,使得MetaMarket必須去尋找新的解決方案,當嘗試使用了各種關系型數據庫以及NoSQL產品后,他們覺得這些已有的工具都不能解決他們的“痛點”,所以決定在2011年開始研發自己的“輪子”Druid,他們將Druid定義為“開源、分布式、面向列式存儲的實時分析數據存儲系統”,所要解決的“痛點”也是上文中反復提及的“在高并發環境下,保證海量數據查詢分析性能,同時又提供海量實時數據的查詢、分析與可視化功能”。
在介紹Druid架構之前,我們先結合有關OLAP的基本原理來理解Druid中的一些基本概念。
1 數據
以圖3.1為例,結合我們在第一章中介紹的OLAP基本概念,按列的類型上述數據可以分成以下三類:
時間序列(Timestamp),Druid既是內存數據庫,又是時間序列數據庫,Druid中所有查詢以及索引過程都和時間維度息息相關。Druid底層使用絕對毫秒數保存時間戳,默認使用ISO-8601格式展示時間(形如:yyyy-MM-ddThh:mm:sss.SSSZ,其中“Z”代表零時區,中國所在的東八區可表示為+08:00)。
維度列(Dimensions),Druid的維度概念和OLAP中一致,一條記錄中的字符類型(String)數據可看作是維度列,維度列被用于過濾篩選(filter)、分組(group)數據。如圖3.1中page、Username、Gender、City這四列。
度量列(Metrics),Druid的度量概念也與OLAP中一致,一條記錄中的數值(Numeric)類型數據可看作是度量列,度量列被用于聚合(aggregation)和計算(computation)操作。如圖3.1中的Characters Added、Characters Removed這兩列。
2 上卷
生產環境中,每天會有成百上千億的原始數據(raw data)進入到Druid中,Druid最小粒度支持毫秒級別的事件,但是在一般使用場景中,我們很少會關注如此細粒度的數據集,同時,對數據按一定規律進行聚合不僅可以節約存儲空間,亦可獲得更有價值的視圖。所以與其他OLAP類產品一樣,Druid也支持上卷(roll-up)操作。最常用的上卷操作是對時間維度進行聚合,比如對圖3.2中的數據按照小時粒度進行聚合可以得到圖3.3,圖3.3相對于圖3.2來說,顯得更加直觀,也更有助于分析人員掌握全局態勢。不過,上卷操作也會帶來信息量的丟失,因為上卷的粒度會變成最小數據可視化粒度,即毫秒級別的原始數據,如果按照分鐘粒度進行roll-up,那么入庫之后我們能夠查看數據的最小粒度即為分鐘級別。
3 分片
Druid是時間序列數據庫,也存在分片(Sharding)的概念。Druid對原始數據按照時間維度進行分片,每一個分片稱為段(Segment)。
Segment是Druid中最基本的數據存儲單元,采用列式(columnar)存儲某一個時間間隔(interval)內某一個數據源(dataSource)的部分數據所對應的所有維度值、度量值、時間維度以及索引。
Segment數據結構
時間維度(絕對毫秒數)和度量值在底層使用整數(Integer)或者浮點數(floating point)數組進行壓縮存儲,默認采用LZ4壓縮算法(可選LZF、uncompressed)。
維度列使用字典編碼、位圖索引以及相應壓縮算法,包含如下三種數據結構,以圖3.1中數據舉例:
為什么使用這三種數據結構,它們有哪些優勢:
使用字典編碼可以減少字符串數據的存儲空間,同時表達更加簡便、緊湊;
位圖索引,結構類似于倒排索引,可以快速地進行按位邏輯操作;
位圖索引尺寸=列基數 *數據行數,對于高基數列,我們在第二章中也詳細介紹了很多位圖索引壓縮算法,Druid中實現了Concisebitmap compression以及Roaring bitmap compression,默認使用Concise。
Segment存儲結構
Segment邏輯名稱形如“datasource_intervalStart_intervalEnd_version_partitionNum”,:
dataSource:數據源;
intervalStart、intervalEnd:時間間隔的起止,使用ISO-8601格式;
version:版本號,默認v1,用于區分多次加載同一數據對應的Segment;
partitionNumber:分區編號,在每個時間間隔內,根據數據量的大小一個Segment內部可能會有多個分區,官方推薦通過控制時間間隔粒度或者partition的個數來保證每個partition的大小在300Mb-700Mb之間,從而獲得最優的加載與查詢性能。
4 集群節點
Druid集群包含多種節點類型,分別是Historical Node、Coordinator Node、Broker Node、Indexing Service Node(包括Overlord、MiddleManager和Peon)以及Realtime Node(包括Firehose和Plumber)。
Druid將整個集群切分成上述角色,有兩個目的:第一,劃分Historical Node和Realtime Node,是將歷史數據的加載與實時流數據處理切割開來,因為二者都需要占用大量內存與CPU;第二,劃分Coordinator Node和Broker Node,將查詢需求與數據如何在集群內分布的需求切割開來,確保用戶的查詢請求不會影響數據在集群內的分布情況,從而不會造成數據“冷熱不均”,局部過熱,影響查詢性能的問題。
圖3.5給出了Druid集群內部的實時/批量數據流以及查詢請求過程。我們可以看到,實時數據到達Realtime Node,經過Indexing Service,在時間窗口內的數據會停留在Realtime Node內存中,而時間窗口外的數據會組織成Segment存儲到Deep Storage中;批量數據經過Indexing Service也會被組織成Segment存儲到Deep Storage中,同時Segment的元信息都會被注冊到元信息庫中,Coordinator Nodes會定期(默認為1分鐘)去同步元信息庫,感知新生成的Segment,并通知在線的Historical Node去加載Segment,Zookeeper也會更新整個集群內部數據分布拓撲圖。
當用戶需要查詢信息時,會將請求提交給Broker Node,Broker Node會請求Zookeeper獲取集群內數據分布拓撲圖,從而知曉請求應該發給哪些Historical Node以及Realtime Node,匯總各節點的返回數據并將最終結果返回給用戶。
在(三)中,我們將逐一介紹各類節點。
1 Historical Node
Historical Node的職責單一,就是負責加載Druid中非實時窗口內且滿足加載規則的所有歷史數據的Segment。每一個Historical Node只與Zookeeper保持同步,不與其他類型節點或者其他Historical Node進行通信。
根據上節知曉,Coordinator Nodes會定期(默認為1分鐘)去同步元信息庫,感知新生成的Segment,將待加載的Segment信息保存在Zookeeper中在線的Historical Nodes的load queue目錄下,當Historical Node感知到需要加載新的Segment時,首先會去本地磁盤目錄下查找該Segment是否已下載,如果沒有,則會從Zookeeper中下載待加載Segment的元信息,此元信息包括Segment存儲在何處、如何解壓以及如何如理該Segment。Historical Node使用內存文件映射方式將index.zip中的XXXXX.smoosh文件加載到內存中,并在Zookeeper中本節點的served segments目錄下聲明該Segment已被加載,從而該Segment可以被查詢。對于重新上線的Historical Node,在完成啟動后,也會掃描本地存儲路徑,將所有掃描到的Segment加載如內存,使其能夠被查詢。
2 Broker Node
Broker Node是整個集群查詢的入口,作為查詢路由角色,Broker Node感知Zookeeper上保存的集群內所有已發布的Segment的元信息,即每個Segment保存在哪些存儲節點上,Broker Node為Zookeeper中每個dataSource創建一個timeline,timeline按照時間順序描述了每個Segment的存放位置。我們知道,每個查詢請求都會包含dataSource以及interval信息,Broker Node根據這兩項信息去查找timeline中所有滿足條件的Segment所對應的存儲節點,并將查詢請求發往對應的節點。
對于每個節點返回的數據,Broker Node默認使用LRU緩存策略;對于集群中存在多個Broker Node的情況,Druid使用memcached共享緩存。對于Historical Node返回的結果,Broker Node認為是“可信的”,會緩存下來,而Real-Time Node返回的實時窗口內的數據,Broker Node認為是可變的,“不可信的”,故不會緩存。所以對每個查詢請求,Broker Node都會先查詢本地緩存,如果不存在才會去查找timeline,再向相應節點發送查詢請求。
3 Coordinator Node
Coordinator Node主要負責Druid集群中Segment的管理與發布,包括加載新Segment、丟棄不符合規則的Segment、管理Segment副本以及Segment負載均衡等。如果集群中存在多個Coordinator Node,則通過選舉算法產生Leader,其他Follower作為備份。
Coordinator會定期(默認一分鐘)同步Zookeeper中整個集群的數據拓撲圖、元信息庫中所有有效的Segment信息以及規則庫,從而決定下一步應該做什么。對于有效且未分配的Segment,Coordinator Node首先按照Historical Node的容量進行倒序排序,即最少容量擁有最高優先級,新的Segment會優先分配到高優先級的Historical Node上。由3.3.4.1節可知,Coordinator Node不會直接與Historical Node打交道,而是在Zookeeper中Historical Node對應的load queue目錄下創建待加載Segment的臨時信息,等待Historical Node去加載該Segment。
Coordinator在每次啟動后都會對比Zookeeper中保存的當前數據拓撲圖以及元信息庫中保存的數據信息,所有在集群中已被加載的、卻在元信息庫中標記為失效或者不存在的Segment會被Coordinator Node記錄在remove list中,其中也包括我們在3.3.3節中所述的同一Segment對應的新舊version,舊version的Segments同樣也會被放入到remove list中,最終被邏輯丟棄。
對于離線的Historical Node,Coordinator Node會默認該Historical Node上所有的Segment已失效,從而通知集群內的其他Historical Node去加載該Segment。但是,在生產環境中,我們會遇到機器臨時下線,Historical Node在很短時間內恢復服務的情況,那么如此“簡單粗暴”的策略勢必會加重整個集群內的網絡負載。對于這種場景,Coordinator會為集群內所有已丟棄的Segment保存一個生存時間(lifetime),這個生存時間表示Coordinator Node在該Segment被標記為丟棄后,允許不被重新分配最長等待時間,如果該Historical Node在該時間內重新上線,則Segment會被重新置為有效,如果超過該時間則會按照加載規則重新分配到其他Historical Node上。
考慮一種最極端的情況,如果集群內所有的Coordinator Node都停止服務,整個集群對外依然有效,不過新Segment不會被加載,過期的Segment也不會被丟棄,即整個集群內的數據拓撲會一直保持不變,直到新的Coordinator Node服務上線。
4 Indexing Service
Indexing Service是負責“生產”Segment的高可用、分布式、Master/Slave架構服務。主要由三類組件構成:負責運行索引任務(indexing task)的Peon,負責控制Peon的MiddleManager,負責任務分發給MiddleManager的Overlord;三者的關系可以解釋為:Overlord是MiddleManager的Master,而MiddleManager又是Peon的Master。其中,Overlord和MiddleManager可以分布式部署,但是Peon和MiddleManager默認在同一臺機器上。圖3.5給出了Indexing Service的整體架構。
Overlord
Overlord負責接受任務、協調任務的分配、創建任務鎖以及收集、返回任務運行狀態給調用者。當集群中有多個Overlord時,則通過選舉算法產生Leader,其他Follower作為備份。
Overlord可以運行在local(默認)和remote兩種模式下,如果運行在local模式下,則Overlord也負責Peon的創建與運行工作,當運行在remote模式下時,Overlord和MiddleManager各司其職,根據圖3.6所示,Overlord接受實時/批量數據流產生的索引任務,將任務信息注冊到Zookeeper的/task目錄下所有在線的MiddleManager對應的目錄中,由MiddleManager去感知產生的新任務,同時每個索引任務的狀態又會由Peon定期同步到Zookeeper中/Status目錄,供Overlord感知當前所有索引任務的運行狀況。
Overlord對外提供可視化界面,通過訪問http://:/console.html,我們可以觀察到集群內目前正在運行的所有索引任務、可用的Peon以及近期Peon完成的所有成功或者失敗的索引任務。
MiddleManager
MiddleManager負責接收Overlord分配的索引任務,同時創建新的進程用于啟動Peon來執行索引任務,每一個MiddleManager可以運行多個Peon實例。
在運行MiddleManager實例的機器上,我們可以在${ java.io.tmpdir}目錄下觀察到以XXX_index_XXX開頭的目錄,每一個目錄都對應一個Peon實例;同時restore.json文件中保存著當前所有運行著的索引任務信息,一方面用于記錄任務狀態,另一方面如果MiddleManager崩潰,可以利用該文件重啟索引任務。
Peon
Peon是Indexing Service的最小工作單元,也是索引任務的具體執行者,所有當前正在運行的Peon任務都可以通過Overlord提供的web可視化界面進行訪問。
5 Real-Time Node
在流式處理領域,有兩種數據處理模式,一種為Stream Push,另一種為Stream Pull。
Stream Pull
如果Druid以Stream Pull方式自主地從外部數據源拉取數據從而生成Indexing Service Tasks,我們則需要建立Real-Time Node。Real-Time Node主要包含兩大“工廠”:一個是連接流式數據源、負責數據接入的Firehose(中文翻譯為水管,很形象地描述了該組件的職責);另一個是負責Segment發布與轉移的Plumber(中文翻譯為搬運工,同樣也十分形象地描述了該組件的職責)。在Druid源代碼中,這兩個組件都是抽象工廠方法,使用者可以根據自己的需求創建不同類型的Firehose或者Plumber。Firehose和Plumber給我的感覺,更類似于Kafka_0.9.0版本后發布的Kafka Connect框架,Firehose類似于Kafka Connect Source,定義了數據的入口,但并不關心接入數據源的類型;而Plumber類似于Kafka Connect Sink,定義了數據的出口,也不關心最終輸出到哪里。
Stream Push
如果采用Stream Push策略,我們需要建立一個“copy service”,負責從數據源中拉取數據并生成Indexing Service Tasks,從而將數據“推入”到Druid中,我們在druid_0.9.1版本之前一直使用的是這種模式,不過這種模式需要外部服務Tranquility,Tranquility組件可以連接多種流式數據源,比如Spark-Streaming、Storm以及Kafka等,所以也產生了Tranquility-Storm、Tranquility-Kafka等外部組件。Tranquility-Kafka的原理與使用將在3.4節中進行詳細介紹。
6 外部拓展
Druid集群依賴一些外部組件,與其說依賴,不如說正是由于Druid開放的架構,所以用戶可以根據自己的需求,使用不同的外部組件。
Deep Storage
Druid目前支持使用本地磁盤(單機模式)、NFS掛載磁盤、HDFS、Amazon S3等存儲方式保存Segments以及索引任務日志。
Zookeeper
Druid使用Zookeeper作為分布式集群內部的通信組件,各類節點通過Curator Framework將實例與服務注冊到Zookeeper上,同時將集群內需要共享的信息也存儲在Zookeeper目錄下,從而簡化集群內部自動連接管理、leader選舉、分布式鎖、path緩存以及分布式隊列等復雜邏輯。
Metadata Storage
Druid集群元信息使用MySQL 或者PostgreSQL存儲,單機版使用derby。在Druid_0.9.1.1版本中,元信息庫druid主要包含十張表,均以“druid_”開頭,如圖3.7所示。
7 加載數據
對于加載外部數據,Druid支持兩種模式:實時流(real-time ingestion)和批量導入(batch ingestion)。
Real-Time Ingestion
實時流過程可以采用Apache Storm、Apache Spark Streaming等流式處理框架產生數據,再經過pipeline工具,比如Apache Kafka、ActiveMQ、RabbitMQ等消息總線類組件,使用Stream Pull 或者Stream Push模式生成Indexing Service Tasks,最終存儲在Druid中。
Batch Ingestion
批量導入模式可以采用結構化信息作為數據源,比如JSON、Avro、Parquet格式的文本,Druid內部使用Map/Reduce批處理框架導入數據。
8 高可用性
Druid高可用性可以總結以下幾點:
Historical Node
如3.3.4.1節中所述,如果某個Historical Node離線時長超過一定閾值,Coordinator Node會將該節點上已加載的Segments重新分配到其他在線的Historical Nodes上,保證滿足加載規則的所有Segments不丟失且可查詢。
Coordinator Node
集群可配置多個Coordinator Node實例,工作模式為主從同步,采用選舉算法產生Leader,其他Follower作為備份。當Leader宕機時,其他Follower能夠迅速failover。
即使當所有Coordinator Node均失效,整個集群對外依然有效,不過新Segments不會被加載,過期的Segments也不會被丟棄,即整個集群內的數據拓撲會一直保持不變,直到新的Coordinator Node服務上線。
Broker Node
Broker Node與Coordinator Node在HA部署方面一致。
Indexing Service
Druid可以為同一個Segment配置多個Indexing Service Tasks副本保證數據完整性。
Real-Time
Real-Time過程的數據完整性主要由接入的實時流語義(semantics)決定。我們在0.9.1.1版本前使用Tranquility-Kafka組件接入實時數據,由于存在時間窗口,即在時間窗口內的數據會被提交給Firehose,時間窗口外的數據則會被丟棄;如果Tranquility-Kafka臨時下線,會導致Kafka中數據“過期”從而被丟棄,無法保證數據完整性,同時這種“copy service”的使用模式不僅占用大量CPU與內存,又不滿足原子操作,所以在0.9.1.1版本后,我們使用Druid的新特性Kafka Indexing Service,Druid內部使用Kafka高級Consumer API保證exactly-once semantics,盡最大可能保證數據完整性。不過我們在使用中,依然發現有數據丟失問題。
Metadata Storage
如果Metadata Storage失效,Coordinator則無法感知新Segment的生成,整個集群中數據拓撲亦不會改變,不過不會影響老數據的訪問。
Zookeeper
如果Zookeeper失效,整個集群的數據拓撲不會改變,由于Broker Node緩存的存在,所以在緩存中的數據依然可以被查詢。
9 數據分層
Druid訪問控制策略采用數據分層(tier),有以下兩種用途:
將不同的Historical Node劃分為不同的group,從而控制集群內不同權限(priority)用戶在查詢時訪問不同group。
通過劃分tier,讓Historical Node加載不同時間范圍的數據。例如tier_1加載2016年Q1數據,tier_2加載2016年Q2數據,tier_3加載2016年Q3數據等;那么根據用戶不同的查詢需求,將請求發往對應tier的Historical Node,不僅可以控制用戶訪問請求,同時也可以減少響應請求的Historical Node數量,從而加速查詢。