Druid 是一個為在大數據集之上做實時統計分析而設計的開源數據存儲。這個系統集合了一個面向列存儲的層,一個分布式、shared-nothing的架構,和一個高級的索引結構,來達成在秒級以內對十億行級別的表進行任意的探索分析。在這篇論文里面,我們會描述Druid的架構,和怎樣支持快速聚合、靈活的過濾、和低延遲數據導入的一些細節。
- 介紹
在最近幾年,互聯網技術的快速增長已經產生了大量由機器產生的數據。單獨來看,這些數據包含很少的有用信息,價值都是很低的。從這些巨大的數據里面分析出有用的信息需要大量的時間和資源,很多公司都選擇了放棄這些數據。雖然已有基礎設施來處理這些居于事件的數據(例如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節介紹下一些相關的工作。 - 問題定義
Druid的最初目的是設計來解決導入和分析大規模交易事件(日志數據)。這種時間序列形式的數據通常在OALP類工作流中比較常見,且數據的本質是非常重的追加寫。
表1: 在Wikipedia由編輯產生的Druid示例數據
例如,考慮下表1包含的數據,表1包含了在Wikipedia編輯而產生的數據。每當用戶編輯一個Wikipedia的頁面的時候,就會產生一條關于編輯的包含了元數據的事件數據,這個元數據包含了3個不同的部分。首先,有一個timestamp列指示編輯的時間。然后,還有一組維度列(dimension)表明關于編輯的各種屬性,例如被編輯的頁面、由誰編輯的、編輯用戶的位置。最后,還有一組包含值的(通常是數字)、可以被聚合計算的指標列(metric),例如在編輯中添加或刪除的字符個數。
我們的目標是在這個數據之上做快速的鉆取(drill-downs)和聚合計算,我們希望回答之如“編輯賈斯汀·比伯這個頁面的編輯者中有多少是來自于舊金山的男性?” 和 “最近一個月中由來自于Calgary的人添加的平均字符數是多少?”。我們也希望可以以任意組合維度來查詢并在秒級以內返回數據。
之所以需要Druid,是因為現實情況是現有的開源關系型數據庫(RDBMS)和NoSQL key/value 數據庫沒辦法為一些交互式應用提供低延遲的數據導入和查詢。在Metamarkets的早期,我們的重點是建立一個托管的儀表板,允許用戶以可視化的方式任意地去瀏覽事件流數據。支撐這個儀表板的數據存儲需要以足夠快的速度返回查詢結果,在這之上的數據可視化才可以給用戶提供良好的用戶體驗。
除了查詢響應時間的要求外,該系統還必須是多租戶和高可用的。Metamarkets的產品是用于高并發的環境中,停機成本是昂貴的,而且許多企業都沒法承受系統不可用時的等待,即便是軟件升級或者是網絡故障。停機時間于創業公司來說,特別是那些缺乏適當的內部運維管理的,是可以決定一個公司的成敗的。
最后,另外一個Metamarkets成立之初面臨的一個挑戰是允許用戶和報警系統可以“實時”地做商業決策。從一個事件數據被創建,到這個事件數據可以被查詢的時間,決定了利益相關方能夠在他們的系統出現潛在災難性情況時多快做出反應。流行的開源數據倉庫系統,例如Hadoop,并不能達到我們所需要的秒級的數據導入和查詢的要求。
數據導入、分析和可用性這些問題存在于多個行業中,自從Druid在2012年10月開源以來,它被作為視頻、網絡監控,運營監控和廣告分析平臺部署到多家公司。
-
架構
一個Druid集群包含不同類型的節點,而每種節點都被設計來做好某組事情。我們相信這樣的設計可以隔離關注并簡化整個系統的復雜度。不同節點的運轉幾乎都是獨立的并且和其他的節點有著最小化的交互,因此集群內的通信故障對于數據可用性的影響微乎其微。
為了解決復雜的數據分析問題,把不同類型的節點組合在一起,就形成了一個完整的系統。Druid這個名字來自于Druid類的角色扮演游戲。Druid集群的構成和數據流向如圖1所示。
圖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: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傳送走,一旦這個segment在Druid集群中的其他地方加載了并可以查詢了,實時節點會刷新它收集的 13:00
到 14:00
這個時段的數據的信息,并且宣布取消為這些數據提供服務。
3.1.1 可用性與可擴展性
實時節點是一個數據的消費者,需要有相應的生產商為其提供數據流。通常,為了數據耐久性的目的,會在生產商與實時節點間放置一個類似于Kafka這樣的消息總線來進行連接,如圖4所示。實時節點通過從消息總線上讀取事件數據來進行數據的導入。從事件數據的創建到事件數據被消費掉通常是在幾百毫秒這個級別。
圖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就可以被查詢了。歷史節點的本地緩存也支持歷史節點的快速更新和重啟,在啟動的時候,該節點會檢查它的緩存,并為任何它找到的數據立刻進行服務的提供。
圖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所示。實時數據永遠不會被緩存,因此查詢實時節點的數據的查詢請求總是會被轉發到實時節點上去。實時數據是不斷變化的,因此緩存實時數據是不可靠的。
圖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節點、歷史節點、實時節點都是仍然可以查詢的。
- 存儲格式
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中都必須有一個時間戳),使用列式來存儲聚合信息比使用行存儲更好這個是 有據可查 的。列式存儲可以有更好的CPU利用率,只需加載和掃描那些它真正需要的數據。而基于行的存儲,在一個聚合計算中相關行中所有列都必須被掃描,這些附加的掃描時間會引起性能惡化。
Druid有多種列類型來表示不同的數據格式。根據列的類型,會使用不同的壓縮算法來降低一個列存儲在內存和磁盤的成本。在表1提供的示例中,page, user, gender, 和 city 列都只包含字符串,直接存儲字符串的成本很高而且沒有必要,可以使用字典編碼(Dictionary encoding)來代替。字典編碼是一個常用的數據壓縮算法,也已經用在類似 PowerDrill 這樣的數據存儲上。在表1的示例中,我們可以將每一個page映射到一個唯一的整數標識符上。
Justin Bieber -> 0Ke$ha -> 1
這個映射關系允許我們使用一個整數數組來表示page列,這個數組索引了原始數據集的相應的行。對于page列,我們可以用以下的方式來表示:
[0, 0, 1, 1]
這個整數數組結果使得它可以很好的應用壓縮算法。在編碼的基礎上使用常用的壓縮算法在列式存儲中很常見。Druid使用的 LZF 壓縮算法。類似的壓縮算法也可以應用于數字列,例如,表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 。這些存儲引擎可以將數據存儲在一個完全的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的換入和換出而下降。