1. 程式人生 > >5-大資料分析之 druid 介紹

5-大資料分析之 druid 介紹

Druid (大資料實時統計分析資料儲存)

摘要

Druid是一個為在大資料集之上做實時統計分析而設計的開源資料儲存。這個系統集合了一個面向列儲存的層,一個分散式、shared-nothing的架構,和一個高階的索引結構,來達成在秒級以內對十億行級別的表進行任意的探索分析。在這篇論文裡面,我們會描述Druid的架構,和怎樣支援快速聚合、靈活的過濾、和低延遲資料匯入的一些細節。

1. 介紹

在最近幾年,網際網路技術的快速增長已經產生了大量由機器產生的資料。單獨來看,這些資料包含很少的有用資訊,價值都是很低的。從這些巨大的資料裡面分析出有用的資訊需要大量的時間和資源,很多公司都選擇了放棄這些資料。雖然已有基礎設施來處理這些居於事件的資料(例如IBM的Netezza,惠普的Vertica,EMC的Green-plum),但它們大多以高價售賣,那些負擔得起的公司才是他們的目標客戶。

幾年前,Google推出了MapReduce,他們利用普通硬體來索引網際網路和分析日誌的機制。在原始的MapReduce論文公佈之後,Hadoop很快就被大量的跟隨和作為參考。Hadoop現在被很多組織機構部署來用於儲存和分析大規模的日誌資料。Hadoop很大的貢獻在於幫助企業將他們那些低價值的事件流資料轉化為高價值的聚合資料,這適用於各種應用,例如商業智慧和AB測試。

和許多偉大的系統一樣,Hadoop開闊了我們處理問題的視野。然而,Hadoop擅長的是儲存和獲取大規模資料,但是它並不提供任何效能上的保證它能多快獲取到資料。此外,雖然Hadoop是一個高可用的系統,但是在高併發負載下效能會下降。最後,Hadoop對於儲存資料可以工作得很好,但是並沒有對資料匯入進行優化,使匯入的資料立即可讀。

早在Metamarkets的產品開發過程中,我們遇上了所有這些問題,並意識到Hadoop是一個很好的後端、批量處理和資料倉庫系統。然而,作為一個需要在高併發環境下(1000+使用者)保證查詢效能和資料可用性的公司,並需要提供產品級別的保證,Hadoop並不能滿足我們的需求。我們在宇宙中探索了不同的解決方案,在嘗試了關係型資料庫管理系統和NoSQL架構後,我們得出了一個結論,就是在開源的世界裡,並沒有可以充分滿足我們需求的系統。最後我們建立了Druid,一個開源的、分散式、列儲存、實時分析的資料儲存。在許多方面,Druid和其他OLAP系統有很多相似之處,互動式查詢系統,記憶體資料庫(MMDB),眾所周知的分散式資料儲存。其中的分散式和查詢模型都參考了當前的一些搜尋引擎的基礎架構。

本文介紹了Druid的架構,探討了建立一個用於生產環境並保持永遠線上的託管服務所做的各種設計決策,並試圖幫助任何一位面臨類似問題的人提供一個可能的解決方案。Druid已經在好幾個技術公司的生產環境中進行了部署。本文的結構如下:我們首先在第2節描述面臨的問題,接著我們在第3節詳細介紹系統的架構,說明資料在系統裡面是怎樣流通的,然後會在第4節討論資料為什麼和怎麼樣轉換為二進位制格式,第5節會簡要介紹下查詢介面,第6節會介紹下現有的一些效能結果,最後,我們在第7節說明怎樣將Druid運行於生產環境,第8節介紹下一些相關的工作。

2. 問題定義

Druid的最初目的是設計來解決匯入和分析大規模交易事件(日誌資料)。這種時間序列形式的資料通常在OALP類工作流中比較常見,且資料的本質是非常重的追加寫。

表1: 在Wikipedia由編輯產生的Druid示例資料

Timestamp Page Username Gender City Characters Added Characters Removed
2011-01-01T01:00:00Z Justin Bieber Boxer Male San Francisco 1800 25
2011-01-01T01:00:00Z Justin Bieber Reach Male Waterloo 2912 42
2011-01-01T02:00:00Z Ke$ha Helz Male Calgary 1953 17
2011-01-01T02:00:00Z Ke$ha Xeno Male Taiyuan 3194 170

例如,考慮下表1包含的資料,表1包含了在Wikipedia編輯而產生的資料。每當使用者編輯一個Wikipedia的頁面的時候,就會產生一條關於編輯的包含了元資料的事件資料,這個元資料包含了3個不同的部分。首先,有一個timestamp列指示編輯的時間。然後,還有一組維度列(dimension)表明關於編輯的各種屬性,例如被編輯的頁面、由誰編輯的、編輯使用者的位置。最後,還有一組包含值的(通常是數字)、可以被聚合計算的指標列(metric),例如在編輯中新增或刪除的字元個數。

我們的目標是在這個資料之上做快速的鑽取(drill-downs)和聚合計算,我們希望回答之如“編輯賈斯汀·比伯這個頁面的編輯者中有多少是來自於舊金山的男性?” 和 “最近一個月中由來自於Calgary的人新增的平均字元數是多少?”。我們也希望可以以任意組合維度來查詢並在秒級以內返回資料。

之所以需要Druid,是因為現實情況是現有的開源關係型資料庫(RDBMS)和NoSQL key/value 資料庫沒辦法為一些互動式應用提供低延遲的資料匯入和查詢。在Metamarkets的早期,我們的重點是建立一個託管的儀表板,允許使用者以視覺化的方式任意地去瀏覽事件流資料。支撐這個儀表板的資料儲存需要以足夠快的速度返回查詢結果,在這之上的資料視覺化才可以給使用者提供良好的使用者體驗。

除了查詢響應時間的要求外,該系統還必須是多租戶和高可用的。Metamarkets的產品是用於高併發的環境中,停機成本是昂貴的,而且許多企業都沒法承受系統不可用時的等待,即便是軟體升級或者是網路故障。停機時間於創業公司來說,特別是那些缺乏適當的內部運維管理的,是可以決定一個公司的成敗的。

最後,另外一個Metamarkets成立之初面臨的一個挑戰是允許使用者和報警系統可以“實時”地做商業決策。從一個事件資料被建立,到這個事件資料可以被查詢的時間,決定了利益相關方能夠在他們的系統出現潛在災難性情況時多快做出反應。流行的開源資料倉庫系統,例如Hadoop,並不能達到我們所需要的秒級的資料匯入和查詢的要求。

資料匯入、分析和可用性這些問題存在於多個行業中,自從Druid在2012年10月開源以來,它被作為視訊、網路監控,運營監控和廣告分析平臺部署到多家公司。

3. 架構

一個Druid叢集包含不同型別的節點,而每種節點都被設計來做好某組事情。我們相信這樣的設計可以隔離關注並簡化整個系統的複雜度。不同節點的運轉幾乎都是獨立的並且和其他的節點有著最小化的互動,因此叢集內的通訊故障對於資料可用性的影響微乎其微。

為了解決複雜的資料分析問題,把不同型別的節點組合在一起,就形成了一個完整的系統。Druid這個名字來自於Druid類的角色扮演遊戲。Druid叢集的構成和資料流向如圖1所示。

Druid叢集概覽和內部資料流向

圖1. Druid叢集概覽和內部資料流向

3.1 實時節點

實時節點封裝了匯入和查詢事件資料的功能,經由這些節點匯入的事件資料可以立刻被查詢。實時節點只關心一小段時間內的事件資料,並定期把這段時間內收集的這批不可變事件資料匯入到Druid叢集裡面另外一個專門負責處理不可變的批量資料的節點中去。實時節點通過Zookeeper的協調和Druid叢集的其他節點協調工作。實時節點通過Zookeeper來宣佈他們的線上狀態和他們提供的資料。

實時節點為所有傳入的事件資料維持一個記憶體中的索引快取。隨著事件資料的傳入,這些索引會逐步遞增,並且這些索引是可以立即查詢的。查詢這些緩存於JVM的基於堆的快取中的事件資料,Druid就表現得和行儲存一樣。為了避免堆溢位問題,實時節點會定期地、或者在達到設定的最大行限制的時候,把記憶體中的索引持久化到磁碟去。這個持久化程序會把保存於記憶體快取中的資料轉換為基於列儲存的格式,這個行儲存相關的會在第4節介紹。所有持久化的索引都是不可變的,並且實時節點會載入這些索引到off-heap記憶體中使得它們可以繼續被查詢。這個過程會在【33】引用文獻中詳細說明並且如圖2所示。

實時節點載入持久化索引
圖2. 實時節點快取事件資料到記憶體中的索引上,然後有規律的持久化到磁碟上。在轉移之前,持久化的索引會週期性地合併在一起。查詢會同時命中記憶體中的和已持久化的索引。

所有的實時節點都會週期性的啟動後臺的計劃任務搜尋本地的持久化索引,後臺計劃任務將這些持久化的索引合併到一起並生成一塊不可變的資料,這些資料塊包含了一段時間內的所有已經由實時節點匯入的事件資料,我們稱這些資料塊為”Segment”。在傳送階段,實時節點將這些segment上傳到一個永久持久化的備份儲存中,通常是一個分散式檔案系統,例如S3或者HDFS,Druid稱之為”Deep Storage”。匯入、持久化、合併和傳送這些階段都是流動的,並且在這些處理階段中不會有任何資料的丟失。

實時節點處理流程
圖3. 節點開始、匯入資料、持久化與定期傳送資料。這些處理程序無限迴圈。不同的實時節點處理流程間的時間是可配置的。

圖3說明了實時節點的各個處理流程。節點啟動於13:47,並且只會接受當前小時和下一小時的事件資料。當事件資料開始匯入後,節點會宣佈它為13:0014:00這個時間段的Segment資料提供服務。每10分鐘(這個時間間隔是可配置的),節點會將記憶體中的快取資料刷到磁碟中進行持久化,在當前小時快結束的時候,節點會準備接收14:0015:00的事件資料,一旦這個情況發生了,節點會準備好為下一個小時提供服務,並且會建立一個新的記憶體中的索引。隨後,節點宣佈它也為14:0015:00這個時段提供一個segment服務。節點並不是馬上就合併13:0014:00這個時段的持久化索引,而是會等待一個可配置的視窗時間,直到所有的13:0014:00這個時間段的一些延遲資料的到來。這個視窗期的時間將事件資料因延遲而導致的資料丟失減低到最小。在視窗期結束時,節點會合並13:0014:00這個時段的所有持久化的索引合併到一個獨立的不可變的segment中,並將這個segment傳送走,一旦這個segment在Druid叢集中的其他地方載入了並可以查詢了,實時節點會重新整理它收集的13:0014:00這個時段的資料的資訊,並且宣佈取消為這些資料提供服務。

3.1.1 可用性與可擴充套件性

實時節點是一個數據的消費者,需要有相應的生產商為其提供資料流。通常,為了資料耐久性的目的,會在生產商與實時節點間放置一個類似於Kafka這樣的訊息匯流排來進行連線,如圖4所示。實時節點通過從訊息總線上讀取事件資料來進行資料的匯入。從事件資料的建立到事件資料被消費掉通常是在幾百毫秒這個級別。

Kafka訊息匯流排
圖4. 多個實時節點可以從同一個訊息匯流排進行讀取。每個節點維護自身的偏移量

圖4中訊息匯流排的作用有兩個。首先,訊息匯流排作為傳入資料的緩衝區。類似於Kafka這樣的訊息匯流排會維持一個指示當前消費者(實時節點)從事件資料流中已經讀取資料的位置偏移量,消費者可以通過程式設計的方式更新偏移量。實時節點每次持久化記憶體中的快取到磁碟的時候,都會更新這個偏移量。在節點掛掉和恢復的情況下,如果節點沒有丟失磁碟資料,節點可以重新載入磁碟中所有持久化的索引資料,並從最後一次提交的偏移位置開始繼續讀取事件資料。從最近提交的偏移位置恢復資料大大減少了資料的恢復時間,在實踐中,我們可以看到節點從故障中恢復僅用了幾秒鐘時間。

訊息匯流排的另外一個目的就是可以讓多個實時節點可以從同一個單一的端點讀取資料。多個實時節點可以從資料匯流排匯入同一組資料,為資料建立一個副本。這樣當一個節點完全掛掉並且磁碟上的資料也丟失了,副本可以確保不會丟失任何資料。統一的單一的資料匯入端點也允許對資料進行分片,這樣多個實時節點時每個節點就可以只匯入一部分的資料,這允許無縫地進行實時節點的新增。在實踐中,這個模型已經讓一個生產環境中最大的Druid叢集消費原始資料的速度大約達到500MB/S(150,000條/秒 或者 2TB/小時)。

3.2 歷史節點

歷史節點封裝了載入和處理由實時節點建立的不可變資料塊(segment)的功能。在很多現實世界的工作流程中,大部分匯入到Druid叢集中的資料都是不可變的,因此,歷史節點通常是Druid叢集中的主要工作元件。歷史節點遵循shared-nothing的架構,因此節點間沒有單點問題。節點間是相互獨立的並且提供的服務也是簡單的,它們只需要知道如果載入、刪除和處理不可變的segment。

類似於實時節點,歷史節點在Zookeeper中通告它們的線上狀態和為哪些資料提供服務。載入和刪除segment的指令會通過Zookeeper來進行釋出,指令會包含segment儲存在deep storage的什麼地方和怎麼解壓、處理這些segment的相關資訊。在歷史節點從deep storage下載某一segment之前,它會先檢查本地快取資訊中看segment是否已經存在於節點中,如果segment還不存在快取中,歷史節點會從deep storage中下載segment到本地。這個處理過程如圖5所示,一旦處理完成,這個segment就會在Zookeeper中進行通告。此時,這個segment就可以被查詢了。歷史節點的本地快取也支援歷史節點的快速更新和重啟,在啟動的時候,該節點會檢查它的快取,併為任何它找到的資料立刻進行服務的提供。

歷史節點從Deep Storage載入資料
圖5. 歷史節點從deep storage下載不可變的segment。segment在可以被查詢之前必須要先載入到記憶體中

歷史節點可以支援讀一致性,因為它們只處理不可變的資料。不可變的資料塊同時支援一個簡單的並行模型:歷史節點可以以非阻塞的方式併發地去掃描和聚合不可變的資料塊。

3.2.1 Tiers

歷史節點可以分組到不同的tier中,哪些節點會被分到一個tier中是可配置的。可以為不同的tier配置不同的效能和容錯引數。Tier的目的是可以根據segment的重要程度來分配高或低的優先順序來進行資料的分佈。例如,可以使用一批很多個核的CPU和大容量記憶體的節點來組成一個“熱點資料”的tier,這個“熱點資料”叢集可以配置來用於下載更多經常被查詢的資料。一個類似的”冷資料”叢集可以使用一些效能要差一些的硬體來建立,“冷資料”叢集可以只包含一些不是經常訪問的segment。

3.2.2 可用性

歷史節點依賴於Zookeeper來管理segment的載入和解除安裝。如果Zookeeper變得不可用的時候,歷史節點就不再可以為新的資料提供服務和解除安裝過期的資料,因為是通過HTTP來為查詢提供服務的,對於那些查詢它當前已經在提供服務的資料,歷史節點仍然可以進行響應。這意味著Zookeeper執行故障時不會影響那些已經存在於歷史節點的資料的可用性。

3.3 Broker節點

Broker節點扮演著歷史節點和實時節點的查詢路由的角色。Broker節點知道釋出於Zookeeper中的關於哪些segment是可查詢的和這些segment是儲存在哪裡的,Broker節點就可以將到來的查詢請求路由到正確的歷史節點或者是實時節點,Broker節點也會將歷史節點和實時節點的區域性結果進行合併,然後返回最終的合併後的結果給呼叫者。

3.3.1 快取

Broker節點包含一個支援LRU失效策略的快取。這個快取可以使用本地堆記憶體或者是一個外部的分散式 key/value 儲存,例如Memcached。每次Broker節點接收到查詢請求時,都會先將查詢對映到一組segment中去。這一組確定的segment的結果可能已經存在於快取中,而不需要重新計算。對於那些不存在於快取的結果,Broker節點會將查詢轉發到正確的歷史節點和實時節點中去,一旦歷史節點返回結果,Broker節點會將這些結果快取起來以供以後使用,這個過程如圖6所示。實時資料永遠不會被快取,因此查詢實時節點的資料的查詢請求總是會被轉發到實時節點上去。實時資料是不斷變化的,因此快取實時資料是不可靠的。

Broker節點的快取
圖6. 結果會為每一個segment快取。查詢會合並快取結果與歷史節點和實時節點的計算結果

快取也可作為資料可用性的附加級別。在所有歷史節點都出現故障的情況下,對於那些命中已經在快取中快取了結果的查詢,仍然是可以返回查詢結果的。

3.3.2 可用性

在所有的Zookeeper都中斷的情況下,資料仍然是可以查詢的。如果Broker節點不可以和Zookeeper進行通訊了,它會使用它最後一次得到的整個叢集的檢視來繼續將查詢請求轉發到歷史節點和實時節點,Broker節點假定叢集的結構和Zookeeper中斷前是一致的。在實踐中,在我們診斷Zookeeper的故障的時候,這種可用性模型使得Druid叢集可以繼續提供查詢服務,為我們爭取了更多的時間。

3.4 協調節點

Druid的協調節點主要負責資料的管理和在歷史節點上的分佈。協調節點告訴歷史節點載入新資料、解除安裝過期資料、複製資料、和為了負載均衡移動資料。Druid為了維持穩定的檢視,使用一個多版本的併發控制交換協議來管理不可變的segment。如果任何不可變的segment包含的資料已經被新的segment完全淘汰了,則過期的segment會從叢集中解除安裝掉。協調節點會經歷一個leader選舉的過程,來決定由一個獨立的節點來執行協調功能,其餘的協調節點則作為冗餘備份節點。

協調節點會週期性的執行,以確定叢集的當前狀態。它通過在執行的時候對比叢集的預期狀態和叢集的實際狀態來做決定。和所有的Druid節點一樣,協調節點維持一個和Zookeeper的連線來獲取當前叢集的資訊。同時協調節點也維持一個與MySQL資料庫的連線,MySQL包含有更多的操作引數和配置資訊。其中一個存在於MySQL的關鍵資訊就是歷史節點可以提供服務的所有segment的一個清單,這個表可以由任何可以建立segment的服務進行更新,例如實時節點。MySQL資料庫中還包含一個Rule表來控制叢集中segment的是如何建立、銷燬和複製。

3.4.1 Rules

Rules管理歷史segment是如何在叢集中載入和解除安裝的。Rules指示segment應該如何分配到不同的歷史節點tier中,每一個tier中應該儲存多少份segment的副本。Rules還可能指示segment何時應該從叢集中完全地解除安裝。Rules通常設定為一段時間,例如,一個使用者可能使用Rules來將最近一個月的有價值的segment載入到一個“熱點資料”的叢集中,最近一年的有價值的資料載入到一個“冷資料”的叢集中,而將更早時間前的資料都解除安裝掉。

協調節點從MySQL資料庫中的rule表載入一組rules。Rules可能被指定到一個特定的資料來源,或者配置一組預設的rules。協調節點會迴圈所有可用segment並會匹配第一條適用於它的rule。

3.4.2 負載均衡

在典型的生產環境中,查詢通常命中數打甚至上百個segment,由於每個歷史節點的資源是有限的,segment必須被分佈到整個叢集中,以確保叢集的負載不會過於不平衡。要確定最佳的負載分佈,需要對查詢模式和速度有一定的瞭解。通常,查詢會覆蓋一個獨立資料來源中最近的一段鄰近時間的一批segment。平均來說,查詢更小的segment則更快。

這些查詢模式提出以更高的比率對歷史segment進行復制,把大的segment以時間相近的形式分散到多個不同的歷史節點中,並且使存在於不同資料來源的segment集中在一起。為了使叢集中segment達到最佳的分佈和均衡,我們根據segment的資料來源、新舊程度、和大小,開發了一個基於成本的優化程式。該演算法的具體細節超出了本文的範疇,我們可能會在將來的文獻中進行討論。

3.4.3 副本/複製(Replication)

協調節點可能會告訴不同的歷史節點載入同一個segment的副本。每一個歷史節點tier中副本的數量是完全可配置。設定一個高級別容錯性的叢集可以設定一個比較高數量的副本數。segment的副本被視為和原始segment一樣的,並使用相同的負載均衡演算法。通過複製segment,單一歷史節點故障對於整個Druid叢集來說是透明的,不會有任何影響。我們使用這個特性來進行軟體升級。我們可以無縫地將一個歷史節點下線,更新它,再啟動回來,然後將這個過程在叢集中所有歷史節點上重複。在過去的兩年中,我們的Druid叢集從來沒有因為軟體升級而出現過停機。

3.4.4 可用性

Druid的協調節點有Zookeeper和MySQL這兩個額外的依賴,協調節點依賴Zookeeper來確定叢集中有哪些歷史節點。如果Zookeeper變為不可用,協調節點將不可以再進行segment的分配、均衡和解除安裝指令的傳送。不過,這些都不會影響資料的可用性。

對於MySQL和Zookeeper響應失效的設計原則是一致的:如果協調節點一個額外的依賴響應失敗了,叢集會維持現狀。Druid使用MySQL來儲存操作管理資訊和關於segment如何存在於叢集中的segment元資料。如果MySQL下線了,這些資訊就在協調節點中變得不可用,不過這不代表資料不可用。如果協調節點不可以和MySQL進行通訊,他們會停止分配新的segment和解除安裝過期的segment。在MySQL故障期間Broker節點、歷史節點、實時節點都是仍然可以查詢的。

4. 儲存格式

Druid中的資料表(稱為資料來源)是一個時間序列事件資料的集合,並分割到一組segment中,而每一個segment通常是0.5-1千萬行。在形式上,我們定義一個segment為跨越一段時間的資料行的集合。Segment是Druid裡面的基本儲存單元,複製和分佈都是在segment基礎之上進行的。

Druid總是需要一個時間戳的列來作為簡化資料分佈策略、資料保持策略、與第一級查詢剪支(first-level query pruning)的方法。Druid分隔它的資料來源到明確定義的時間間隔中,通常是一個小時或者一天,或者進一步的根據其他列的值來進行分隔,以達到期望的segment大小。segment分隔的時間粒度是一個數據大小和時間範圍的函式。一個超過一年的資料集最好按天分隔,而一個超過一天的資料集則最好按小時分隔。

Segment是由一個數據源識別符號、資料的時間範圍、和一個新segment建立時自增的版本字串來組合起來作為唯一識別符號。版本字串表明了segment的新舊程度,高版本號的segment的資料比低版本號的segment的資料要新。這些segment的元資料用於系統的併發控制,讀操作總是讀取特定時間範圍內有最新版本識別符號的那些segment。

Druid的segment儲存在一個面向列的儲存中。由於Druid是適用於聚合計算事件資料流(所有的資料進入到Druid中都必須有一個時間戳),使用列式來儲存聚合資訊比使用行儲存更好這個是[有據可查][1]的。列式儲存可以有更好的CPU利用率,只需載入和掃描那些它真正需要的資料。而基於行的儲存,在一個聚合計算中相關行中所有列都必須被掃描,這些附加的掃描時間會引起效能惡化。

Druid有多種列型別來表示不同的資料格式。根據列的型別,會使用不同的壓縮演算法來降低一個列儲存在記憶體和磁碟的成本。在表1提供的示例中,page, user, gender, 和 city 列都只包含字串,直接儲存字串的成本很高而且沒有必要,可以使用字典編碼(Dictionary encoding)來代替。字典編碼是一個常用的資料壓縮演算法,也已經用在類似[PowerDrill][17]這樣的資料儲存上。在表1的示例中,我們可以將每一個page對映到一個唯一的整數識別符號上。

Justin Bieber -> 0
Ke$ha -> 1

這個對映關係允許我們使用一個整數陣列來表示page列,這個陣列索引了原始資料集的相應的行。對於page列,我們可以用以下的方式來表示:

[0, 0, 1, 1]

這個整數陣列結果使得它可以很好的應用壓縮演算法。在編碼的基礎上使用常用的壓縮演算法在列式儲存中很常見。Druid使用的[LZF][24]壓縮演算法。類似的壓縮演算法也可以應用於數字列,例如,表1中增加的字元數和刪除的字元數這兩列也可以使用獨立的陣列來表示:

Characters Added   -> [1800, 2912, 1953, 3194]
Characters Removed -> [25, 42, 17, 170]

在這種情況下,我們以和它們字典描述相反的方式來壓縮這些原始值。

4.1 索引過濾資料

In many real world OLAP workflows, queries are issued for the aggregated results of some set of metrics where some set of di- mension specifications are met. An example query is: “How many Wikipedia edits were done by users in San Francisco who are also male?” This query is filtering the Wikipedia data set in Table 1 based on a Boolean expression of dimension values. In many real world data sets, dimension columns contain strings and metric columns contain numeric values. Druid creates additional lookup indices for string columns such that only those rows that pertain to a particular query filter are ever scanned.
Let us consider the page column in Table 1. For each unique page in Table 1, we can form some representation indicating in which table rows a particular page is seen. We can store this information in a binary array where the array indices represent our rows. If a particular page is seen in a certain row, that array index is marked as 1. For example:

Justin Bieber -> rows [0, 1] -> [1][1][0][0]
Ke$ha         -> rows [2, 3] -> [0][0][1][1]

Justin Bieber is seen in rows 0 and 1. This mapping of col- umn values to row indices forms an inverted index [39]. To know whichrowscontainJustin BieberorKe$ha,wecanORtogether the two arrays.

[0][1][0][1] OR [1][0][1][0] = [1][1][1][1]

This approach of performing Boolean operations on large bitmap sets is commonly used in search engines. Bitmap indices for OLAP workloads is described in detail in [32]. Bitmap compression al- gorithms are a well-defined area of research [2, 44, 42] and often utilize run-length encoding. Druid opted to use the Concise algo- rithm [10]. Figure 7 illustrates the number of bytes using Concise compression versus using an integer array. The results were gen- erated on a cc2.8xlarge system with a single thread, 2G heap, 512m young gen, and a forced GC between each run. The data set is a single day’s worth of data collected from the Twitter garden hose [41] data stream. The data set contains 2,272,295 rows and 12 dimensions of varying cardinality. As an additional comparison, we also resorted the data set rows to maximize compression.


圖7. Integer array size versus Concise set size.

In the unsorted case, the total Concise size was 53,451,144 bytes and the total integer array size was 127,248,520 bytes. Overall, Concise compressed sets are about 42% smaller than integer ar- rays. In the sorted case, the total Concise compressed size was 43,832,884 bytes and the total integer array size was 127,248,520 bytes. What is interesting to note is that after sorting, global com- pression only increased minimally.

4.2 Storage Engine

Druid的持久化元件允許不同的儲存引擎以外掛的方式接入,類似於[Dynamo][12]。這些儲存引擎可以將資料儲存在一個完全的in-memory結構的引擎中,例如JVM heap,或者是儲存於 memory-mapped 結構的儲存中。Druid中儲存引擎可配置更換的這個能力依賴於一個特定的應用規範。一個in-memory的儲存引擎要比memory-mapped儲存引擎的成本昂貴得多,但是如果對於效能特別敏感的話,in-memory儲存引擎則是更好的選擇。預設情況下使用的是memory-mapped儲存引擎。

當使用一個memory-mapped儲存引擎的時候,Druid依賴於作業系統來對segment在記憶體中進行換入和換出操作。因為只有當segment載入到記憶體中了才可以被查詢,所以memory-mapped儲存引擎允許將最近的segment保留在記憶體中,而那些不會再被查詢的segment則被換出。使用memory-mapped的主要缺點是當一個查詢需要更多的segment並且已經超出了節點的記憶體容量時,在這種情況下,查詢效能將會因為不斷的在在記憶體中進行segment的換入和換出而下降。