万亿级日志与行为数据存储查询技术剖析
http://www.sohu.com/a/126082450_355140
目前大數據存儲查詢方案大概可以分為:Hbase系、Dremel系、預聚合系、Lucene系,本文作者將就自身的使用經驗說說這幾個系的優缺點,如有紕漏,歡迎一起探討。
寫在前面
近些年,大數據背后的價值也開始得到關注和重視,越來越多的企業開始保存和分析數據,希望從中挖掘大數據的價值。大數據產生的根本還是增量數據,單純的用戶數據不足以構成大數據,然而用戶的行為或行為相關的日志的數據量,加之隨著物聯網的發力,產生的增量數據將不可預估,存儲和查詢增量數據尤為關鍵。所以,在筆者的工作經歷中,本著以下的目標,尋找更優的大數據存儲和查詢方案:
數據無損:數據分析挖掘都依賴于我們保存的數據,只有做到數據的無損,才有可能任意的定義指標,滿足各種業務需求。
保證數據實時性:數據的實時性越來越重要,實時的數據能夠更好的運維產品和調整策略,價值更高。單進程每秒接入3.5萬數據以上,數據從產生到能夠查詢到結果這個間隔不會超過5秒。
業務需求快速響應:隨著越來越快的業務發展和數據應用要求的提高,數據的查詢需要更靈活,快速響應不同且多變的需求。最好是任意定義指標后能夠實時查詢出結果。
數據靈活探索性:探索性數據分析在對數據進行概括性描述,發現變量之間的相關性以及引導出新的假設。到了大數據時代,海量的無結構、半結構數據從多種渠道源源不斷地積累,不受分析模型和研究假設的限制,如何從中找出規律并產生分析模型和研究假設成為新挑戰。因此,探索性數據分析成為大數據分析中不可缺少的一步并且走向前臺。
超大數據集,統計分析秒級響應:萬億數據量級,千級維度(非稀疏)的統計分析秒級響應。
目前大數據存儲查詢方案大概可以分為:Hbase系、Dremel系、預聚合系、Lucene系,筆者就自身的使用經驗說說這幾個系的優缺點,如有紕漏,歡迎一起探討。
數據查詢包括大體可以分為兩步,首先根據某一個或幾個字段篩選出符合條件的數據,然后根據關聯填充其他所需字段信息或者聚合其他字段信息,本文中提到的大數據技術,都將圍繞這兩方面。
一、Hbase系
筆者認為Hbase系的解決方案(例如Opentsdb和Kylin)適合相對固定的業務報表類需求,只需要統計少量維度即可滿足業務報表需求,對于單值查詢有優勢,但很難滿足靈活聚合數據的場景。
Hbase的表包含的的概念有rowkey、列簇、列限定符、版本(timestamp)和值,對應實際Hdfs的存儲結構可以用下圖做一個簡單總結:
Hbase表中的每一個列簇會對應一個實際的文件,某種層面來說,Hbase并非真正意義的列式存儲方案,只是列簇存儲。每個文件有若干個DataBlock(數據塊默認64k),DataBlock是HBase中數據存儲的最小單元,DataBlock中以KeyValue的方式存儲用戶數據(KeyValue后面有timestamp,圖中未標注),其他信息主要包含索引、元數據等信息,在此不做深入探討。每個KeyValue都由4個部分構成,分別為key length,value length,key和value。其中key的結構相對復雜,包括rowkey、列、KeyType等信息,而value值對應具體列值的二進制數據。為了便于查詢,對key做了一個簡單的倒排索引,直接使用了java的ConcurrentSkipListMap。
Hbase管理的核心思想是分級分塊,存儲時根據Rowkey的范圍不同,分散到不同的Region,Region又按照列簇分為不同的Store,每個Store實際上又包括StoreFile(對應Hfile)和MemStore,然后由RegionServer管理不同的Region,RegionServer即對應具體的進程,分散不同的機器,提供分布式的存儲和查詢。查詢時,首先獲取meta表(一種特殊的Region)所在的RegionServer,通過meta表查找表rowkey相對應的Region和RegionServer信息,最后連接數據所在的RegionServer,查找到相應的數據。
Hbase的這種結構,特別適合根據rowkey做單值查詢,不適合scan的場景,因為大部分Scan的情況基本上需要掃描所有數據,性能會非常差。雖然也有擴展的Hbase二級索引方案,但基本上都是通過協處理器,需要另外建立一份rowkey的對應關系,Scan的時候先通過二級索引查找rowkey,然后在根據rowkey查找相應的數據。
這種方式一定程度上能加快數據掃描,但那對于一些識別度不高的列,如性別這樣的字段,對應的rowkey相當之多,這樣的字段在查找二級索引時的作用很小,另外二級索引所帶來的IO性能的開銷都會隨之增加。而在需要聚合的場景,對于Hbase而言恰恰需要大量scan數據,會非常影響性能。Hbase只有一個簡單rowkey的倒排索引,缺少列索引,所有的查詢和聚合只能依賴于rowkey,很難解決聚合的性能問題。
隨著Hbase的發展,基于Hbase做數據存儲包括Opentsdb和Kylin也隨之產生,例如Kylin也是一種預聚合方案,因其底層存儲使用Hbase,故筆者將其歸為Hbase系。在筆者看來,Opentsdb和Kylin的數據結構極其相似,都是將各種維度值組合,結合時間戳拼成rowkey,利用字典的原理將維度值標簽化,達到壓縮的目的。如此,可以滿足快速查詢數據的需要,但同時也會受限于Hbase索引,聚合需要大量scan,并不能提升數據聚合的速度。
為了避免查詢數據時的聚合,Kylin可以通過cube的方式定制數據結構,在數據接入時通過指定metric來提前聚合數據。這樣雖然在一定程度上解決了數據聚合慢的情況,但這是一種典型的空間換時間的方案,組合在維度多、或者有高基數維度的情況,數據膨脹會非常嚴重,筆者曾遇到存儲后的數據比原始數據大90倍的情況。另外,業務的變化會導致重建cube,難以靈活的滿足業務需要。
二、Dremel系
Parquet作為Dremel系的代表,相對Hbase的方案,Scan的性能更好,也避免了存儲索引和生成索引的開銷。但對于數據還原和聚合,相對直接使用正向索引來說成本會很高,而且以離線處理為主,很難提高數據寫的實時性。
Google的Dremel,其最早用于網頁文檔數據分析,所以設計為嵌套的數據結構,當然它也可以用于扁平的二維表數據存儲。開源技術中,Parquet算是Dremel系的代表,各種查詢引擎(Hive/Impala/Drill)、計算框架甚至一些序列化結構數據(如ProtoBuf)都對其進行了支持,甚至Spark還專門針對Parquet的數據格式進行了優化,前途一片光明,本文主要結合Parquet來展開論述。
可以用下圖簡單表示Parquet的文件格式:
Parquet的數據水平切分為多個Row Group,Row Group為數據讀寫的緩存單元,每個Row Group包含各個的數據列(Column Chunk),數據列有若干Page,Page是壓縮和編碼的單元,其相應存儲的信息包括元數據信息(PageHeader)、重復深度(Repetition Levels)、定義深度(Definition Levels)和列值(Values)信息。
Page實際有三種類型:數據Page、字典Page和索引Page。數據Page用于存儲當前行組中該列的值,字典Page存儲該列值的編碼字典,每一個列塊中最多包含一個字典Page,索引Page目前還不支持,未來可能會引入Bloom Filter,能夠判斷列值是否存在,更有利于判斷搜索條件,提升查詢速度。
從Parquet的存儲結構來看,Parquet沒有嚴格意義上的索引,在查詢的過程中需要直接對Row Group的列數據進行掃描,有兩方面來保證查詢優化,一個是映射下推(Project PushDown),另外一個是謂詞下推(Predicate PushDown)。
映射下推主要是利用列式存儲的優勢,查詢數據時只需要掃描查詢中需要的列,由于每一列的所有值都是連續存儲的,所以分區取出每一列的所有值就可以實現TableScan算子,而避免掃描整個文件內容。
謂詞下推在數據庫之類的查詢系統中最常用的優化手段之一,通過將一些過濾條件盡可能的在最底層執行,減少上層交互的數據量,從而提升性能。另外,針對謂詞下推Parquet做了更進一步的優化,優化的方法是對每一個Row Group的每一個Column Chunk在存儲的時候都計算對應的統計信息,包括該Column Chunk的最大值、最小值和空值個數。通過這些統計值和該列的過濾條件可以判斷該Row Group是否需要掃描。未來還會增加諸如Bloom Filter和Index等優化數據,更加有效的完成謂詞下推。
通過這兩方面的優化,Parquet的查詢時掃描數據的性能能夠得到大幅度提升。那Parquet如果填充數據(不同的列拼成一行記錄)和聚合數據呢?
主要是使用了Striping/Assembly算法實現的,該算法的思想是將數據的值分為三部分:重復深度(Repetition Levels)、定義深度(Definition Levels)和列值(Values)。通過重復深度可以在讀取的時候結合Schema的定義可以知道需要在哪一層創建一個新的repeated節點(如第一層的的為0,表示是新記錄,否則則表示repeat的數據),然后通過定義深度知道該值的路徑上第幾層開始是未定義,從而還原出數據的嵌套結構,如此便能清楚的把一行數據還原出來。由于缺少行號對應的列正向索引,沒有辦法直接尋址,單純的依賴于Striping/Assembly算法還原數據或者聚合處理,相對來說成本會高很多。
另外,Parquet的實時寫方面是硬傷,基于Parquet的方案基本上都是批量寫。一般情況,都是定期生成Parquet文件,所以數據延遲比較嚴重。為了提高數據的實時性,還需要其他解決方案來解決數據實時的查詢,Parquet只能作為歷史數據查詢的補充。
Parquet存儲是相對索引的存儲來說,是一種折中處理,沒有倒排索引,而是通過Row Group水平分割數據,然后再根據Column垂直分割,保證數據IO不高,直接Scan數據進行查詢,相對Hbase的方案,Scan的性能更好。這種方式,避免了存儲索引和生成索引的開銷,隨著索引Page的完善,相信查詢性能值得信賴。而對于數據還原和聚合也沒有利用正向索引,而是通過Striping/Assembly算法來解決,這種方式更好能夠很取巧的解決數據嵌套填充的問題, 但是相對直接使用正向索引來說成本會很高。
另外,由于是基于Row Group為讀寫的基本單元,屬于粗粒度的數據寫入,數據生成應該還是以離線處理為主,很難提高數據寫的實時性,而引入其他的解決方案又會帶來存儲架構的復雜性,維護成本都會相應增加。
三、預聚合系
最近幾年,隨著OLAP場景的需要,預聚合的解決方案越來越多。其中比較典型的有Kylin、Druid和Pinot。預聚合的方案,筆者不想做過多介紹,其本身只是單純的為了滿足OLAP查詢的場景,需要指定預聚合的指標,在數據接入的時候根據指定的指標進行聚合運算,數據在聚合的過程中會丟失metric對應的列值信息。
筆者認為,這種方式需要以有損數據為代價,雖然能夠滿足短期的OLAP需求,但是對于數據存儲是非常不利的,會丟掉數據本身存在的潛在價值。另外,查詢的指標也相對固定,沒有辦法靈活的自由定義所需的指標,只能查詢提前聚合好的指標。
四、Lucene系
Lucene算是java中最先進的開源全文檢索工具,基于它有兩個很不錯的全文檢索產品ElasticSearch和Solr。Lucene經過多年的發展,整個索引體系已經非常完善,能夠滿足的的查詢場景遠多于傳統的數據庫存儲,這都歸功于其強大的索引。但對于日志、行為類時序數據,所有的搜索請求都也必須搜索所有的分片,另外,對于聚合分析場景的支持也是軟肋。
Lucene中把一條數據對應為一個Document,數據中的字段對應Lucene的Field,Field的信息可以拆分為多個Term,同時Term中會包含其所屬的Field信息,在Lucene中每一個Document都會分配一個行號。然后在數據接入時建立Term和行號的對應關系,就能夠根據字段的信息快速的搜索出相應的行號,而Term與行號的對應關系我們稱之為字典。大部分時候查詢是多個條件的組合,于是Lucene引入了跳表的思想,來加快行號的求交和求并。字典和跳表就共同組成了Lucene的倒排索引。Lucene從4開始使用了FST的數據結構,即得到了很高的字典壓縮率,又加快了字典的檢索。
為了快速的還原數據信息和聚合數據,Lucene還引入了列正向索引和行正向索引。列正向索引主要是行號和Term的對應關系,行正向主要是行號和Document的對應關系。這兩種索引都是可以根據需要配置使用,例如只有單純的查詢,只是用行正向索引就可以,為了實現數據的聚合則必須列正向索引。
有了這些索引后,就可以通過Term來查詢出行號,利用正向索引根據行號還原數據信息,或者對數據進行聚合。
另外,為了滿足全文檢索的需求,Lucene還引入了分詞、詞向量、高亮以及打分的機制等等。總的來看,Lucene的整個索引體系比較臃腫,其設計的根本還是搜索引擎的思想,滿足全文檢索的需求。
Lucene本身是單機版的,沒有辦法分布式,也就以為著其能處理的還是小數據量。ElasticSearch提供了Lucene的分布式處理的解決方案,其核心思想是將Lucene的索引分片。
在寫入場景中,對于同一個index的數據,會按照設定的分片數分別建立分片索引,這些分片索引可能位于同一臺服務器,也可能不同。同時,各分片索引還需要為自己對應的副本進行同步,直到副本寫入成功,一次寫入才算完整的完成。當然,單個文檔的寫入請求只會涉及到一個分片的寫入。搜索場景則大致是逆過程,接受請求的節點將請求分發至所有承擔該分片查詢請求的節點,然后匯總查詢請求。這里值得注意的是,任意一個搜索請求均需要在該index的所有分片上執行。
由于ElasticSearch是一個搜索框架,對于所有的搜索請求,都必須搜索所有的分片。對于一個針對內容的搜索應用來說,這顯然沒有什么問題,因為對應的內容會被存儲到哪一個分片往往是不可知的。然而對于日志、行為類數據則不然,因為很多時候我們關注的是某一個特定時間段的數據,這時如果我們可以針對性的搜索這一部分數據,那么搜索性能顯然會得到明顯的提升。
同時,這類數據往往具有另一個非常重要的特征,即時效性。很多時候我們的需求往往是這樣的:對于最近一段時間的熱數據,其查詢頻率往往要比失去時效的冷數據高得多,而ElasticSearch這樣不加區分的分片方式顯然不足以支持這樣的需求。
而另外一方面,ElasticSearch對于聚合分析場景的支持也是軟肋,典型的問題是,使用Hyperloglog這類求基數的聚合函數時,非常容易發生oom。這固然跟這類聚合算法的內存消耗相對高有關(事實上,hll在基數估計領域是以內存消耗低著稱的,高是相對count,sum這類簡單聚合而言)。
五、Tindex
數果智能根據開源的方案自研了一套數據存儲的解決方案,該方案的索引層通過改造Lucene實現,數據查詢和索引寫入框架通過擴展Druid實現。既保證了數據的實時性和指標自由定義的問題,又能滿足大數據量秒級查詢的需求,系統架構如下圖,基本實現了文章開頭提出的幾個目標。
Tindex主要涉及的幾個組件
Tindex-Segment,負責文件存儲格式,包括數據的索引和存儲,查詢優化,以及段內數據搜索與實時聚合等。Tindex是基于Lucene的思想重構實現的,由于Lucene索引內容過于復雜,但是其索引的性能在開源方案中比較完善,在數據的壓縮和性能之間做了很好的平衡。我們通過改造,主要保留了其必要的索引信息,比原有的Lucene節省了更多的存儲空間,同時也加快了查詢速度。主要改進有以下幾點:
1、高效壓縮存儲格式
對于海量行為數據的存儲來說,存儲容量無疑是一個不容忽視的問題。對于使用索引的方案來說,索引后的數據容量通常相對原有數據會有一定程度的膨脹。針對這類情況,Tindex針對索引的不同部分,分別使用了不同形式的壓縮技術,保障了能夠支持高效查詢的同時僅僅需要較少的容量。對于數據內容部分,使用字典的方式編碼存儲,每條記錄僅僅存儲文檔編號。對于字典本身的存儲,使用了前綴壓縮的方式,從而降低高基數維度的空間消耗。實際情況下,使用 Tindex 壓縮后的數據占用的存儲容量僅僅為原始數據的1/5左右。
2、列式倒排和正向索引的存儲
由于實際使用中,往往需要同時支持搜索和聚合兩種場景,而這兩種方式對于索引結構的需求是完全相反的。針對這兩種情況,Tindex結合了倒排索引和列正向索引這兩種不同類型的索引。對于倒排索引部分,使用字典和跳表等技術,實現了數據的快速檢索,而對于正向部分,則通過高效的壓縮技術,實現了對于海量行下指定列的快速讀取。同時,根據不同的情況,可以選擇性的只建立其中一種索引(默認情況對于每一列均會同時建兩種索引),從而節省大約一般的存儲空間和索引時間。
Tindex-Druid,負責分布式查詢引擎、指標定義引擎、數據的實時導入、實時數據和元數據管理以及數據緩存。之所以選擇Druid是因為我們發現其框架擴展性、查詢引擎設計的非常好,很多性能細節都考慮在內。例如:
-
堆外內存的復用,避免GC問題;
-
根據查詢數據的粒度,以Sequence的方式構建小批量的數據,內存利用率更高;
-
查詢有bySegment級別的緩存,可以做到大范圍固定模式的查詢;
-
多種query,最大化提升查詢性能,例如topN、timeSeries等查詢等等。
框架可靈活的擴展,也是我們考慮的一個很重要的元素,在我們重寫了索引后,Druid社區針對高基數維度的查詢上線了groupByV2,我們很快就完成了groupByV2也可見其框架非常靈活。
在我們看來,Druid的查詢引擎很強大,但是索引層還是針對OLAP查詢的場景,這就是我們選擇Druid框架進行索引擴展的根本原因。 另外其充分考慮分布式的穩定性,HA策略,針對不同的機器設備情況和應用場景,靈活的配置最大化利用硬件性能來滿足場景需要也是我們所看重的。
在開源的Druid版本上自研,繼承了Druid所有優點的同時,對查詢部分代碼全部重新實現,從而在以下幾個方面做了較大改進:
1、去掉指標預聚合,指標可以在查詢時自由定義:
對于數據接入來說,不必區分維度和指標,只需要定義數據類型即可,數據使用原始數據的方式進行存儲。當需要聚合時,在查詢時定義指標即可。假設我們要接入一條包含數字的數據,我們現在只需要定義一個float類型的普通維度。
2、支持多種類型:
不同于原生的Druid只支持string類型維度的情況,我們改進后的版本可以支持string, int, long, float、時間等多種維度類型。在原生的Druid中,如果我們需要一個數值型的維度,那么我們只能通過string來實現,這樣會帶來一個很大的問題,即基于范圍的過濾不能利用有序的倒排表,只能通過逐個比較來實現(因為我們不能把字符串大小當成數值大小,這樣會導致這樣的結果‘12’ < ’2’),從而性能會非常差,因為數值類型維度很容易出現高基維。對于改進后的版本,這樣的問題就簡單多了,將維度定義為對應的類型即可。
3、實現數據動態加載:
原有的Druid線上的數據,需要在啟動時,全部加載才可以提供查詢服務。我們通過改造,實現了LRU策略,啟動的時候只需要加載段的元數據信息和少量的段信息即可。一方面提升了服務的啟動時間,另外一方面,由于索引文件的讀取基本都是MMap,當有大量數據段需要加載,在內存不足的情況,會直接使用磁盤swap Cache換頁,嚴重影響查詢性能。數據動態加載的很好的避免了使用磁盤swap Cache換頁,查詢都盡量使用內存,可以通過配置,最大限度的通過硬件環境提供最好的查詢環境。
HDFS,大數據發展這么多年,HDFS已經成為PB級、ZB級甚至更多數據的分布式存儲標準,很成熟了,所以數果也選用HDFS,不必重新造輪子。Tindex與HDFS可以完美結合,可以作為一個高壓縮、自帶索引的文件格式,兼容Hive,Spark的所有操作。
Kafka/MetaQ,消息隊列,目前Tindex支持kafka、MetaQ等消息隊列,由于Tindex對外擴展接口都是基于SPI機制實現,所以如有需要也可以擴展支持更多的消息隊列。
Ecosystem Tools,負責Tindex的生態工具支持,目前主要支持Spark、Hive,計劃擴展支持Impala、Drill等大數據查詢引擎。
支持冷數據下線,通過離線方式(spark/Hive)查詢,對于時序數據庫普遍存在的一個問題是,對于失去時效性的數據,我們往往不希望它們繼續占據寶貴的查詢資源。然后我們往往需要在某些時候對他們查詢。對于Tindex而言,可以通過將超過一定時間的數據定義為冷數據,這樣對應的索引數據會從查詢節點下線。當我們需要再次查詢時,只需要調用對應的離線接口進行查詢即可。
SQL Engine,負責SQL語義轉換及表達式定義。
Zookeeper,負責集群狀態管理。
未來還會持續優化改造后的Lucene索引,來得到更高的查詢性能。優化指標聚合方式,包括:小批量的處理數據,充分利用CPU向量化并行計算的能力;利用code compile避免聚合虛函數頻繁調用;與大數據生態對接的持續完善等等。
轉載于:https://www.cnblogs.com/davidwang456/articles/9981526.html
總結
以上是生活随笔為你收集整理的万亿级日志与行为数据存储查询技术剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: elasticsearch版本不同,批量
- 下一篇: eBay的Elasticsearch性能