Dremel made simple with Parquet (Parquet 原理分析)


原版地址: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.

4b463c3f136464a93ad5d5e023f7a53b

如果把它換成行式存儲的,那麼數據就是一行挨着一行存儲的

061f34650df99d004b0d55886de89d05

按列存,有幾個好處:

  • 按列存,能夠更好地壓縮數據,因爲一列的數據一般都是同質的(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 表示.

d30ed6cd613b548076bd485bbc14191a

Maps,首先有一個 repeated field 在外面,裏面每個 field,是一個 group,group 裏面是 key-value 對,其中key 是 required 的.

0923a5c53ef576504134ed0169120d06

列式存儲格式

列式存儲,簡單來說就是三件事:1. 把一個嵌套的結構,映射爲若干列  2. 把一條嵌套的數據,寫入這些列裏. 3. 還能根據這些列,把原來的嵌套結構拼出來. 做到這三點,目的就達到了.

譯註:直觀來看,嵌套結構含有兩種信息:1. 字段的嵌套關係 2. 最終每個字段的值. 所以如何轉換成列式也可以從這裏下手,分別解決『值』和『嵌套關係』.

Parquet 的做法是,爲嵌套結構的 schema 中每個基本類型的 field,建立一個列. 若用一棵樹描述schema,基本類型的 field,就是樹的葉子.

上邊的 address book 結構用樹表示:

3017b6d50e1c34840c431b63ab8687c8

觀察上圖,其實最終的值,都是在基本類型的 field 中的,group 類型的 field 本身不含有值,是基本類型組合起來的.

對上圖藍色葉子節點,每個對應一個列,就可以把結構中所有的值存起來了,如下表.

c488b45d59f921c64c8306d7e50dd69a

現在,『值』的問題解決了,還剩『嵌套關係』,這種關係,用叫做 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:

f1d4bc6c968b20c3f7a65e35503af64e

fb745d043166a81830b585bd6b72d425

這裏 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:

ab3126ecd930394e41f5a1531820e4f5

不要讓 definition level 太大,這很重要,目標是所用的比特越少越好(後面會說)

Repetition level

對於一個帶 repeated field 的結構,轉成列式表示後,一列可能有多個值,這些值的一部分是一坨里的,另一部分可能是另一坨里的,但一條記錄的全部列都放在一列裏,傻傻分不清楚,所以需要一個值來區分怎麼分成不同的坨. 這個值就是 repetition level:對於列中的一個值,它告訴我這個值,是在哪個層級上,發生重複的.  這句話不太好理解,還是看例子吧.

2eb86bffb7000fa12279b27c3ae78ae2

這個結構轉成列式的,實際也只有一列: level1.level2,這一列的各個值,對應的 repeatiton level 如下:

8d7afc3f1773b5c406ce78cb73556a7f

爲了表述方便,稱在一個嵌套結構裏,一個 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 告訴我們,在從列式表達,還原嵌套結構的時候,是在哪一級插入新值的.

55bdadbcd530bfde58b3c1328c3cec16

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 的例子. 下表顯示了每一列 兩種標識可能出現的最大值,並解釋了爲什麼要比列所在深度小.

b5634952a7f05109eb3fbb738ea09734

單說 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):

4656c052cd5f18c21781b5ec39a53d24

爲了將這條嵌套結構的 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

最後列中存儲的東西是:

351de516e064ff3ea9668794a6e5fbb3

注意,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.

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