在論證了大規模執行Druid的挑戰之後,我想提出我對下一代開源時間序列儲存的看法,這應該不會出現Druid固有的問題。
其他目標和自我約束:
時間序列儲存可擴充套件到單個群集中的PB級壓縮資料和100k處理核心。雲優先:利用雲的優勢。從數十兆兆位元組的資料和一千個處理核心開始,具有成本效益。在合理規模的群集中,處理少於5 TB資料的查詢應在3秒以內(p99延遲)執行-涵蓋互動式廣告分析用例。高度一致的查詢延遲:相似的查詢應始終花費相同的時間來完成,而不管叢集中並行執行的查詢是什麼。新攝取的資料應立即可查詢。仔細想想:提出的設計有望在3-5年內變得越來越重要,而不是不那麼重要。非目標:
本地部署。小規模的成本效益。隨機更新和刪除舊資料的效率,儘管這些事情應該是可能的。對於任何小的查詢,即使在沒有負載的系統中,p99的等待時間也不到半秒。易於首次部署和軟體更新。最後的介紹性說明:這篇文章基於在Metamarkets大規模執行Druid的經驗和理論研究,但是所描述的設計尚未在生產中實施和測試。這篇文章中的某些陳述是錯誤的。如果您有任何意見或更正,請在此帖子下發表評論!
設計概述具有三個解耦子系統的時間序列儲存的設計。淺藍色線表示未壓縮的面向行的資料流;深藍線-壓縮的柱狀資料;紅線-查詢結果。
該系統由三部分組成,各部分之間有嚴格的職責分離:流處理系統,儲存和計算樹。
流處理系統攝取資料(接受“寫入”),對其進行分割槽,將每個時間間隔內的資料轉換為壓縮的列格式並將其寫入Storage。流處理系統的工作人員還負責計算最新資料的部分查詢結果。
計算樹具有多個級別的節點:最低級別的節點從Storage中下載特定分割槽和間隔的資料,併為其計算部分結果。如果查詢間隔包括最新資料,則第二層中的節點合併特定分割槽的所有分割槽的結果,並接受最低層中的節點和Stream處理系統的工作程式的接受。第三級中的節點合併或合併第二級中節點的每個時間間隔結果,幷包含每個時間間隔查詢結果的快取。這些節點還可能負責群集平衡和較低級別的計算樹的自動縮放。
此設計的關鍵原則:
計算和儲存的分離。這個想法來自BigQuery。在我有關Druid問題的文章中,我解釋了Druid中缺少這種分隔如何使查詢延遲不可預測,因為查詢之間會相互干擾。
使計算樹中的節點(幾乎)是無狀態的,這意味著它們是“一次性”的。它們可能是亞馬遜的EC2或Google的可搶佔例項,它們比普通例項便宜幾倍。同樣,計算樹可以在數分鐘之內放大和縮小,從而有可能e。G。在查詢負載較低時,每晚和週末將其按比例縮小。
資料攝取(在流處理系統中)和儲存分開。這個想法實際上已經在Druid中實現,它具有實時節點。這樣的關注點分離可以使Storage保持非常簡單,不需要分配資源來進行提取,列壓縮,查詢處理等。它只專注於從磁碟讀取位元組塊並將其透過網路傳送到計算中的節點和樹。
流處理系統也可能比支援寫操作的儲存更動態。流處理系統可以根據資料攝取強度的變化而按比例放大或縮小,通常在晚上和週末較低。流處理系統可能具有在儲存中難以實現的功能,例如動態重新分割槽。
網路是瓶頸如果查詢的下載量沒有使Storage的出站網路頻寬飽和,則網路對總查詢延遲的貢獻是恆定的,並且與查詢大小無關。如果將雲物件儲存用作儲存(請參閱下面的“雲物件儲存”部分),或者相對於儲存中的歷史資料量,系統中的查詢負載不成比例地較小,則可以授予此許可權。
如果這兩個條件都不適用,則可以使用Storage託管一些非時間序列的,下載頻率較低的資料,以便人為地增加Storage群集的大小,從而增加其出站網路頻寬。
否則,在儲存和計算樹之間的網路吞吐量可能將成為限制所提出設計中查詢延遲的因素。有幾種方法可以減輕這種情況:
與僅生成一個表的典型SQL查詢不同,對該系統的查詢應組成所有子查詢,而這些子查詢是在分析介面的單個螢幕上所需的。Analytics(分析)介面通常包括至少幾個,有時是幾十個表,圖表等,它們是同一時間序列資料的子查詢的結果。在第三級計算樹中慷慨地快取查詢結果,以減少重做相同計算的負載。投影下推:僅從儲存區下載查詢處理所需的列子集。按維度鍵分割槽(最常出現在查詢過濾器中)僅下載和處理所需的分割槽-謂詞下推式。由於許多實際資料維度中的金鑰頻率是Poisson-,Zipf-或其他不均勻分佈的,因此理想情況下,Stream處理系統應支援“部分”分割槽,請參見下圖。由於這種分割槽的基數較低,因此可以在各個分割槽變得太小而無法以列格式和處理進行有效壓縮之前,將資料按多個維度進行分割槽。部分分割槽可實現金鑰分配不均。每個盒子都是一個分割槽。具有“其他值”的分割槽可能具有數千個“長尾”值。
更一般而言,資料段(分割槽)的元資料應包括有關所有維度的資訊,該維度似乎在此分割槽中僅填充了一個(或很少)鍵,從而可以從“意外”分割槽中受益。色譜柱壓縮應強烈支援壓縮率,而不是減壓或處理速度。列資料應從儲存流式傳輸到計算樹中的節點,並且一旦所有必需列的第一個塊到達計算節點,就開始子查詢處理。這樣可以使網路和CPU的貢獻在總查詢延遲中儘可能地重疊。要從中受益,將列從儲存傳送到計算樹的順序應該比僅在儲存中的磁碟上排列列的順序或列名稱按字母順序排列的順序更聰明。列也可以按小塊以交錯順序傳送,而不是逐列傳送。一旦部分結果準備就緒,就遞增計算最終查詢結果,並將增量結果流式傳輸到客戶端,以使客戶端感知查詢執行得更快。在本文的後面,我將詳細介紹系統的每個部分。
雲物件儲存它是Amazon S3,Google雲端儲存(GCS),Azure Blob儲存以及其他雲提供商的類似產品。
從概念上講,這正是設計的時間序列儲存中應使用的儲存方式,因為GCS由名為Colossus的系統提供支援,並且它也是BigQuery的儲存層。
雲物件儲存API不夠完善,不足以在單個請求中支援多個位元組範圍的下載(用於多列的投影下推),因此每列的每次下載應是一個單獨的請求。我懷疑這不是BigQuery的工作方式,它與Colossus的整合更緊密,可以實現適當的多列投影下推。
在我看來,“雲物件儲存”選項的主要缺點可能是其p99延遲和吞吐量。一些基準測試表明,GCS和S3在100 ms的延遲中具有p99延遲(這是可以接受的),並且吞吐量僅受下載端VM功能的限制,但是如果在併發100個負載的情況下仍然如此,我將感到非常驚訝一個節點的請求,以及整個叢集中一百萬個併發請求的規模。請注意,所有云提供商都沒有針對物件儲存延遲和吞吐量的SLA,對於GCS,公認吞吐量是“相當多的變數”。
(注意:之前,在上面的部分中,我提到了Cloud Object Storage API不支援範圍請求,這是不正確的,儘管它們仍然不支援(截至2019年10月)單個請求中的多個範圍下載,因此併發查詢放大係數不會消失。)
HDFS中Parquet格式的資料分割槽此選項的主要優點是與Hadoop生態系統的其餘部分很好地整合-計算樹甚至可以“附加”到某些已經存在的資料倉庫中。大型聯接或多步查詢等不適用於時間序列正規化的複雜查詢可以由同一HDFS群集頂部的Spark,Impala,Hive或Presto之類的系統處理。
同樣重要的是,旨在部署設計的時間序列儲存的組織可能已經具有非常大的HDFS叢集,該叢集具有較大的出站網路頻寬,並且如果時間序列儲存使用此HDFS叢集儲存其資料分割槽,則它可能會工作圍繞網路的可擴充套件性問題。
但是,庫存HDFS透過單個NameNode路由所有讀取請求。100k併發讀取請求(假設只需要一個讀取請求就可以在計算樹中的一個節點上下載資料分割槽)接近NameNode的絕對可伸縮性限制,因此,如果HDFS叢集實際上忙於處理某些內容,則超出該限制與時間序列儲存無關的操作。
此外,當HDFS用作“遠端”分散式檔案系統時,即使對於Parquet格式的檔案,它也不支援投影下推,因此整個資料分割槽應由計算樹中的節點下載。如果時間序列資料中有數百列,並且通常只使用一小部分進行查詢,則效果將不佳。正如雲物件儲存所建議的那樣,使每個資料分割槽的每一列都成為一個單獨的檔案,由於擴大了檔案和讀取請求的數量,因此施加了更大的可擴充套件性限制。NameNode將無法處理一百萬個併發請求,並且HDFS並未針對小於10 MB的檔案進行最佳化,假設最佳資料分割槽的大小約為一百萬,則資料分割槽的各個列將具有的大小行。
但是,在某些情況下(例如,存在大量未充分利用的HDFS叢集)並且在某些使用情況下,HDFS似乎是最經濟高效的選擇,並且執行良好。
Apache KuduApache Kudu是一種列式資料儲存,旨在在許多情況下替換HDFS + Parquet對。它結合了節省空間的列式儲存以及快速進行單行讀寫的能力。設計的時間序列系統實際上不需要第二部分,因為寫入是由Stream處理系統處理的,而我們希望使Storage更加便宜並且不浪費CPU(例如用於後臺壓縮任務),每個Storage節點上的記憶體和磁碟資源支援單行讀取和寫入。此外,在Kudu中對舊資料進行單行寫入的方式要求在Kudu節點上進行分割槽解壓縮,而在建議的時間序列儲存設計中,只有壓縮後的資料應在儲存和計算樹之間傳輸。
另一方面,Kudu具有多種功能,這些功能吸引了時間序列系統,而HDFS沒有:
類似於RDBMS的語義。Kudu中的資料以表格的形式組織,而不僅僅是一堆檔案。Kudu中的平板電腦伺服器(節點)比HDFS中的伺服器更獨立,從而可以在進行讀取時繞過查詢主節點(Kudu等效於NameNode),從而大大提高了讀取可擴充套件性。投影下推。它是用C ++編寫的,因此尾部延遲應該比用Java編寫並且會出現GC暫停的HDFS更好。Kudu論文提到,從理論上講,它可能支援可插拔的儲存佈局。如果實施的儲存佈局放棄了Kudu對提取單行寫入和舊資料寫入的支援,但更適合於時間序列儲存設計,則Kudu可能會成為比HDFS更好的儲存選項。
Cassandra或Scylla每個資料分割槽可以儲存在類似Cassandra的系統中的單個條目中。從Cassandra的角度來看,列具有二進位制型別,並存儲資料分割槽的壓縮列。
該選項與Kudu共享許多優點,甚至具有更好的優點:出色的讀取可伸縮性,極低的延遲(尤其是如果使用ScyllaDB),表語義,僅下載所需列的能力(投影下推式)。
另一方面,類似Cassandra的系統並非設計用於多個MB的列值和大約100 MB的總行大小,並且在填充此類資料時可能開始遇到操作問題。而且,它們不支援在單行甚至單行中的單列級別上進行流讀取,但可以在這些系統中相對容易地實現。
Cassandra旨在承受高寫入負載,因此使用類似LSM的儲存結構和大量記憶體,在時間序列系統中用作儲存時將浪費資源。
將計算樹的節點重用為儲存(已在2019中新增)請參閱此處的想法說明。https://github.com/apache/druid/issues/8575
流處理系統如上所述,Druid已經將資料攝取與所謂的索引子系統或實時節點中的儲存區分開了。但是,儘管該索引子系統實現了完整的分散式流處理系統的功能的子集,但它並未利用其中的任何功能,甚至也沒有利用Mesos或YARN之類的資源管理器,並且一切都在Druid原始碼中完成。Druid的索引子系統的效率要比現代流處理系統低得多,因為對其進行的開發工作少了數十倍。
同樣,時間序列資料通常在Druid之前的其他流處理系統中進行組合或豐富。例如,沃爾瑪(Walmart)透過Storm來做到這一點,而Metamarkets將Samza用於類似目的。從本質上講,這意味著兩個獨立的流處理系統正在資料管道中一個接一個地執行,從而阻止了對映運算子與Druid的提取終端運算子的融合,這是流處理系統中的常見最佳化。
這就是為什麼我認為在下一代時間序列中,資料提取應充分利用某些現有的流處理系統。
流處理系統與其餘時間序列儲存之間需要緊密整合,例如允許計算樹中的節點查詢流處理系統中的工作程式。這意味著與Storage的情況不同,它可能很難支援多個流處理系統。應該只選擇一個,並將其與時間序列系統整合。
Flink,Storm和Heron都是可能的候選人。很難判斷當前哪個技術更合適,或者說在哪個技術上更合適,因為這些專案可以快速相互複製要素。如果設計的時間序列系統實際上是在某個組織中建立的,則選擇可能取決於該組織中已使用的流處理系統。
閱讀Druid Development郵件列表中的該執行緒,以獲取有關此主題的更多資訊。
計算樹對於系統的這一部分的外觀,我並不太費勁。上面的“設計概述”部分介紹了一些可能的方法。
這種方法至少存在一個問題:如果需要快取太多查詢結果,則計算樹的第三(最高)級別的多個節點將無法有效地處理對特定時間序列(表)的查詢。為了始終將相似的子查詢(僅在總體查詢間隔上不同的子查詢)路由到相同的節點並捕獲快取的結果,應將具有多個子查詢的一個“複合”查詢分解為多個獨立的查詢,進而使用網路儲存和計算樹之間的效率較低:請參見上面的“網路是瓶頸”部分,該列表中的第一項。
但是,可以在垂直方向上擴充套件第三級計算樹中的節點,以使其足夠大,從而能夠處理所有查詢並容納任何單個時間序列(甚至最繁忙的時間序列)的整個快取。
垂直擴充套件意味著第三級計算樹中的一個節點應處理大量併發查詢。這就是為什麼我認為如果從頭開始構建計算樹的原因之一,它應該選擇非同步伺服器體系結構而不是阻塞(Go風格的綠色執行緒也可以)。其他兩個原因是:
第一層計算樹中的節點透過儲存執行大量的網路I / O。這些節點上的計算取決於來自Storage的資料到達,並具有不可預知的延遲:來自Storage的資料請求通常會得到重新排序的響應。計算樹所有級別的節點都應支援增量查詢結果計算,並可能以很長的間隔返回同一查詢的多個結果。如上文“網路是瓶頸”一節所述,它使系統更具容錯能力(在我的第一篇文章中討論了執行Druid的挑戰),並使其變得更快。平臺理想情況下,構建計算樹的程式設計平臺應具有以下特徵:
支援執行時程式碼生成,以使查詢更快地完成並提高CPU利用率。這篇有關Impala中執行時程式碼生成的部落格文章對此進行了很好的解釋。出於相同的原因,生成的機器程式碼應該是“最佳”的,並在可能的情況下進行向量化處理。較低的堆/物件記憶體開銷,因為記憶體昂貴,因此使計算樹中的節點更便宜。始終較短的垃圾回收暫停(對於具有託管記憶體的平臺),以支援設計的時間序列儲存的“一致查詢延遲”目標。從純技術角度來看,C ++是贏家,它可以滿足所有這些要求。選擇C ++與效能無關的缺點也是眾所周知的:開發速度,可除錯性,使用外掛體系結構擴充套件系統都很困難等。
JVM仍然是一個不錯的選擇,我相信該系統的效率可能比使用C ++內建的系統低不超過20%:
JVM允許搭載JIT編譯器以達到與執行時程式碼生成目標相同的效果。對於時間序列處理,主要在列解壓縮期間以及在資料上執行特定聚合時需要程式碼向量化。兩者都可以在JNI函式中完成。當為數十千位元組的解壓縮資料支付一次時,JNI的開銷相對較小(我們可能希望以這種大小的塊進行處理以適合L2快取中的所有解壓縮資料)。巴拿馬專案將使此開銷更小。如果將資料儲存在堆外記憶體中並進行處理,則垃圾回收的JNI含義也很小或根本不存在。可以透過將所有網路IO,資料儲存,緩衝和處理都放在堆外記憶體中,從而使堆記憶體很小,從而僅對每個查詢分配一些堆。使用Shenandoah GC可以縮短垃圾收集的暫停時間。如果核心處理迴圈中使用的所有資料結構都是非堆分配的,則堆記憶體的讀取和寫入障礙不會對CPU利用率造成太大影響。據我所知,儘管Go或Rust目前不支援執行時程式碼生成,儘管新增這種支援可能不需要太多的駭客操作:請參閱gojit專案以及有關Rust的StackOverflow問題。對於其他條件,Go的執行時和生成的程式碼可能效率較低,但是出於某些非技術性原因,它比Rust更有效。