Parquet存儲格式 - 論文翻譯【轉】

Apache Parquet是Hadoop生態圈中一種新型列式存儲格式,它能夠兼容Hadoop生態圈中大多數計算框架(Mapreduce、Spark等),被多種查詢引擎支持(Hive、Impala、Drill等),而且它是語言和平臺無關的。Parquet最初是由Twitter和Cloudera合做開發完成並開源,2015年5月從Apache的孵化器裏畢業成爲Apache頂級項目。html

Parquet最初的靈感來自Google於2010年發表的Dremel論文,文中介紹了一種支持嵌套結構的存儲格式,而且使用了列式存儲的方式提高查詢性能。java

 

論文是英文的,學習起來有點難度,幸虧找到了一篇翻譯的文章,對比着看很好。git

此處將該文章複製過來。github

地址: http://lastorder.me/dremel-make-simple-with-parquet.html數據庫

 

原文:Dremel made simple with Parquet | Twitter Engineering Blogapache

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,看事後恍然大悟. 如下就翻譯這篇博客,算是對本身閱讀的總結,也與更多人分享.json

 

對於優化『關係型數據庫上的分析任務』,列式存儲(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所需帶寬也會減少.
    • 因爲每列存的數據類型是相同的,we can use encodings better suited to the modern processors’ pipeline by making instruction branching more predictable. (沒想好怎麼翻譯,各位本身理解吧)

嵌套結構的模型

首先是嵌套結構的模型,此處選取的模型就跟 PB 相似. 多個 field 能夠造成一個 group,一個 field 能夠重複出現(叫作 repeated field),這樣就簡單地描述了嵌套和重複,沒有必要用更復雜的結構如 Map / List / Sets,由於這些都能用 group 和 repeated field 的各類組合來描述. (熟悉 PB 的人,對這裏說的東西應該很清楚,由於這就是跟 PB 同樣的,若是此處有疑惑,最好的方法是當即左轉出門去看一下 PB)

整個結構是從最外層一個 message 開始的. 每一個 field 有三個屬性:repetition、type、name. 一個 field 的 type 屬性,要麼是 group,要麼是基本類型(int, float, boolean, string),repetition 屬性,有如下三種:

  • required:出現,且只能出現 1 次.
  • 出現 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(https://github.com/Parquet/parquet-mr/tree/master/parquet-column/src/main/java/parquet/column/values/rle) 的高效的編碼方式.  一個不少值爲 NULL 的稀疏的列,壓縮後幾乎不怎麼佔空間,與此類似,一個幾乎老是有值的 optional 列,will cost very little overhead to store millions of 1s(在這個也沒想好怎麼翻譯,總之是開銷很小的意思了). 現實情況是,用於存儲 levels 的空間,能夠忽略不計. 以存儲一個扁平結構爲例(沒有嵌套),直接順序地把一列的值寫入,若是某個field是 optional 的,那就取一位用來標識是否爲 null.

完.

對於Parquet 裏面的具體實現,實在不想讀Java,有時間再看好了,或許也會補上 RLE + bit packing 的相關說明,以及示例代碼.

相關文章
相關標籤/搜索