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.
若是把它換成行式存儲的,那麼數據就是一行挨着一行存儲的
按列存,有幾個好處:
首先是嵌套結構的模型,此處選取的模型就跟 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 屬性,有如下三種:
例如,下邊是一個 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 的兩個值描述. 有了這倆值,就能夠把原來的嵌套結構徹底還原出來,下文將詳細講解這兩個值究竟是什麼. ]
( 這倆 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 太大,這很重要,目標是所用的比特越少越好(後面會說)
對於一個帶 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 出現的層級:
下圖能夠看出,換句話說就是 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 整個遍歷一次,
最後列中存儲的東西是:
注意,NULL 值在這裏列出來,是爲了表述清晰,可是其實是不會存儲的. 列中小於最大 definition 值的(這個例子裏最大值是2),都應該是 NULL.
爲了經過列是存儲,還原重建這條嵌套結構的記錄,寫一個循環讀列中的值,
高效存儲 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 的相關說明,以及示例代碼.