標籤:
原文: https://mp.weixin.qq.com/s?__biz=MjM5NzAyNTE0Ng==&mid=207895956&idx=1&sn=58e8af26fd3c6025acfa5bc679d2ab01&scene=1&srcid=0919Sz0SAs6DNlHTl7GYxrGW&key=dffc561732c2265121a47642e3bebf851225841a00d06325b09e7d125978a26d60870026c28e5375d5f6f3dd479d73bb&ascene=0&uin=Mjk1ODMyNTYyMg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.10.5+build(14F27)&version=11020201&pass_ticket=VWw5L3fXKRxnYGILAFOTg%2B5jK7t%2FJb5MZ3fNO4RAu2kqv%2FWZS4bYH3n6XCZyy9NX
[譯者注]從頭到尾讀懂一篇國外經典技術論文!相信這是很多技術愛好者一直以來想乾的事情。本系列譯文的目標是滿足廣大技術愛好者對原始論文一窺究竟的需求,盡量對原文全量翻譯。原始論文中不乏較晦澀的學術性語句,也可能會有您不感興趣的段落,所以譯者會添加【譯者預讀】【譯者總結】等環節協助大家選擇性的閱讀,或者協助讀者總結。根據譯者的翻譯過程,論文中也難免缺少細節的推導過程(Google的天才們總是把我們想的跟他們一樣聰明),遂添加特殊的【譯者YY】環節,根據譯者的理解對較為複雜的內容進行解釋和分析,此部分主觀性很大難免有誤,望讀者矯正。所有非原文內容皆以藍色文字顯示。
廢話不多說,大家一覽為快吧!
【譯者預讀】此篇是伴隨著Dremel神話橫空出世的原始論文(不知道Dremel的讀者可以立刻搜尋感受一下Dremel的強大)。文章深入分析了Dremel是如何利用巧妙的資料存放區結構+分布式並行計算,實現了3秒查詢1PB的神話。
論文的前幾部分是“abstract”、“introduction”、“background”,介紹性的文字較多,其核心意思是:面對海量資料的分析處理,MR(MapReduce)的優勢不需多言,其劣勢在於時效性較差不滿足互動式查詢的需求,比如3秒內完成對萬億資料的一次查詢等,Dremel應此需求而生,與MR成為有效互補。
摘要
Dremel是一個用於分析唯讀嵌套資料的可伸縮、互動式ad-hoc查詢系統。通過結合多層級樹狀執行過程和列狀資料結構,它能做到幾秒內完成萬億行資料之上的彙總查詢。此系統可伸縮至成千上萬的CPU和PB層級的資料,而且在google已有幾千使用者。在本篇論文中,我們將描述Dremel的架構和實現,解釋它為何是MapReduce計算的有力互補。我們提供一種嵌套記錄的列狀儲存結構,並且討論在擁有幾千個節點的系統上進行的實驗。
1. 介紹
大規模分析型資料處理在互連網公司乃至整個行業中都已經越來越廣泛,尤其是因為目前已經可以用廉價的儲存來收集和儲存海量的關鍵業務資料。如何讓分析師和工程師便捷的利用這些資料也變得越來越重要;在資料探測、監控、線上使用者支援、快速原型、資料管道調試以及其他任務中,互動的回應時間一般都會造成本質的區別。
執行大規模互動式資料分析對並行計算能力要求很高。例如,如果使用普通的硬碟,希望在1秒內讀取1TB壓縮的資料,則需要成千上萬塊硬碟。相似的,CPU密集的查詢操作也需要運行在成千上萬個核上。在Google,大量的並行計算是使用普通PC組成的共用叢集完成的[5]。一個叢集通常會部署大量共用資源的分布式應用,各自產生不同的負載,運行在不同硬體設定的機器上。一個分布式應用的單個工作任務可能會比其他任務花費更多的時間,或者可能由於故障或者被叢集管理系統取代而永遠不能完成。因此,處理好異常、故障是實現快速執行和容錯的重要因素[10]。
互連網和科學計算中的資料經常是獨立的、互相沒有關聯的。因此,在這些領域一個靈活的資料模型是十分必要的。在程式設計語言中使用的資料結構、分布式系統之間交換的訊息、結構化文檔等等,都可以用嵌套式表達法來很自然的描述。規格化、重新組合這些互連網規模的資料通常是代價昂貴的。嵌套資料模型成為了大部分結構化資料在Google處理的基礎[21],據報道也在其他互連網公司被使用。
這篇論文描述了一個叫做Dremel的系統,它支援在普通PC組成的共用叢集上對超大規模的資料集合執行互動式查詢。不像傳統的資料庫,它能夠操作原位嵌套資料。原位意味著在適當的位置訪問資料的能力,比如,在一個Distributed File System(比如GFS[14])或者其他儲存層(比如Bigtable[8])。查詢這些資料一般需要一系列的MapReduce(MR[12])任務,而Dremel可以同時執行很多,而且執行時間比MR小得多。Dremel不是為了成為MR的替代品,而是經常與它協同使用來分析MR管道的輸出或者建立大規模計算的原型系統。
Dremel自從2006就投入生產了並且在Google有幾千使用者。多種多樣Dremel的執行個體被部署在公司裡,排列著成千上萬個節點。使用此系統的例子包括:
分析網路文檔
追蹤Android市場應用程式的安裝資料
Google產品的崩潰報告分析
Google Books的OCR結果
垃圾郵件分析
Google Maps裡地圖組件調試
託管Bigtable執行個體中的Tablet遷移
Google分布式構建系統中的測試結果分析
成百上千的硬碟的磁碟IO統計資訊
Google資料中心上啟動並執行任務的資源監控
Google程式碼程式庫的符號和依賴關係分析
Dremel基於互連網搜尋和並行DBMS的概念。首先,它的架構借鑒了用在分布式搜尋引擎中的服務樹概念[11]。就像一個web搜尋請求一樣,查詢請求被推入此樹、在每個步驟被重寫。通過彙總從下層樹節點中收到的回複,不斷裝配查詢的最終結果。其次,Dremel提供了一個進階、類SQL的語言來表達ad-hoc查詢。與Pig[18]和Hive[16]不同,它使用自己技術執行查詢,而不是翻譯為MR任務。
最後也是最重要的,Dremel使用了一個column-striped的儲存結構,使得它能夠從二級儲存中讀取較少資料並且通過更廉價的壓縮減少CPU消耗。列儲存曾被採用來分析關係型資料[1],但是據我們瞭解還沒有推廣到嵌套資料模型上。我們所展現的列狀儲存格式在Google已經有很多資料處理工具支援,包括MR、Sawzall[20]、以及FlumeJava[7]。
在本論文中我們做了如下貢獻:
我們描述了一個嵌套資料的列狀儲存格式。並且提供了演算法,將嵌套記錄解剖為列結構,在查詢時重新裝配它們(第4章節)。
我們描述了Dremel的查詢語言和執行過程。兩者都被定製化設計,能夠在column-striped的嵌套資料上高效執行,不需要裝載原始嵌套記錄(章節5)。
我們展示了在web搜尋系統中使用的樹狀執行過程如何被適用到資料庫處理,並且解釋他們的優劣,以及如何做到高效彙總查詢(章節6)。
我們在萬億記錄、TB層級的資料集合上進行實驗,系統執行個體擁有1000-4000個節點[章節7]。
這篇論文結構如下。章節2中我們解釋了Dremel如何結合其他資料管理工具進行資料分析。它的資料模型在章節3介紹。上述主要貢獻覆蓋在章節4-8。相關工作在章節9討論。章節10是總結。
2. 背景
作為開始我們來看一個情境,這個情境說明了互動式查詢處理的必要性,以及它在資料管理生態系統上怎麼定位。假設一個Google的員工Alice,誕生了一個新奇的靈感,她想從web網頁中提取新類型的signals。她運行一個MR任務,分析輸入資料然後產生這種signals的資料集合,在Distributed File System上儲存這數十億條記錄。為了分析她實驗的結果,她啟動Dremel然後執行幾個互動式命令:
DEFINE TABLE t AS /path/to/data/*
SELECT TOP(signal1, 100), COUNT(*) FROM t
她的命令只需幾秒鐘就執行完畢。她也運行了幾個其他的查詢來證實她的演算法是正確的。她發現signal1中有非預期的情況於是寫了個FlumeJava[7]程式執行了一個更加複雜的分析式計算。一旦這個問題解決了,她建立一個管道,持續的處理輸入資料。然後她編寫了一些SQL查詢來跨維度彙總管道的輸出結果,然後將它們添加到一個互動dashboard,其他工程師能非常快速的定位和查詢它。
上述案例要求在查詢處理器和其他資料管理工具之間互相操作。第一個組成部分是一個通用儲存層。Google File System(GFS[14])是公司中廣泛使用的分布式儲存層。GFS使用冗餘複製來保護資料不受硬碟故障影響,即使出現掉隊者(stragglers)也能達到快速回應時間。對原位元據管理來說,一個高效能的儲存層是非常重要的。它允許訪問資料時不消耗太多時間在載入階段。這個要求也導致資料庫在分析型資料處理中[13]不太被使用。另外一個好處是,在檔案系統中能使用標準工具便捷的操作資料,比如,遷移到另外的叢集,改變存取權限,或者基於檔案名稱定義一個資料子集。
第二個構建互相協作的資料管理組件的要素,是一個共用的儲存格式。列狀儲存已經證明了它適用於扁平的關係型資料,但是使它適用於Google則需要適配到一個嵌套資料模型。圖1展示了主要的思想:一個嵌套欄位比如A.B.C,它的所有值被連續儲存。因此,A.B.C被讀取時,不需讀取A.E、A.B.D等等。我們面臨的挑戰是如何保護所有的結構化資訊,並且能夠按任意欄位子集來重建記錄。下一步我們討論資料模型,然後是演算法和查詢處理。
【譯者總結】前幾部分最需要關注的其實是圖1中的嵌套資料和列狀儲存格式(columnar representatin of nested data)。這是Dremel提升效能的核心理論,而作者沒有對此圖著重強調,其實對圖1右邊列狀儲存的理解是攻克此篇論文的關鍵。
3.資料模型
【譯者預讀】有經驗的程式員都知道理解一個系統的第一步就是理解它的資料模型,所以此章節可稱之為論文最核心的部分之一。其數學公式對於廣大coder不很直觀,但其實並不複雜,就2中描述的結構一樣,本質上和JSON、XML描述的資料結構沒有區別,就是一種嵌套的、定製化的資料結構。需要著重理解的是在下面章節會頻繁使用的幾個名詞和基礎知識。比如記錄(record)、欄位(field)、列(column)等。記錄(record)就是指一條完整的嵌套資料,如果是在DB中一條記錄就是一行(row)資料。欄位和列在大部分情況下指的是同一個概念,比2中Name、Language等,它們是結構中的一個欄位(field),將來儲存時就是一個列(column)。比如在Google裡爬蟲抓來的一個網頁(Document)的資料就是一條記錄,而將其結構化之後其中的Forward連結、Url就是欄位(或列)。所謂的列狀儲存其實就是將原始記錄按欄位切分,各個欄位的資料獨立集中儲存(比如將所有記錄中Name.Url這一列的值放在一起儲存)。另外需要注意的是欄位的類型,每個欄位都屬於某種類型,比如required,表示有且僅有一個值;optional,表示可選,0到1個值;repeated(*),表示重複,0到N個值,等。其中repeated和optional類型是非常重要的,作者會從它們身上抽象出一些重要的概念,以便用最少的代價來無損的描述出原始的資料。最後還需要補充兩個術語,一是column-stripe,表示圖1右邊按列儲存的一堆列值(列“條”,某個列下順序儲存的一長條資料);另一個是在論文中廣泛使用的路徑運算式,xxx.xxx.xxx,其作用類似於XML中的XPath,比如Name.Language.Code,就表示圖2中的code欄位,因為是在樹狀結構中,用這樣的path能夠準確的描述其位置。
在此章節中我們介紹Dremel的資料模型以及一些後續將會用到的術語。這個在分布式系統中經常面對的資料模型(‘Protocol Buffers’[21])在Google使用廣泛,也提供了開源實現。這個資料模型是基於強型別嵌套記錄的。它的抽象文法是:
π = dom | <A1 : π [*|?],…,An : π [*|?]>
π是一個原子類型(一個int、一個string…比如DocId)或者記錄類型(指向一個子結構,比如Name)。在dom中原子類型包含整型、浮點數、字串等等。記錄則由一到多個欄位組成。欄位i在一個記錄中命名為Ai,以及一個標籤(比如(?)或(*),指明該欄位是可選的或重複的…)。重複欄位(*)表示在一個記錄中可能出現多次,是多個值的列表,欄位出現的順序是非常重要的。可選欄位(?)可能在記錄中不出現。如果不是重複欄位也不是可選欄位,則該欄位在記錄中必須有值,有且僅有一個。
圖2進行了舉例說明。它描述了一個叫Document的schema,表示一個網頁。schema定義使用了[21]中介紹的具體文法。一個網頁文檔必有整型DocId和可選的Links屬性,包含Forward和Backword列表,列表中每一項代表其他網頁的DocId。一個網頁能有多個名字Name,表示不同的URL。名字包含一系列Code和(可選)Country的組合(也就是Language)。圖2也展現了兩個樣本記錄,r1和r2,遵循上述schema。我們將使用這些樣本記錄來解釋下一章節涉及到的演算法。schema的欄位定義按照樹狀層級。一個嵌套欄位的完整路徑使用簡單的點綴符號表示,如,Name.Language.Code。
嵌套資料模型為Google的序列化、結構化資料奠定了一個平台無關的可擴充機制。而且有為C++、Java等語言打造的代碼產生工具。通過使用標準二進位on-the-wire結構,實現跨語言互通性,欄位值按它們在記錄中出現的次序被順序的陳列。這樣一來,一個Java編寫的MR程式能利用一個C++庫暴露的資料來源。因此,如果記錄被儲存在一個列狀結構中,快速裝配就成為了MR和其他資料處理工具之間互通性的重要因素。
4. 嵌套列狀儲存
1所示,我們目標是連續的儲存一個欄位的所有值來改善檢索效率。在本章節中,我們面對下列挑戰:一個列狀格式記錄的無損表示(章節4.1),快速encoding(章節4.2),高效的記錄裝配(章節4.3)。
4.1 重複深度、定義深度
只有欄位值不能表達清楚記錄的結構。給出一個重複欄位的兩個值,我們不知道此值是按什麼‘深度’被重複的(比如,這些值是來自兩個不同的記錄,還是相同的記錄中兩個重複的值)。同樣的,給出一個缺失的可選欄位,我們不知道整個路徑有多少欄位被顯示定義了。因此我們將介紹重複深度和定義深度的概念。圖3概述了所有原子欄位的重複和定義深度以供參考。
【譯者注】讀者請重新審視一1右邊的列狀儲存結構,這是Dremel的目標,它就是要將圖2中Document那種嵌套結構轉變為列狀儲存結構。要實現這個目標的方式多種多樣,而此章節中Dremel信心滿滿的推出了它設計的最佳化、最節省成本、效率最高的方法,並且引出了兩個全新的概念,重複深度和定義深度。因為Dremel會將記錄肢解、再按列各自集中儲存,此舉難免會導致資料失真,比2中,我們把r1和r2的URL列值放在一起得到[“http://A”,"http://B","http://C"],那怎麼知道它們各自屬於哪條記錄、屬於記錄中的哪個Name…這裡提出的兩個深度概念其實就是為瞭解決此失真問題,實現無損表達。
【譯者YY】在翻譯上面一段文字的譯者其實感覺很突兀,原文作者試圖擺出一個難題來引發讀者的思考(只有一個赤裸裸的欄位值如何弄清楚它所屬的記錄和結構),但是像我這樣按部就班的人,讀到這裡腦子裡思考的是一些更淺顯的問題。上面論文中雖然提到“列狀儲存已經證明了它適用於扁平的關係型資料”、“Dremel希望按欄位連續的儲存所有值來提升檢索效率”,但都是一筆帶過,沒有詳述這麼做為何能提升檢索效率?提升檢索效率的方法多種多樣,這麼做是不是唯一的、最好的方法?Dremel作者是怎麼一步步想到這個方法的(不要告訴我就是靈光一現、一揮而就的)?作者之所以省略,應該是有其他論文早就證明、推導過列狀儲存的優勢和誕生過程。但在此文中直接將其面臨的細節問題搬上檯面,引出兩個陌生的深度概念,不禁略顯突兀,讓人困惑。這裡會先保留這些困惑,直譯原文,在此節結束的譯者YY環節,譯者將嘗試與擁有同樣困惑的讀者一起,YY一下箇中奧妙。
重複深度。注意在圖2中的Code欄位。可以看到它在r1出現了3次。‘en-us’、‘en’在第一個Name中,而‘en-gb’在第三個Name中。結合了圖2你肯定能理解我上一句話並知道‘en-us’、‘en’、‘en-gb’出現在r1中的具體位置,但是不看圖的話呢?怎麼用文字,或者說是一種定義、一種屬性、一個數值,詮釋清楚它們出現的位置?這就是重複深度這個概念的作用,它能用一個數字告訴我們在路徑中的什麼重複欄位,此值重複了,以此來確定此值的位置(注意,這裡的重複,特指在某個repeated類型的欄位下“重複”出現的“重複”)。我們用深度0表示一個紀錄的開頭(虛擬根節點),深度的計算忽略非重複欄位(標籤不是repeated的欄位都不算在深度裡)。所以在Name.Language.Code這個路徑中,包含兩個重複欄位,Name和Language,如果在Name處重複,重複深度為1(虛擬根節點是0,下一級就是1),在Language處重複就是2,不可能在Code處重複,它是required類型,表示有且僅有一個;同樣的,在路徑Links.Forward中,Links是optional的,不參與深度計算(不可能重複),Forward是repeated的,因此只有在Forward處重複時重複深度為1。現在我們從上至下掃描紀錄r1。當我們遇到’en-us’,我們沒看到任何重複欄位,也就是說,重複深度是0。當我們遇到‘en’,欄位Language重複了(在‘en-us’的路徑裡已經出現過一個Language),所以重複深度是2.最終,當我們遇到’en-gb‘,Name重複了(Name在前面‘en-us’和‘en’的路徑裡已經出現過一次,而此Name後Language只出現過一次,沒有重複),所以重複深度是1。因此,r1中Code的值的重複深度是0、2、1.
【譯者注】樹的深度很好理解,根節點是0,下一級就是1,再下一級就是2,依次類推。但重複深度有所不同,它skip掉了所有非repeated類型的欄位,也就是說只有repeated類型才能算作一級深度。這麼做的原因是在已知schema的情況下,對於重複深度這個值而言,只需要repeated類型的參與就夠了(夠下面的split和裝配演算法所需了),沒必要按照完整的schema樹來計算深度值。
要注意第二個Name在r1中沒有包含任何Code值。為了確定‘en-gb’出現在第三個Name而不是第二個,我們添加一個NULL值在‘en’和‘en-gb’之間(3所示)。在Language欄位中Code欄位是必須值,所以它缺失意味著Language也沒有定義。一般來說,確定一個路徑中有哪些欄位被明確定義需要一些額外的資訊,也就是接下來介紹的定義深度。。
定義深度。路徑p中一個欄位的每個值,尤其是NULL,都有一個定義深度,說明了在p中有多少個可選欄位實際上是有值的。例如,我們看到r1沒有Backward連結,而link欄位是定義了的(在深度1)。為了保護此資訊,我們為Links.Backward列添加一個NULL值,並設定其定義深度為1。相似的,在r2中Name.Language.Country定義深度為1,而在r1中分別為2(‘en’處)和1(‘http://B’處)。
定義深度使用整型而不是簡單的is-null二進位位,這樣葉子節點的資料(比如,Name.Language.Country)才能包含足夠的資訊,指明它父節點出現的情況;在章節4.3給出了使用該資訊的具體例子。
【譯者注】定義深度從某種意義上是服務於重複深度的。在論文中其實有一個非常重要的理論介紹的不是很明顯,只是簡單的用sequentially、contiguously這樣的單詞帶過。這個理論就是在圖1右邊的列狀儲存中,所有列都是先儲存r1,後儲存r2,也就是說對所有的列,記錄儲存的順序是一致的。這個順序就像所有列值都包含的一個唯一主鍵,邏輯上能夠將被肢解出來的列值串在一起,知道它們屬於同一條記錄,這也是保證記錄被拆分之後不會失真的一個重要手段。既然順序是十分必要的不能失真的因素,那當某條記錄的某一列的值為空白時就不能簡單的跳過,必須顯式的為其儲存一個NULL值,以保證記錄順序有效。而NULL值本身能詮釋的資訊不夠,比如記錄中某個Name.Language.Country列為空白,那可能表示Country沒有值(如‘en’),也可能表示Language沒有值(如‘http://B’),這兩種情況在裝配演算法中是需要區分處理的,不能失真,所以才需要引出定義深度,能夠準確描述出此資訊。
上面大概提到的encoding保證了record的結構是無損的。這個比較好理解,此處就不過多介紹證明過程了。
Encoding(編碼)。每一列被儲存為塊的集合。每個塊包含重複深度和定義深度(下文統稱為深度)並且包含欄位值。NULLs沒有明確儲存因為他們根據定義深度可以確定:任何定義深度小於重複和可選欄位數量之和就意味著一個NULL。必須欄位的值不需要儲存定義深度。相似的,重複深度只在必要時儲存;比如,定義深度0意味著重複深度0,所以後者可省略。事實上,圖3中,沒有為DocId儲存深度。深度被打包為bit序列。我們只使用必需的位;比如,如果最大定義深度是3,我們只需使用2個bit。
【譯者總結】這裡又提到了塊(block)等概念,其實論文應該簡而言之——圖3中那多張類似“表”的結構(長得很像一張Table,暫稱其為“表”,無傷大雅),一張“表”就是一個column-stripe,就是塊(block)集合,“表”中的每一行就是一個block,就是圖1右邊所示的列狀儲存。物理上像一張張獨立的“表”,而邏輯上可以做到圖1右部所示的樹狀、列狀結構。在後面裝配狀態機器演算法一節中讀者能對此有較深理解
【譯者YY】讀完原文章節後,這裡YY一下上面提到的種種困惑
大家都知道任何的技術方案都不是空想出來的,肯定是因為某些痛處催生最佳化而得來的。譯者嘗試YY一下Dremel的推導過程:
step1. 首先,不考慮任何效能、最佳化,也不考慮分布式環境,只想要實現功能,最直接的做法就是按記錄儲存,比如把一個爬蟲抓來的一個Document(2中的r1、r2),直接儲存到一個GFS的檔案中。查詢時讀取出必需的檔案,解析為結構化資料,查詢出結果。這樣做肯定是能實現功能的,但是我們不會這麼做,因為它的劣勢十分明顯——我只需要讀取r1中的Name.URL資訊,這裡卻需要把整條記錄都讀出來,無用資料遠超過有效資料,是效能的極大浪費。(其實也就是論文中一直強調的面向記錄儲存的劣勢)
step2. 第一步中失敗就失敗在儲存時資料是非結構化的(儲存時非結構化就意味著需要讀取整條資料然後在記憶體中解析為結構化資料),那當前的最佳化目標就是做到在儲存介質裡資料就是結構化的(這樣就可以按結構唯讀取出必要的資料)。不用想的太遠,最經典的結構化儲存就是眾人皆知的關係型資料庫,它的表、列、行、關聯等概念足以在儲存時就按實現資料結構化,而且同樣能做到無損。對於嵌套型資料,關係型資料庫也早有設計表結構的定式了(其實就是一系列一對多的表結構),以Name.Language.Country這樣的路徑為例,就三張表,Name、Language、Country,三表包含自己內部的required欄位,同時包含父表的外鍵體現一對多的關聯關係(Country表包含Language_id,Language表包含Name_id)。這樣一個老掉牙的設計其實就能實現Dremel的一個重要目標——唯讀取必需的列。query要統計Country,就只需要遍曆Country表,如果還要統計Language欄位,那就是Language+Country兩表join查詢,一點不浪費(要知道Dremel查詢過程中對一個column-stripe的遍曆也是逃不掉的,就相當於這裡遍曆一張表了)。
第二步的YY有點不靠譜了,但是並沒有跑題,如果不考慮通用性、不考慮為嵌套結構建表多麼噁心(事實上利用動態schema將一個嵌套結構翻譯成關係表也不是難事),也不考慮酷不酷,為什麼不能這麼做呢?但是答案還是不能,原因有二。Language+Country這個樣本太簡單了,假如是Name+Country的統計(比如統計Country是‘xxx’的Name有多少個),問題就暴露的很明顯了,除了遍曆Name、Country表,還需要涉及到Language表(從Country表只能得到關聯的Language,需要三表join查詢Name+Language+Country才能得到結果),這就違背了Dremel的目標(只遍曆必需的表)。改變表設計是可以解決該問題——在Country表裡增加對Name的外部索引鍵關聯。那就繼續往極端情況去發散,假如Name之上還有一層呢?Country下面還有一層呢?這些層都可能會join查詢呢?最終你會發現按照這個方向,你需要在所有的表裡加上它所有祖先表的外鍵。不僅如此,上面曾經提到過為避免失真Dremel採用順序化儲存,順序就相當於一條記錄的主鍵,所有列值都要包含它,那就意味著在這個方案裡各張表還要再加上record_id這個外鍵。這樣一來已經足夠令人無法直視了(光是冗餘的外鍵儲存就浪費了很大的空間)。第二個原因其實很簡單,即使能動態schema、動態控製表結構,也不夠通用,較難擴充,不適合通用資料分析平台。
step3. 經過上面對第二步的糾結,我們發現擺在面前的難題其實就是2個,一是要解決每張表上可能無窮無盡的外鍵,二是這個方案要足夠通用化。再回過頭看看Dremel最終採用的方案,也許你會發現它其實就是在第二步上做了兩個天才的改良:第一,用重複深度+定義深度+順序這三劍客取代所有外鍵;第二,表設計時不區分required、repeated等類型,一視同仁,都設計為欄位值+重複深度+定義深度這樣三列。對於第一個改良,我只能說這三劍客確實是神器,它們足夠為任意兩張“表”的資料建立關聯關係(具體是如何做到請看下面4.3中的狀態機器演算法),足以取代繁雜的外鍵;對於第二個改良,其實也是為了支援通用的結構和演算法而妥協的結果,論文中不止一次的提到那兩個深度並不是對所有欄位都是必須的,比如DocId欄位的r和d永遠都是0(如果在關係型資料庫中設計表的話DocId只會作為某張表的一個列而不是獨立成為一張表)、所有欄位非NULL的定義深度永遠都相等,這些造成的些許浪費是為了通用化所付出的代價,但是問題不大,只要在儲存、計算時稍作手腳就可以盡量避免浪費(上面encoding一節提到如何做手腳)。
上面3個step的推導看似毫無章法,其實是邏輯緊密的,代表了譯者這樣一個普通coder為了實現一個目標而不斷反省最佳化的過程,並沒有任何跳躍性的思維,除了step3中那兩個改良,不是譯者的YY水平所能駕馭的。我這裡也妄自揣測一下,Google的天才們想出這樣的方案可能是基於兩條路線:一是對資料分析計算過程進行了高等級的抽象,建立了數學模型,協助了推導過程(數學題的好處就是它大部分情況下是有解的);另一種就是為了避免儲存record_id、避免處理複雜的外部索引鍵關聯,得出按順序儲存、按順序遍曆的思路,通過在“順序”二字上做足文章(各column-stripe中,記錄間是按固定順序的,那記錄內也可以按由上而下的固定順序,掃描時把“順序”發揮到極致),推匯出4.3中狀態機器演算法的大概流程,剩下最後一道難題——面前只有一個欄位值,沒有任何外鍵(關聯資訊),僅僅知道它和其他欄位值都是按嚴格順序儲存的,怎麼能知道它屬於哪條記錄以及在記錄內的確切位置?針對這一問題最終推匯出重複深度和定義深度的概念(在4.1剛開頭,作者就直接提出了擺在他面前的這最後一道難題去引讀者入戲——“只有欄位值不能表達清楚記錄的結構……這些值是來自兩個不同的記錄,還是相同的記錄中兩個重複的值?……”)。但對於按部就班、接受不了跳躍性思維的譯者來說,還是希望論文裡能詳細介紹這最後一道難題之前的推導過程的——為什麼要按照列狀結構、為什麼把記錄拆解的這麼零散、無損表示的方法有很多種為何要選擇這一種…… 所以才有如上YY,僅供讀者參考和矯正。另外論文中曾經提到“列狀儲存已經證明了它適用於扁平的關係型資料”,這也是為什麼譯者會聯想到基於關係型資料庫遇到的問題進行推導。
4.2 分割記錄為列狀儲存
上面我們展示了使用列狀格式表達出記錄結構並進行encoding。我們要面對的下一個挑戰是如何高效率製造column-stripe以及重複和定義深度。計算重複和定義深度的基礎的演算法在Appendix A中給出。演算法遍曆記錄結構然後計算每個列值的深度,為NULL時也不例外。在Google,經常會有一個schema包含了成千上萬的欄位,卻只有幾百個在記錄中被使用。因此,我們需要儘可能廉價的處理缺失欄位。為了製造column-stripe,我們建立一個樹狀結構,節點為欄位的writer,它的結構與schema中的欄位層級匹配。基礎的想法是只在欄位writer有自己的資料時執行更新,而不嘗試往下傳遞父節點狀態,除非絕對必要。子節點writer繼承父節點的深度值。當任意值被添加時,一個子writer將深度值同步到父節點。
4.3 記錄裝配
【譯者預讀】遍曆column-stripe時,面前是赤裸裸的欄位值(比如‘en’)和兩個int(重複深度、定義深度),沒有任何的關聯資訊,怎麼知道它屬於哪條記錄?處於記錄內的什麼位置?這就是本章節狀態機器演算法要解決的問題。譯者認為此演算法的核心在於“順序”二字,在沒有任何關聯資訊的情況下,記錄儲存順序就是record的主鍵,record內由上而下的欄位順序就是位置,而兩個int就是判斷順序的唯一線索。
從列狀資料高效的裝配記錄是很重要的。拿到一個欄位的子集,我們的目標是重組原始記錄就好像他們只包含選擇的欄位,其他列就當不存在。核心想法是:我們為每個欄位建立一個有限狀態機器(FSM),讀取欄位值和深度,然後順序的將值添加到輸出結果上。一個欄位的FSM狀態對應這個欄位的reader。重複深度驅動狀態變遷。一旦一個reader擷取了一個值,我們將查看下一個值的重複深度來決定狀態如何變化、跳轉到哪個reader。一個FSM狀態變化的始終就是一條記錄裝配的全過程。
圖4以Document為例,展示了一個FSM重組一條完整記錄的過程。開始狀態是DocId。一旦一個DocId值被讀取,FSM轉變到Links.Backward。擷取完所有重複欄位Backward的值,FSM跳向Links.Forward,依次類推。記錄裝配演算法細節在Appendix B中。
【譯者注】由於Appendix B的存在(原始論文中對核心演算法都附帶了原始碼和解釋,可在原文中查閱),這裡對狀態跳轉的介紹過於簡單,所以稍作補充。首先要確定3個思路:第一,所有資料都是按圖3那種類似一張張“表”的形式儲存的;第二,演算法會結合schema,按照一定次序一張張的讀取某些“表”(不是所有的,比如只統計Forward那就只會讀取這一張“表”),次序是不固定的,這個次序也就是狀態機器內狀態變遷的過程;第三,無論次序多麼不固定,它都是按記錄的順序不斷迴圈的(比如當前資料按順序儲存著r1,r2,r3… 那會進入第一個迴圈讀取並裝配出r1,第二個迴圈裝配出r2…),一個迴圈就是一個狀態機器從開始到結束的生命週期。
通過對上面三點的思考,可以想到掃描過程中需要不斷做一件非常重要的事情——掃描到某張“表”的某一行時要判斷這一行是不是屬於下一條記錄了,如果是,那為了繼續填充目前記錄,就需要跳至下一張“表”繼續掃描另一個欄位值,否則就用此行的值裝配目前記錄,如此重複直到需要跳出最後一張“表”,一次迴圈結束(一個狀態機器結束,一條記錄被裝配完畢,進入下一個迴圈)。理解了這一點就能理解為何要用狀態機器來實現演算法了,因為迴圈內就是不斷進行狀態判斷的過程。再深入思考一下,可以想到這個判斷不僅是簡單的“是否屬於下一條記錄”,對於repeated欄位的子孫欄位,還需要判斷是否屬於同一個記錄的下一個祖先、並且是哪個層級的祖先。舉個例子:
比如當前正在裝配r1中的某個Name的某個Language,掃描到了Name.Language.Country的某一行,如果此行重複深度為0,表示屬於下一條記錄,說明當前Name下Language不會再重複了(當前Name的所有Language裝配完畢),於是跳至Name.Url繼續裝配其他屬性;如果為1,表示屬於r1的下一個Name,也說明當前Name下Language不會再重複(當前Name的所有Language裝配完畢),那也跳到Name.Url;如果為2,表示屬於當前Name的下一個Language(當前Name的Language還未裝配完畢),那就走一個小迴圈,跳回上一個Name.Language.Code以裝配當前Name的下一個Language。
樣本還可以舉更多,但重要的是從樣本中抽象出狀態變化的本質,下面一段是論文對該本質的簡單描述
FSM的構造邏輯可以這麼表示:設定r為當前欄位讀取器為欄位f所返回的下一個重複深度。在schema樹中,我們找到它在深度r的祖先,然後選擇該祖先節點的第一個葉子欄位n。這給了我們一個FSM狀態變化(f;r)->n.比如,讓r=1作為f=Name.Language.Country讀取的下一個重複深度。它的祖先重複深度1的是Name,它的第一個葉子欄位是n=Name.Url。FSM組裝演算法細節在Appendix C中。
如果只有一個欄位子集需要被處理,FSM則更簡單。圖5描述了一個FSM,讀取欄位DocId和Name.Language.Country。圖中展示了輸出記錄s1和s2。注意我們的encoding和裝配演算法保護了欄位Country的封閉結構。這個對於應用訪問過程很重要,比如,Country出現在第二個Name的第一個Language,在XPath中,就可以用此運算式訪問:/Name[2]/Language[1]/Country.
(下文繼續......)
[轉載] Google大資料引擎Dremel剖析(1)