原版地址:https://blog.twitter.com/engineering/en_us/a/2013/dremel-made-simple-with-parquet.html
寫在前面:
本來想翻譯一下的,結果發現已經有翻譯的版本了,仔細看了一下,有些許地方說的不是很清楚。就同時參考了原文,補了一些個人的理解上去。。
Google 對於傳說中3秒查詢 1 PB 數據的 Dremel,有一篇論文:Dremel: Interactive Analysis of Web-Scale Datasets http://research.google.com/pubs/pub36632.html. 這篇論文基本上在描述 Dremel 的數據存儲格式.
用容易理解但不準確的的話概括上面那篇論文,就是怎麼把一些嵌套的 Protobuff 結構(有相同 schema,如果你不熟悉 Protobuff,那類比 xml 或者 json),拆成若干個表存儲(就是邏輯上的二維表),然後通過查那些表,還能快速拼裝回原來的 PB(指 Protobuff 下同),再而且,如果你只關注嵌套結構中的某一個層級的某一部分,我可以只讀那一部分的數據,只把你關心的那一部分拼裝回來,所謂指哪打哪,由於不用讀其他不必要的部分,所以省掉了很多 IO,所以速度很快. 然而由於我很笨,所以一直感覺看的雲裏霧裏,直到 2013年9月11號,Twitter 的 Engineering blog 發了一篇博客叫 Dremel made simple with Parquet,看過後恍然大悟. 以下就翻譯這篇博客,算是對自己閱讀的總結,也與更多人分享.
對於優化『關係型數據庫上的分析任務』,列式存儲(Columnar Storage)是個比較流行的技術. 這一技術對處理大數據集的好處是有據可查的,可以參見諸多學術資料,以及一些用作分析的商業數據庫.(http://people.csail.mit.edu/tdanford/6830papers/stonebraker-cstore.pdf, http://www.vldb.org/pvldb/,http://www.monetdb.org/)
我們的目標是,對於一個查詢,儘量只讀取對這個查詢有用的數據,以此來讓磁盤 IO 最小. 用 Parquet,我們做到了把 Twitter 的大數據集上的 IO 縮減到原來的 1/3. 我們也做到了『指哪打哪』,也就是遍歷(scan)一個數據集的時候,如果只讀取部分列,那麼讀取時間也相應會縮短,時間縮短的比例就是那幾列的數據量佔全部列數據量的比例. 原理很簡單,就是不採用傳統的按行存儲,而是連續存儲一列的數據. 如果數據是扁平的(比如二維表形式),那列改成按列存儲毫無難度,處理嵌套的數據結構纔是真正的挑戰.
我們的開源項目 Parquet 是 Hadoop 上的一種支持列式存儲文件格式,起初只是 Twitter 和 Coudera 在合作開發,發展到現在已經有包括 Criteo公司 在內的許多其他貢獻者了. Parquet 用 Dremel 的論文中描述的方式,把嵌套結構存儲成扁平格式. 由於受益於這種技術,我們決定寫篇更通俗易懂的文章來向大家介紹它. 首先講一下嵌套數據結構的一般模型,然後會解釋爲什麼這個模型可以被一坨扁平的列(columns)所描述,最後討論爲什麼列式是高效的.
何謂列式存儲?看下面的例子,這就是三個列 A B C.
如果把它換成行式存儲的,那麼數據就是一行挨着一行存儲的
按列存,有幾個好處:
- 按列存,能夠更好地壓縮數據,因爲一列的數據一般都是同質的(homogenous). 對於hadoop集羣來說,空間節省非常可觀.
- I/O 會大大減少,因爲掃描(遍歷/scan)的時候,可以只讀其中部分列. 而且由於數據壓縮的更好的緣故,IO所需帶寬也會減小.
- 由於每列存的數據類型是相同的, 我們就可以根據不同列,選擇對應最優的 不同的壓縮方式,來優化。
嵌套結構的模型
首先是嵌套結構的模型,此處選取的模型就跟 PB(Protocol buffers)類似. 多個 field 可以形成一個 group,一個 field 可以重複出現(叫做 repeated field),這樣就簡單地描述了嵌套和重複,沒有必要用更復雜的結構如 Map / List / Sets,因爲這些都能用 group 和 repeated field 的各種組合來描述. (熟悉 PB 的人,對這裏說的東西應該很清楚,我覺得不知道PB也沒事,就可以理解成把repeated fields 和 groups做了一個簡單的映射)。
整個結構是從最外層一個 message 開始的. 每個 field 有三個屬性:repetition、type、name. 一個 field 的 type 屬性,要麼是 group,要麼是基本類型(int, float, boolean, string),repetition 屬性,有以下三種:
- required:出現,且只能出現 1 次.
- optional:出現 1 或 0 次.
- repeated:0 到 任意多次
例如,下邊是一個 address book 的 schema.
message AddressBook {
required string owner;
repeated string ownerPhoneNumbers;
repeated group contacts {
required string name;
optional string phoneNumber;
}
}
Lists(或者 Sets)可以用 repeated field 表示.
Maps,首先有一個 repeated field 在外面,裏面每個 field,是一個 group,group 裏面是 key-value 對,其中key 是 required 的.
列式存儲格式
列式存儲,簡單來說就是三件事:1. 把一個嵌套的結構,映射爲若干列 2. 把一條嵌套的數據,寫入這些列裏. 3. 還能根據這些列,把原來的嵌套結構拼出來. 做到這三點,目的就達到了.
譯註:直觀來看,嵌套結構含有兩種信息:1. 字段的嵌套關係 2. 最終每個字段的值. 所以如何轉換成列式也可以從這裏下手,分別解決『值』和『嵌套關係』.
Parquet 的做法是,爲嵌套結構的 schema 中每個基本類型的 field,建立一個列. 若用一棵樹描述schema,基本類型的 field,就是樹的葉子.
上邊的 address book 結構用樹表示:
觀察上圖,其實最終的值,都是在基本類型的 field 中的,group 類型的 field 本身不含有值,是基本類型組合起來的.
對上圖藍色葉子節點,每個對應一個列,就可以把結構中所有的值存起來了,如下表.
現在,『值』的問題解決了,還剩『嵌套關係』,這種關係,用叫做 repetition level 和 definition level 的兩個值描述. 有了這倆值,就可以把原來的嵌套結構完全還原出來,下文將詳細講解這兩個值到底是什麼. ]
Definition Level
( 這倆 Level 容易把人看糊塗,如果看文字描述沒明白,請看例子回頭再看文字描述)
爲支持嵌套結構,我們需要知道一個 field,到哪一層,變成 null 了(就是指field沒有定義),這就是 definition level 的功能. 設想,如果一個field 有定義,則它的parents 也肯定有定義,這是很顯然的. 如果一個 field 是沒有定義的,那有可能它的上級是沒定義的,但上上級有定義;也有可能是它的上級 和 上上級都沒定義,所以需要知道到底是從哪一級開始沒定義的,這是還原整條記錄所必須知道的.
譯註:(假設有一種一旦出現就每代必須遺傳的病)如果你得了這個病,那麼有可能你是第一個,你爸爸沒這個病; 也可能是從你爸爸開始纔出現這種病的(你爺爺還沒這種病); 也有可能是從你爺爺開始就已經得病了. 反過來,如果你爸爸沒這個病,那麼你爺爺肯定也是健康的. 你需要一個值,描述是從你家第幾代開始得病的,這個值就類似 definition level. 希望這比喻有助於理解.
對於扁平結構(就是沒有任何嵌套),optional field 可以用一個 bit 來表示是否有定義: 有:1, 無:0 .
對於嵌套結構,我們可以給每一級的 optional field 都加一個 bit 來記錄是否有定義,但其實沒有必要,因爲如上一段所說,因爲嵌套的特性上層沒定義,那下層當然也是沒定義的,所以只要知道從哪一級開始沒定義就可以了.
最後,required field 因爲總是有定義的,所以不需要 definition level.
還是看例子,下邊是一個簡單的嵌套的schema:
message ExampleDefinitionLevel {
optional group a {
optional group b {
optional string c;
}
}
}
轉換成列式,它只有一列 a.b.c,所有 field 都是 optional 的,都可能是 null. 如果 c 有定義,那麼 a b 作爲它的上層,也將是有定義的. 當 c 是 null 時候,可能是因爲它的某一級 parent 爲 null 才導致 c 是 null 的,這時爲了記錄嵌套結構的狀況,我們就需要保存最先出現 null 的那一層的深度了. 一共三個嵌套的 optional field,所以最大 definition level 是 3.
以下是各種情形下,a.b.c 的 definiton level:
這裏 definition level 不會大於3,等於 3 的時候,表示 c 有定義; 等於 0,1,2 的時候,指明瞭 null 出現的層級.
required 總是有定義的,所以不需要 definition level. 下面把 b 改成 required,看看情況如何.
message ExampleDefinitionLevel {
optional group a {
required group b {
optional string c;
}
}
}
現在最大的 definition level 是 2,因爲 b 不需要 definition level. 下面是各種情形下,a.b.c 的 definition level:
不要讓 definition level 太大,這很重要,目標是所用的比特越少越好(後面會說)
Repetition level
對於一個帶 repeated field 的結構,轉成列式表示後,一列可能有多個值,這些值的一部分是一坨里的,另一部分可能是另一坨里的,但一條記錄的全部列都放在一列裏,傻傻分不清楚,所以需要一個值來區分怎麼分成不同的坨. 這個值就是 repetition level:對於列中的一個值,它告訴我這個值,是在哪個層級上,發生重複的. 這句話不太好理解,還是看例子吧.
這個結構轉成列式的,實際也只有一列: level1.level2,這一列的各個值,對應的 repeatiton level 如下:
爲了表述方便,稱在一個嵌套結構裏,一個 repeated field 連續出現的一組值爲一個 List(只是爲了描述方便),比如 a,b,c 是一個 level2 List, d,e,f,g 是一個level2 List,h 是一個level2 List,i,j 是一個level2 List。a,b,c,d,e,f,g 所在的兩個 level2 list 是同一個 level1 List 裏的,h,i,j 所在的兩個 level2 List 是同一個 level1 List裏的。
那麼:repetition level 標示着新 List 出現的層級:
- 0 表示整條記錄的開始,此時應該創建新的 level1 List 和 level2 List
- 1 表示 level1 List 的開始,此時應該創建一個 level2 List
- 2 表示 level2 List中新的值產生,此時不新建 List,只在 List 裏插入新值.
下圖可以看出,換句話說就是 repetition level 告訴我們,在從列式表達,還原嵌套結構的時候,是在哪一級插入新值的.
repetiton = 0,標誌着一整條新 record 的開始. 在扁平化結構裏,沒有 repetition 所以 repetition level 總是 0. Only levels that are repeated need a Repetition level: optional 和 required 永遠也不會重複,在計算 repetition level 的時候,可將其跳過.
拆分與組裝
message AddressBook {
required string owner;
repeated string ownerPhoneNumbers;
repeated group contacts {
required string name;
optional string phoneNumber;
}
}
現在我們同時用這兩種標識(definition level, repetition level),重新考慮 Address book 的例子. 下表顯示了每一列 兩種標識可能出現的最大值,並解釋了爲什麼要比列所在深度小.
單說 contacts.phoneNumber 這一列,如果 手機號有定義,則 definition level 達到最大即2,如果有一個聯繫人是沒有手機號的,則 definition level是 1. 如果聯繫人是空的,則 definition level 是0.
AddressBook {
owner: "Julien Le Dem",
ownerPhoneNumbers: "555 123 4567",
ownerPhoneNumbers: "555 666 1337",
contacts: {
name: "Dmitriy Ryaboy",
phoneNumber: "555 987 6543",
},
contacts: {
name: "Chris Aniszczyk"
}
}
AddressBook {
owner: "A. Nonymous"
}
現在我們拿 contacts.phoneNumber 這一列來做說明.
若一條記錄是如下這樣的:
AddressBook {
contacts: {
phoneNumber: "555 987 6543"
}
contacts: {
}
}
AddressBook {
}
轉成列式之後,列中存儲的東西應該是這樣的(R = Repetiton Level, D = Definition Level):
爲了將這條嵌套結構的 record 轉換成列式,我們把這個 record 整個遍歷一次,
- contacts.phoneNumber: “555 987 6543”
- new record: R = 0
- value is defined: D = maximum (2)
- contacts.phoneNumber: null
- repeated contacts: R = 1
- only defined up to contacts: D = 1
- contacts: null
- new record: R = 0
- only defined up to AddressBook: D = 0
最後列中存儲的東西是:
注意,NULL 值在這裏列出來,是爲了表述清晰,但是實際上是不會存儲的. 列中小於最大 definition 值的(這個例子裏最大值是2),都應該是 NULL.
爲了通過列是存儲,還原重建這條嵌套結構的記錄,寫一個循環讀列中的值,
- R=0, D=2, Value = “555 987 6543”:
- R = 0 這是一個新的 record. 從根開始按照schema 重建結構,直到 repetition level 達到 2
- D = 2 是最大值,值是有定義的,所以此時將值插入.
- R=1, D=1:
- R = 1 level1 的 contact list 中一條新記錄
- D = 1 contacts 有定義,但 phoneNumber 沒定義,所建一個空的 contacts 即可.
- R=0, D=0:
- R = 0 一條新 record. 可以重建嵌套結構,直到達到 definition level 的值.
- D = 0 => contacts 是 null,所以最後拼裝出來的是一個空的 Address Book
高效存儲 Definition Levels 和 Repetiton Levels.
在存儲方面,問題很容易歸結爲:每一個基本類型的列,都要創建三個子列(R, D, Value). 然而,得益於我們所採用的這種列式的格式,三個子列的總開銷其實並不大. 因爲兩種 Levels的最大值,是由 schema 的深度決定的,並且通常只用幾個 bit 就夠用了(1個bit 就可表達1層嵌套,2個bit就可以表達3層嵌套了,3個bit就能夠表達7層嵌套了, [ 譯註:四層嵌套編程的時候就已經很噁心了,從編程和可維護角度,也不應該搞的嵌套層次太深(個人觀點) ]),對於上面的 AddressBook 實例,owner這一列,深度爲1,contacts.name 深度爲2,而這個表達能力已經很強了. R level 和 D level 的下限 總是0,上限總是列的深度. 如果一個 field 不是 repeated 的,就更好了,可以不需要 repetition level,而 required field 則不需要 definition level,這降低了兩種 level 的上限.
考慮特殊情況,所有 field 全是 required(相當於SQL 中的NOT NULL),repetition level 和 definition level 就完全不需要了(總是0,所以不需要存儲),直接存值就ok了. 如果我們要同時支持存儲扁平結構,那麼兩種 level也是一樣不需要存儲空間的.
由於以上這些特性,我們可以找到一種結合 Run Length Encoding 和 bit packing(之後會寫一篇關於各種常用的壓縮算法的博客,,) 的高效的編碼方式. 一個很多值爲 NULL 的稀疏的列,壓縮後幾乎不怎麼佔空間,與此相似,一個幾乎總是有值的 optional 列,可以壓縮成千上萬個相同的數據,如1. 現實狀況是,用於存儲 levels 的空間,可以忽略不計. 以存儲一個扁平結構爲例(沒有嵌套),直接順序地把一列的值寫入,如果某個field是 optional 的,那就取一位用來標識是否爲 null.