Dremel: Interactive Analysis of Web-Scale Datasets 1~6節算法思想部分翻譯

摘要:

Dremel是一個具有可擴展性和交互性,專用於分析只讀嵌套數據的查詢系統。它本身對多級操作數和柱狀數據佈局的融合使它得以在秒級的反應時間內對有萬億數量級行記錄的表進行集成語句查詢。這個系統在谷歌包含數以千計的CPUPT級的數據量,並有着上千名使用者。這篇論文中,我們將會介紹Dremel的體系結構以及其實現,並闡述它如何實現基於MapReduce的計算。我們將呈現一種全新的嵌套式數據柱狀存儲方式並通過一個基於幾千節點的樣例系統實驗分析性能。


1.介紹:

大型數據分析性處理已經在互聯網公司和各個工業行業裏成爲一個潮流,這並不僅是因爲低價的存儲設備使得所有人對關鍵業務數據的存儲成爲可能。將這些數據放在分析師與工程師們觸手可得處的重要性與日俱增,交互反應時間的差別常常令各種各樣的任務,比如數據挖掘,監視(monitoring),在線用戶支持(onlinecustomer support),快速原型(rapidprototyping),數據流水線排錯(debuggingof data pipelines)等等任務的結果產生質的差別。

實現大型的交互式數據分析需要很高水平的並行性。比如,想從現今的商用硬盤裏用一秒以內的時間得到tb數量級的數據,我們需要同時使用上萬個磁盤。同樣的,對CPU要求很高的查詢語句要想在秒級時間內完成也需要數千個內核同時運行才得以實現。在Google,大量並行計算是基於共享的商業機器集羣完成的[5]。一個集羣通常都會跑着許多需要一起共享資源,卻有着不同工作負荷和使用不同硬件參數的分佈式應用。在這些分佈式應用中,某些任務可能會花費比自己單節點時更多的時間去完成,甚至因爲節點失效或者是任務被集羣管理器系統搶佔等原因永遠無法完成。所以,爲了快速執行完成任務同時得到高容錯性的重要任務就在於如何處理這些“掉隊者”或者“失敗者”[10]

網頁計算或是科學計算所使用的數據彼此間通常都是沒有相關性的。所以,一個靈活的數據模型在這些領域就顯得尤爲重要。編程語言所使用的數據結構,分佈式系統中的交換信息(包),結構化文檔等等總是以嵌套的形式來表達自己。在網頁中將這些信息規範化以及重新組合等等通常都是禁止的。谷歌的大部分結構化數據都是基於嵌套式數據模型的,很多主流的互聯網公司聽說也是如此。

這篇論文闡述了一個支持基於商用機器的共享集羣中超大規模數據集的交互式分析的系統,名爲Dremel。與傳統的數據庫不同,它可以對insitu的嵌套式數據進行查詢。Insitu,指的是“合適地”找到數據的能力,比如,在一個像GFS一樣的分佈式文件系統或是其它存儲層中。Dremel可以對存儲數據執行很多查詢語句,而這些工作本來可能是需要用MapReduce這樣的工具來實現的,但Dremel只需要花費後者百分之幾的時間即可完成任務。Dremel的目的並不是成爲MR的替代品,它經常和MR一起工作去分析MR的輸出流或者是進行更大量數據的快速原型。

Dremel誕生於2006年,在谷歌內部有着上千用戶。Dremel的很多應用案例都被佈置在公司成千上萬的數據節點當中。以下便是其中一些樣例:

  • 對爬取的網絡文件進行分析(Analysisof crawled web documents

  • 跟蹤安卓市場的應用安裝信息(Trackinginstall data for applications on Android Market

  • 谷歌產品的運行崩潰信息(Crashreporting for Google products

  • 谷歌書籍的光學字符識別(OpticalCharacter Recognition

  • 垃圾郵件分析(Spamanalysis

  • 谷歌地圖的地圖片排錯(Debuggingof map tiles on Google Maps

  • 大數據表的子表遷移(Tabletmigrations in managed Bigtable instances

  • 谷歌分佈式系統的測試結果(Resultsof tests run on Google’s distributed build system

  • 成百上千的硬盤I/O數據信息(Resultsof tests run on Google’s distributed build system

  • 對運行在谷歌數據中心的應用進行資源監控(Resourcemonitoring for jobs run in Google’s data centers

  • 谷歌代碼庫中的標記和依賴(Resourcemonitoring for jobs run in Google’s data centers

創建Dremel的靈感來自於網絡搜索和並行式DBMS。首先,它的體系架構借鑑了分佈式搜索引擎的服務樹結構[11]。就像一個網絡搜索請求一樣,一個查詢語句會被往樹下推,每一步都會重寫。查詢結果是對所有低層樹回覆的集合。第二,Dremel提供了一個高級的,類似SQL的語言來表達專門的查詢語句。與Pig[18]Hive[16]不同,Dremel可以自己實現查詢,並不需要將工作轉化成MR任務。

最後,也是最重要的一點,Dremel使用了一種柱狀存儲機制,而正是這一點使它能夠從二級存儲中讀取更少的數據量,更低廉的壓縮代價也減少了CPU消耗。列向存儲機制已經被用來對關係型數據進行分析,但據我們所知,它還並沒有被應用到嵌套數據模型中去。我們展示的柱狀存儲格式受到谷歌許多數據處理工具的支持,包括MRSawzall[20],和FlumeJava[7]

本篇論文主要內容如下:

  • 介紹一種針對嵌套式數據的創新式柱狀存儲格式。我們會展示拆分嵌套式數據成列以及重組它們的算法。

  • 我們將會列出Dremel的查詢語言和執行方式。它們都是爲了更有效地操作柱狀嵌套式數據的同時不需要重組柱狀數據(第五小節)而專門設計的。

  • 我們會展示網絡搜索系統中的執行樹是如何被應用到數據庫處理中,並闡釋它在處理執行集合查詢語句時高效的原因。

  • 我們會展示一個基於千億級數據記錄,數tb數據集,基於10004000系統數據節點的實驗(第七小節)

本篇論文的結構如下:在第二節,我們會解釋Dremel是如何與其它數據管理工具相結合來進行數據分析的。第三節會介紹它的數據模型。前文所提及的最主要內容會在第四到第八小節中依次詳述。一些相關工作會在第九小節簡述。第十小節則爲結語。


2.背景

我們先來看看交互式查詢處理是如何與大型數據管理生態系統合作的。假設谷歌工程師Alice有了一個新點子,她想從網頁中提取新類型的信號。她在分佈式文件系統上跑了一個MR任務,這個任務會把input數據啓動併產出一個包含新信號的數據集,結果就存儲在分佈式系統的億萬條記錄之中。爲了分析她的數據結果,她啓動Dremel並執行了幾條交互式命令:

DEFINETABLE t AS /path/to/data/*

SELECTTOP(signal1, 100), COUNT(*) FROM t

她的命令很快就在幾秒內被執行了。她又執行了幾個別語句來確認自己的算法無誤。當她發現Signal1裏有一個bug她寫了一個可以進行更多複雜分析計算的FlumeJava[7]程序來深層次地分析之前的輸出集合。這個工作完成後,她創建了一個持續處理輸入數據的工作流水線。她又制定了一些SQL語句去彙集跨越了多維度的流水線輸出結果,同時把它們添加到一個交互式的儀表盤中去。最後她將她的新數據集放入一個類別中以方便別的工程師快速定位使用它。

以上這個情景就要求查詢語句處理器和其它數據管理工具的內部交互操作。首先它們必須要有一個共同的存儲層commonstoragelayerGFS[14]就是這樣一種在谷歌廣泛使用的分佈式存儲層。GFS使用了複製備份的方法以保護數據,同時能在出現個別節點罷工的時候照樣保證低反應時間。一個高性能的存儲層對insitu數據管理的要求是苛刻的。它要能夠快速地定位所需要數據位置,而這恰恰是傳統數據庫分析數據處理時的一個主要阻力——大部分時候DBMS取數據和執行一個查詢語句的時間都可以用來跑好幾打的MR任務了[13]。這個共同的平臺還有一個附加的好處,就是文件系統中的數據可以被通用工具很方便地操作,比如,轉移數據到另一個集羣上,改變訪問優先級,或是定義一個基於文件名的數據分析子集。


1:嵌套型數據的按記錄存儲vs.柱狀存儲


第二,要建立一個可內部交互操作的數據管理部件還需要一個統一的存儲格式。柱狀存儲已經在平整的關係型數據上取得了成功,但要使它對谷歌同樣奏效的話就要求它能夠適應嵌套式數據模型。圖1闡述了它的主要思想:相同嵌套字段(比如ABC)的所有值都被連續地存儲着。因此,A.B.C可以在不讀入A.EA.B.D的情況下就被檢索到。在此我們強調,我們所面臨的挑戰是如何保存所有的數據結構信息以及如何從任意字段的子集中重構記錄。接下來我們將討論我們的數據模型,這之後還有算法和查詢語句處理。


3.數據模型

這部分我們會展示Dremel的數據模型,並介紹一些之後會用到的術語。這個在谷歌廣泛使用的數據模型來自於分佈式系統的內容,具體實現方式也已經開源。這個數據模型基於強類型的嵌套記錄,它的抽象語法如下:

其中τ指代原子類型或是記錄類型。dom中的原子類型由整型,浮點數,字符串等組成。記錄由一個或者多個字段組成。一個記錄中的字段i被命名爲Ai,也可以是一個多下標的表示。重複字段(*)在一個記錄中可能重複多次。它們被當作爲一列值,比如,在一個記錄中它域所出現的順序一定是值得我們注意的。選擇字段(?)可能不會在一個記錄中出現。其餘情況下,一個字段是必須的,比如說它必須剛剛好出現一次。


2:兩個嵌套式記錄的樣本以及它們的模型


爲了更好地闡述這個概念,請看圖2。它刻畫了一個定義記錄類型Document的模型,代表了一個網絡文件。這個模型的定義用了具體的語法形式[21]。一個Document需要一個整型的DocId和一個可選的LinksLinks又包含了存儲其它網頁DocIdForwardBackward。一個Document可以有多個名字,其實也就是這個Document指向的不同的URLs。一個Name包含了一個Code序列和一個可選的Country對。圖2還展示了兩個基於這個模型的記錄樣本r1r2。記錄模型的結構用縮進表示出來了。接下來的模塊裏我們將繼續用這個樣本來解釋我們的算法。這個模型中定義的字段可以組成一個樹結構。這個嵌套字段的全路徑是用普通的點符號表示,比如:Name.Language.Code

嵌套式數據模型支持平臺無關和谷歌序列化結構數據可擴展機制。代碼生成工具可以綁定C++Java這類編程語言。記錄的標準二進制在線表示法使跨語言的內部可操作性(Cross-languageinteroperability)成爲可能,通過這種方式字段值也按照它們在一條記錄中出現的先後被順序記錄下來。同樣通過這種方式,一個Java寫的MR程序可以使用數據源中用C++庫表示的記錄。因此,如果以柱狀形式來存儲數據,爲了使Dremel可以和MR或是其它數據處理工具合作,如何彙集它們就變得很重要。


4.嵌套式柱狀存儲

正如圖1當中展示的那樣,我們的目的是想通過連續存儲某一個字段的所有值來提高檢索效率。在本模塊中,我們將着重討論如下問題:柱狀格式記錄結構的無損存儲(4.1);快速編碼(4.2);記錄有效重組(4.3)。


4.1  重複級別和定義級別

如果僅僅有數據的話是無法表示一個記錄的結構的。給一個可重複字段的兩個值,我們不會知道這個值是處在哪一個“級別”上的。(比如,它們到底是來自兩個不同的記錄,還是一個記錄中的兩個重複記錄)。同樣的,如果一個可選擇字段被弄丟了,我們就不會知道哪一個是清晰定義的記錄。所以我們引進了重複和定義級別的概念,詳細定義可見後文。作爲參考,可以查看總結了我們樣例記錄中所有原子字段重複和定義級別的圖3

重複級別:回顧下圖2中的Code字段。它在r1中出現了3次。‘en-us和‘en’出現在第一個Name中,而‘en-gb’出現在第三個Name中。爲了不使這些讓我們產生混淆,我們會給每個值添加一個重複級別。它會告訴我們這個值重複了這個字段路徑中的哪一個重複字段atwhat repeated field in the field’s path the value hasrepeated)。字段路徑Name.Language.Code包含了兩個重複字段,NameLanguage。因此,Code的重複級別取區間[0,2];級別0表示這是一個新紀錄的起點。現在假設我們在從頭到尾掃描記錄r1。當我們遇到‘en-us’時,我們還沒有遇到任何的重複字段,也就是說,重複級別爲0。當我們遇到‘en’時,Language字段重複了,所以它的重複級別就是2。最終,當我們掃描到‘en-gb’Name已經重複了最多次(LanguageName之後只出現了一次),所以它的重複級別是1。所以,r1Code字段值的重複級別是021

可以注意到r1中的第二個Name字段並不包含任何的Code值。爲了確定‘en-gb’確實是出現在第三個Name而不是第二個,我們在‘en’和‘en-gb’中添加一個NULL值(詳見圖3)。CodeLanguage當中的一個必須字段,所以它的值如果消失則表明Language並沒有被定義。但是一般情況下,要決定嵌套記錄的存在哪一級別還需要一些額外信息。


3:圖2中樣本記錄的柱狀表示,包括重複級別(r)和定義級別(d


定義級別:每一個帶有路徑p的字段值,尤其是那些NULL地址的,都有一個定義級別來記錄p中有多少可以不用定義的字段(因爲它們是可重複或是可選擇的)是真正存在的。我們可以觀察到r1並沒有Backward鏈接,但是,字段Links是定義了的。爲了保存這個信息,我們可以給Links.Backward列添加一個NULL值和定義級別1。同樣地,r2中消失的Name.Language.Country也有一個定義級別1,而r1中這個的定義級別分別爲2Name.Language中)和1Name中)。

我們使用整型的定義級別而不是用0-1比特值的原因,是因爲這樣葉節點字段的數據(比如:Name.Language.Country)就會包含它父母字段的出現信息;這種信息我們要如何使用將會在4.3節給出。

上文中羅列出的編碼方式可以無損地保存記錄結構。證明過程爲了省紙就免了吧~


編碼:每一列都是以塊的集合的形式存儲。每個塊都會保存重複級別和定義級別的信息(下文簡稱爲級別)和壓縮過的字段值。NULL值們並不會特別保存,因爲它們因爲通過定義級別就可以得到:任何比該字段路徑中重複字段和可選字段之和小的定義級別都表示它值爲NULL。定義級別並不是爲那些永遠定義過的值保存的。相同的,重複級別只在被要求的時候對它進行保存;比如,定義級別爲0就表示它的重複級別頁爲0,所以後者在保存時就可以省了。實際上,在圖3中,沒有什麼級別是爲DocId存的。級別會以比特序列的方式被打包。我們只需要用足夠的bit數;比如,如果最大定義級別是3,每個定義級別我們就用兩個bit去存儲。


4.2  將記錄拆分成列

上文我們展示了記錄結構的編碼方式,接下來我們要面臨的挑戰便是如何高效地利用重複級別和定義級別產生列條。

附錄A中有用來計算重複級別和定義級別的基礎算法。算法在記錄結構中遞歸計算每一字段值的級別。正如之前闡述過的,重複和定義級別甚至在字段值不存在時也需要被計算。谷歌大部分的數據集都很稀疏;一個有幾千字段的模型在實際記錄中只用了一百個字段這種事情一點都不奇怪。所以,我們儘自己所能地嘗試去降低處理消失字段的代價。爲了創建列條,我們創建了一顆符合數據模型結構的樹,取名“字段作家(fieldwriters)”。最基本的思想是去更新這個樹當且僅當它們有自己的數據值,並且除非相當必要時,否則不去輕易把父節點的狀態傳播下去。爲了實現後者,子作家(childwriters)會繼承它父母的級別,無論何時子作家都會同步父母的級別值。


4.3  記錄匯合

從列向存儲的數據中高效地還原記錄對基於記錄存儲的數據處理工具(比如MR)而言要求十分嚴格。給了一個子集的字段,我們的目標是去重構它原始的記錄,彷彿它們本身就是包含了我們所有選擇了的字段,其餘的字段都不存在一樣。最核心的思想是,我們創造了一個有限狀態機(FSM)來讀字段值和每個字段的級別,並且把這些值順序附加到輸出記錄中。有限狀態機的一個狀態對應了一個被選擇字段的讀取器。狀態轉換用重複級別標記着。一旦一個讀取器讀到一個值,我們就去查看下一個重複級別已確定使用哪一個狀態/讀取器。每次讀取記錄FSM就會被從頭到尾地遍歷一次。


4:完整記錄匯合自動機;邊上標記的是重複級別


4展示了一個重構我們的樣例的完整記錄的有限狀態機。初始狀態是DocId。一旦一個DocId值已經被讀到,FSM轉移到Links.Backward。在所有重複的Backward值都被排出後,FSM會跳到Links.Forward,以此類推。附錄B中有記錄重構的詳細實現算法。

爲了描述FSM究竟是如何實現狀態轉移的,我們假設下一個當下的讀取器返回的字段f的重複級別是1。從模型樹上的f開始,我們找到它在l層重複祖先,並選擇該祖先中的第一個葉子字段n。這便給了我們一個有限狀態機的轉移(f,l) → n。比如,讓l=1作爲f= Name.Language.Country的讀取器讀取的下一個重複級別。它的重複級別爲1的祖先是NameName的第一個葉子字段是n= Name.Url。附錄C中有FSM構建算法的詳細過程。


5:從兩個字段重構信息的自動機以及樣例(右)


如果僅僅需要檢索一個字段的子集,我們創建一個更簡單的FSM,它消耗的資源也更少。圖5描述了有一個只讀取Name.Language.CountryDocId字段的狀態機。這圖同時還展示了s1s2通過這個自動機產生的結果。需注意我們的編碼和重構算法保存了Country字段的附屬結構。這對於需要用到這些信息的應用而言很重要。比如,找到在第二個Name,第一個Language出現的Country。在路徑語言中(XPath),與這個功能相似的表達式可以寫成/Name[2]/Language[1]/Country


5.查詢語言

Dremel的查詢語言是基於SQL的,並且爲了能夠在柱狀嵌套存儲設備上高效地運行而專門設計。如何規範地定義語言明顯已經不在本論文的探討範圍以內;取而代之地,我們會討論下它有哪些優點。每一個SQL語句(和它翻譯成爲的代數操作符)會取一個或者多個嵌套表和它們的結構,然後輸出一個嵌套表作爲它的輸出。圖6描述了一個實現映射,選擇和記錄自聚合的樣例查詢語句。這個查詢語句基於圖2的表t={r1,r2}分析。字段都通過路徑表達式來表達。這個查詢語句會產生一個嵌套結構雖然這個查詢本身並沒有體現記錄重構的痕跡。


6:查詢語句樣例/其輸出/以及輸出結構


爲了剖析一個查詢語句具體的實現,我們以選擇操作爲例(WHERE語句)。想象一個以標記樹形式存在的嵌套記錄,這顆標記樹的每一個標記都對應它的字段名。這個選擇操作符會剪掉這顆樹中不符合我們選擇條件的枝葉。所以,只有那些Name.Url被定義了並且以http開頭的嵌套記錄會被留下。接下來,考慮映射操作。每一個SELECT語句中的梯級表達式都會省略掉同一嵌套級別中該字段裏最常重複的輸入字段。所以,字符串的連接表達式在Name.Language.Code級裏忽略了輸入結構中Str的值。那個COUNT表達式表示記錄的自聚合。這個聚合在訪問每一個Name子記錄中就已經完成,並且忽略了作爲一個無符號64位整數的Name.Language.Code的重複次數。

Dremel查詢語言支持嵌套子語句查詢,記錄內部或記錄間的聚合運算,求前k位運算,交運算,用戶自定義函數等等;其中一些功能在實驗部分被強化了。


6.執行查詢語句

簡潔起見,我們將討論一個只讀系統執行查詢語句處理的核心思想。許多的Dremel查詢語句都是一輪的聚合;所以我們會着重解釋這一點並用他們在下一小節做實驗。關於交,索引,更新等等的問題將被放在日後的工作中考慮。


7:系統體系架構以及服務器節點的內部運行


樹結構:Dremel用了一個多級別的服務樹來執行查詢語句(見圖7)。一個根服務器接收到新來的查詢語句,從表中讀取元數據,並把這個查詢語句路由給這顆服務樹的下一個層級。葉子服務器和存儲層交流或者是直接找到本地磁盤內的數據。我們以一個簡單的聚合語句爲例:

SELECT A,COUNT(B) FROM T GROUP BY A

當根服務器接收到以上語句時,它決定生成構成T的所有的分割塊(比如水平切分表)並重寫這個查詢語句如下:


R11...R1n是這個查詢將送到服務樹第一層級的1...n號節點的輸入:


T1i是表T中的一個即將被第一層級服務器節點i處理的不相交子集。每一個服務層都會執行這種相似的重寫過程。最終這些查詢語句會到達平行掃描T表子表的葉子層。在向上返回時,中間服務器節點就會平行地匯合各個部分節點的輸出結果。我們剛纔展示的這個執行模型相當適合只返回很小或者是中等大小數據量答案的聚合查詢語句——這種數據量的答案是我們查詢時最常遇到的情況。需要返回結果很大的聚合語句或是其它類型的查詢語句就需要依賴於我們已知的DBMS或是MR的執行機制了。


查詢語句調度器:Dremel是一個多用戶的系統,比如,通常會有很多個查詢語句同時被執行。查詢語句調度器會基於它們的優先級來調度查詢,同時平衡負載。它另一個很重要的角色是當某一服務器的執行速度掉隊或者是一個子表的副本無法讀取時提供容錯機制

每個查詢語句所需要處理的數據量往往比我們提供的處理單位大,通常我們稱這些處理單位爲槽(slot)。一個槽對應一個葉服務器的執行線程。比如,一個有3000葉節點服務器的系統,每個葉服務器有8個線程,那麼這個系統就有24000個槽。所以,一個被分爲100000份的表可以分別給每個槽分配5個子表。在每個查詢語句執行時,查詢語句調度器會計算一個處理子表時間的直方圖。如果一個子表用了過長的時間被處理,它就會進行重新調度,把它放到別的服務器上。某些子表可能會需要被重新調度很多次。

葉節點服務器讀取嵌套數據的柱狀條。每條裏的塊都異步地被事先取得;預讀取(read-ahead)的高速緩存一般來講都有着95%的命中率。子表通常會被複制3份。當一個葉子節點無法取得某一份備份時,它會轉向另外一份備份。

查詢語句調度器有一個規定返回結果時至少子表已掃描百分比的參數。簡單展示一下,把這個值設低一點(比如,從100%改到98%)經常能夠很明顯地提升我們的運行時間,尤其當我們用小一點的複製因子時。

每一個服務器都有一個內部執行樹,如圖7右手邊描述的那樣。內部樹通常有一個物理查詢語句執行計劃存在,包括對梯級表達式的分析。優化一點的情況,爲大多數常見的梯級函數生成專門的代碼。一種針對映射-選擇-聚合查詢語句的執行計劃就包括以下思想:1.一個用來循規蹈矩掃描輸入的柱狀數據的迭代器集合;2.省略聚合語句的結果;3.用正確的重複級別和定義級別指代梯級函數;4.在查詢語句執行期間完全徹底地忽略記錄。附錄D中有詳細闡述。

某些Dremel的查詢語句,比如top-kcount-disdinct,通過我們這個已知的一輪算法會只返回一個大致(approximate)的結果。

===========================================================================================================================================

標紅部分表示不是很確定翻譯正確性,最好自己參考論文原文重新理解。另第一次翻譯論文可能有理解不夠翻譯不準確或是錯誤的地方歡迎指出>////////<

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章