深入Parquet文件格式設(shè)計(jì)原理及實(shí)現(xiàn)細(xì)節(jié)
引言
思考半天決定講一個(gè)大家既熟悉又陌生的話題:Parquet文件。相信每個(gè)做大數(shù)據(jù)的工程師肯定都接觸過Parquet文件,都知道它是一種列式存儲(chǔ)格式,在面對(duì)OLAP查詢時(shí)可以減少讀取的數(shù)據(jù)量,提高查詢性能。但是對(duì)于它的格式具體是如何設(shè)計(jì)的,以及更重要的:為什么這樣設(shè)計(jì),可能就沒有那么清楚了。
這篇文章會(huì)帶你深入Parquet文件的原理和實(shí)現(xiàn)細(xì)節(jié),并試圖說明這些設(shè)計(jì)背后的意義。
Parquet解決什么問題
要理解一個(gè)系統(tǒng),首先第一個(gè)要提出的問題就是
這個(gè)系統(tǒng)為了解決什么問題?
也就是“這個(gè)系統(tǒng)提供了什么功能”。這是理解任何一個(gè)系統(tǒng)都需要關(guān)注的主線。只要心中有這條主線,就不會(huì)陷入各種細(xì)節(jié)的泥沼,而迷失了方向。
對(duì)于Parquet文件來說,這條主線在Twitter宣布Parquet開源的文章中就講得非常清楚
Parquet is an open-source columnar storage format for Hadoop.
...
Not all data people store in Hadoop is a simple table — complex nested structures abound. For example, one of Twitter’s common internal datasets has a schema nested seven levels deep, with over 80 leaf nodes.
也就是說,Twitter想在Hadoop上面設(shè)計(jì)一種新的列式存儲(chǔ)格式,這種格式可以保存包含嵌套結(jié)構(gòu)的數(shù)據(jù)。所以Parquet文件格式試圖解決的問題,用一句話來說,就是列式存儲(chǔ)一個(gè)類型包含嵌套結(jié)構(gòu)的數(shù)據(jù)集。
什么是“包含嵌套結(jié)構(gòu)的數(shù)據(jù)集”呢?舉個(gè)例子
假設(shè)我們要存儲(chǔ)1000個(gè)用戶的電話簿信息,其中每個(gè)用戶的電話簿用下面的這個(gè)結(jié)構(gòu)來表示
{ "owner": "Lei Li", "ownerPhoneNumbers": ["13354127165", "18819972777"], "contacts": [ { "name": "Meimei Han", "phoneNumber": "18561628306" }, { "name": "Lucy", "phoneNumber": "14550091758" } ] }
可以看到其中 ownerPhoneNumbers
字段是一個(gè)數(shù)組,而contacts
字段更是一個(gè)對(duì)象的數(shù)組。所以這個(gè)類型就不能用簡(jiǎn)單的二維表來存儲(chǔ),因?yàn)樗饲短捉Y(jié)構(gòu)。
如果把這個(gè)包含嵌套結(jié)構(gòu)的類型稱為AddressBook
,那么Parquet文件的目標(biāo)就是以面向列的方式保存AddressBook對(duì)象所構(gòu)成的數(shù)據(jù)集。
接下來再來講如何“以面向列的方式保存”。對(duì)于一個(gè)二維表的數(shù)據(jù),相信大家可以很容易地想象出怎樣列式地存儲(chǔ)這些數(shù)據(jù),例如
name | age | phoneNumber |
Lei Li | 16 | 13354127165 |
Meimei Han | 14 | 18561628306 |
Lucy | 15 | 14550091758 |
把它列式存儲(chǔ)就變成了
"Lei Li"
"Meimei Han"
"Lucy"
16
14
15
"13354127165"
"18561628306"
"14550091758"
但如果數(shù)據(jù)是一個(gè)包含數(shù)組和對(duì)象的復(fù)雜嵌套結(jié)構(gòu)呢?可能就不是這么直觀了。
在Parquet里面,保存嵌套結(jié)構(gòu)的方式是把所有字段打平以后順序存儲(chǔ)。
什么意思呢?以電話簿的例子來說,真正有數(shù)據(jù)的其實(shí)只有4列:
- owner
- ownerPhoneNumbers
- contacts.name
- contacts.phoneNumber
所以只需要把原始數(shù)據(jù)看做是一個(gè)4列的表即可。舉個(gè)例子:
假設(shè)有2條AddressBook記錄
{ "owner": "Lei Li", "ownerPhoneNumbers": ["13354127165", "18819972777"], "contacts": [ { "name": "Meimei Han", "phoneNumber": "18561628306" }, { "name": "Lucy", "phoneNumber": "14550091758" } ] }, { "owner": "Meimei Han", "ownerPhoneNumbers": ["15130245254"], "contacts": [ { "name": "Lily" }, { "name": "Lucy", "phoneNumber": "14550091758" } ] }
以列式保存之后,就會(huì)變成這樣
"Lei Li"
"Meimei Han"
"13354127165"
"18819972777"
"15130245254"
"Meimei Han"
"Lucy"
"Lily"
"Lucy"
"18561628306"
"14550091758"
"14550091758"
聰明的朋友肯定很快就發(fā)現(xiàn)了,因?yàn)樵冀Y(jié)構(gòu)里有個(gè)數(shù)組,長(zhǎng)度是不定的,如果只是把數(shù)據(jù)按順序存放,那就無法區(qū)分record之間的邊界,也就不知道每個(gè)值究竟屬于哪條record了。所以簡(jiǎn)單地打平是不可行的。
為了解決這個(gè)問題,Parquet的設(shè)計(jì)者引入了兩個(gè)新的概念:repetition level和definition level。這兩個(gè)值會(huì)保存額外的信息,可以用來重構(gòu)出數(shù)據(jù)原本的結(jié)構(gòu)。
關(guān)于repetition level和definition level具體是如何工作的,我會(huì)放到最后來講。這里只需要記住,Parquet文件對(duì)每個(gè)value,都同時(shí)保存了它們的repetition level和definition level,以便確定這個(gè)value屬于哪條record。
Parquet具體是怎么存放數(shù)據(jù)
接下來我們會(huì)深入Parquet文件的內(nèi)部,講講Parquet具體是怎么存放數(shù)據(jù)的。
首先放一張Parquet文件的整體結(jié)構(gòu)圖
其實(shí)Parquet還有一張更常見的結(jié)構(gòu)圖,官方也經(jīng)常引用,但我覺得層次不清晰,反而更讓人費(fèi)解,所以就自己畫了上面這張圖。
看過Parquet的整體結(jié)構(gòu)圖之后,可能你已經(jīng)被這些概念搞迷糊了:Header,Row Group,Column Chunk,Page,F(xiàn)ooter……沒關(guān)系,還是回到我們的主線——列式存儲(chǔ)一個(gè)包含嵌套結(jié)構(gòu)的數(shù)據(jù)集,我會(huì)把解決這個(gè)問題的思路自上而下地拆解,自然而然地就能產(chǎn)生這些概念。
Row Group
首先,因?yàn)槲覀円鎯?chǔ)的對(duì)象是一個(gè)數(shù)據(jù)集,而這個(gè)數(shù)據(jù)集往往包含上億條record,所以我們會(huì)進(jìn)行一次水平切分,把這些record切成多個(gè)“分片”,每個(gè)分片被稱為Row Group。為什么要進(jìn)行水平切分?雖然Parquet的官方文檔沒有解釋,但我認(rèn)為主要和HDFS有關(guān)。因?yàn)镠DFS存儲(chǔ)數(shù)據(jù)的單位是Block,默認(rèn)為128m。如果不對(duì)數(shù)據(jù)進(jìn)行水平切分,只要數(shù)據(jù)量足夠大(超過128m),一條record的數(shù)據(jù)就會(huì)跨越多個(gè)Block,會(huì)增加很多IO開銷。Parquet的官方文檔也建議,把HDFS的block size設(shè)置為1g,同時(shí)把Parquet的parquet.block.size也設(shè)置為1g,目的就是使一個(gè)Row Group正好存放在一個(gè)HDFS Block里面。
Column Chunk
在水平切分之后,就輪到列式存儲(chǔ)標(biāo)志性的垂直切分了。切分方式和上文提到的一致,會(huì)把一個(gè)嵌套結(jié)構(gòu)打平以后拆分成多列,其中每一列的數(shù)據(jù)所構(gòu)成的分片就被稱為Column Chunk。最后再把這些Column Chunk順序地保存。
Page
把數(shù)據(jù)拆解到Column Chunk級(jí)別之后,其結(jié)構(gòu)已經(jīng)相當(dāng)簡(jiǎn)單了。對(duì)Column Chunk,Parquet會(huì)進(jìn)行最后一次水平切分,分解成為一個(gè)個(gè)的Page。每個(gè)Page的默認(rèn)大小為1m。這次的水平切分又是為了什么?盡管Parquet的官方文檔又一次地沒有解釋,我認(rèn)為主要是為了讓數(shù)據(jù)讀取的粒度足夠小,便于單條數(shù)據(jù)或小批量數(shù)據(jù)的查詢。因?yàn)镻age是Parquet文件最小的讀取單位,同時(shí)也是壓縮的單位,如果沒有Page這一級(jí)別,壓縮就只能對(duì)整個(gè)Column Chunk進(jìn)行壓縮,而Column Chunk如果整個(gè)被壓縮,就無法從中間讀取數(shù)據(jù),只能把Column Chunk整個(gè)讀出來之后解壓,才能讀到其中的數(shù)據(jù)。
Header, Index和Footer
最后聊聊Data以外的Metadata部分,主要是:Header,Index和Footer。
Header
Header的內(nèi)容很少,只有4個(gè)字節(jié),本質(zhì)是一個(gè)magic number,用來指示文件類型。這個(gè)magic number目前有兩種變體,分別是“PAR1”和“PARE”。其中“PAR1”代表的是普通的Parquet文件,“PARE”代表的是加密過的Parquet文件。
Index
Index是Parquet文件的索引塊,主要為了支持“謂詞下推”(Predicate Pushdown)功能。謂詞下推是一種優(yōu)化查詢性能的技術(shù),簡(jiǎn)單地來說就是把查詢條件發(fā)給存儲(chǔ)層,讓存儲(chǔ)層可以做初步的過濾,把肯定不滿足查詢條件的數(shù)據(jù)排除掉,從而減少數(shù)據(jù)的讀取和傳輸量。舉個(gè)例子,對(duì)于csv文件,因?yàn)椴恢С种^詞下推,Spark只能把整個(gè)文件的數(shù)據(jù)全部讀出來以后,再用where條件對(duì)數(shù)據(jù)進(jìn)行過濾。而如果是Parquet文件,因?yàn)樽詭ax-Min索引,Spark就可以根據(jù)每個(gè)Page的max和min值,選擇是否要跳過這個(gè)Page,不用讀取這部分?jǐn)?shù)據(jù),也就減少了IO的開銷。
目前Parquet的索引有兩種,一種是Max-Min統(tǒng)計(jì)信息,一種是BloomFilter。其中Max-Min索引是對(duì)每個(gè)Page都記錄它所含數(shù)據(jù)的最大值和最小值,這樣某個(gè)Page是否不滿足查詢條件就可以通過這個(gè)Page的max和min值來判斷。BloomFilter索引則是對(duì)Max-Min索引的補(bǔ)充,針對(duì)value比較稀疏,max-min范圍比較大的列,用Max-Min索引的效果就不太好,BloomFilter可以克服這一點(diǎn),同時(shí)也可以用于單條數(shù)據(jù)的查詢。
Footer
Footer是Parquet元數(shù)據(jù)的大本營,包含了諸如schema,Block的offset和size,Column Chunk的offset和size等所有重要的元數(shù)據(jù)。另外Footer還承擔(dān)了整個(gè)文件入口的職責(zé),讀取Parquet文件的第一步就是讀取Footer信息,轉(zhuǎn)換成元數(shù)據(jù)之后,再根據(jù)這些元數(shù)據(jù)跳轉(zhuǎn)到對(duì)應(yīng)的block和column,讀取真正所要的數(shù)據(jù)。
關(guān)于Footer還有一個(gè)問題,就是為什么Parquet要把元數(shù)據(jù)放在文件的末尾而不是開頭?這主要是為了讓文件寫入的操作可以在一趟(one pass)內(nèi)完成。因?yàn)楹芏嘣獢?shù)據(jù)的信息需要把文件基本寫完以后才知道(例如總行數(shù),各個(gè)Block的offset等),如果要寫在文件開頭,就必須seek回文件的初始位置,大部分文件系統(tǒng)并不支持這種寫入操作(例如HDFS)。而如果寫在文件末尾,那么整個(gè)寫入過程就不需要任何回退。
Parquet如何把嵌套結(jié)構(gòu)編碼進(jìn)列式存儲(chǔ)
講完了Parquet的整體結(jié)構(gòu)之后,我們還剩下最后一個(gè)問題,也就是我之前埋下的伏筆:Parquet如何把嵌套結(jié)構(gòu)編碼進(jìn)列式存儲(chǔ)。在上文中我提到了Parquet是通過repetition level和definition level來解決這個(gè)問題,接下來就會(huì)詳細(xì)地講解一下這是怎么實(shí)現(xiàn)的。
還是上文用到的例子
{ "owner": "Lei Li", "ownerPhoneNumbers": ["13354127165", "18819972777"], "contacts": [ { "name": "Meimei Han", "phoneNumber": "18561628306" }, { "name": "Lucy", "phoneNumber": "14550091758" } ] }, { "owner": "Meimei Han", "ownerPhoneNumbers": ["15130245254"], "contacts": [ { "name": "Lily" }, { "name": "Lucy", "phoneNumber": "14550091758" } ] }
注意其中的第三列contacts.name,它有4個(gè)值”Meimei Han”,“Lucy”,“Lily”,“Lucy”,其中前兩個(gè)屬于前一條record,后兩個(gè)屬于后一條record。Parquet是如何表達(dá)這個(gè)信息的呢?它是用repetition level這個(gè)值來表達(dá)的。
repetition level主要用來表達(dá)數(shù)組類型字段的長(zhǎng)度,但它并不直接記錄長(zhǎng)度,而是通過記錄嵌套層級(jí)的變化來間接地表達(dá)長(zhǎng)度,即如果嵌套層級(jí)不變,那么說明數(shù)組還在延續(xù),如果嵌套層級(jí)變了,說明前一個(gè)數(shù)組結(jié)束了。如果在某個(gè)值上嵌套層級(jí)由0提高到了1,則這個(gè)值的repetition level就是0。如果在某個(gè)值的位置嵌套層級(jí)不變,則這個(gè)值的repetition level就是它的嵌套層級(jí)。對(duì)于上文中的例子,對(duì)應(yīng)的repetition level就是
Value | Repetition Level |
Meimei Han | 0 |
Lucy | 1 |
Lily | 0 |
Lucy | 1 |
還不是很明白?換個(gè)更典型的例子
[["a", "b"], ["c", "d", "e"]]
它對(duì)應(yīng)的repetition level會(huì)被編碼成
Value | Repetition Level |
a | 0 |
b | 2 |
c | 1 |
d | 2 |
e | 2 |
因?yàn)檫@個(gè)數(shù)組的嵌套層級(jí)是2,而”a”是從level 0到level 2的邊界,所以它的repetition level是0,”c”是從level 1到level 2的邊界,所以它的repetition level是1,其他字母的嵌套層級(jí)沒有發(fā)生變化,所以它們的repetition level就是2。
總結(jié)一下,repetition level主要用來表達(dá)數(shù)組的長(zhǎng)度。
講完了repetition level,再來講講definition level。與repetition level類似的,definition level主要用來表達(dá)null的位置。因?yàn)镻arquet文件里不會(huì)顯式地存儲(chǔ)null,所以通過definition level來判斷某個(gè)值是否是null。例如對(duì)于下面這個(gè)例子
AddressBook { contacts: { phoneNumber: "555 987 6543" } contacts: { } } AddressBook { }
對(duì)應(yīng)的definition level是這樣編碼的
Value | Definition Level |
555 987 6543 | 2 |
NULL | 1 |
NULL | 0 |
可以看到,凡是definition level小于嵌套層級(jí)的,都表達(dá)了這個(gè)值是null。而definition level具體的值則表達(dá)null出現(xiàn)在哪一個(gè)嵌套層級(jí)。
Parquet最難理解的部分到此就結(jié)束了。你或許會(huì)有疑問,如果對(duì)每個(gè)值都保存repetition level和definition level,那么這部分的數(shù)據(jù)量肯定不?。▋蓚€(gè)int32整數(shù),共8個(gè)字節(jié)),搞不好比本來要存的數(shù)據(jù)還要大,是不是本末倒置了?顯然Parquet也考慮到了這個(gè)問題,所以有很多的優(yōu)化措施,例如“對(duì)非數(shù)組類型的值不保存repetition level”,“對(duì)必填字段不保存definition level”等,真正存儲(chǔ)這兩個(gè)level時(shí),也使用的是bit-packing + RLE編碼,盡可能地對(duì)這部分?jǐn)?shù)據(jù)進(jìn)行了壓縮。篇幅有限,就不在這里展開了。
最后聊聊Parquet格式的演進(jìn)
Parquet格式最初由Twitter和Cloudera提出,作為RCFile格式的替代者,和早一個(gè)月提出的ORC格式類似。聯(lián)想到ORC是由Facebook和Hortonworks提出的,這兩者的競(jìng)爭(zhēng)關(guān)系不言自明。(有機(jī)會(huì)可以再來寫寫ORC格式)
Parquet在2013年宣布開源后,2014年被Cloudera捐給Apache基金會(huì),進(jìn)入孵化流程,并于2015年畢業(yè)成為頂級(jí)項(xiàng)目。Parquet的框架在進(jìn)入Apache基金會(huì)之前已經(jīng)基本成型,此后變化得也不快,主要新增了幾個(gè)功能:
- Column Index
- BloomFilter
- 模塊化加密
這些改動(dòng)主要是為了加強(qiáng)對(duì)謂詞下推的支持,但也有個(gè)副作用:文件體積變得更大了。
以上就是深入Parquet文件原理實(shí)現(xiàn)細(xì)節(jié)及設(shè)計(jì)意義的詳細(xì)內(nèi)容,更多關(guān)于Parquet文件原理設(shè)計(jì)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
簡(jiǎn)單了解JAVA SimpleDateFormat yyyy和YYYY的區(qū)別
這篇文章主要介紹了簡(jiǎn)單了解JAVA SimpleDateFormat yyyy和YYYY的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03Java通過Freemarker模板實(shí)現(xiàn)生成Word文件
FreeMarker是一款模板引擎: 即一種基于模板和要改變的數(shù)據(jù), 并用來生成輸出文本的通用工具。本文將根據(jù)Freemarker模板實(shí)現(xiàn)生成Word文件,需要的可以參考一下2022-09-09Java實(shí)戰(zhàn)之圖書管理系統(tǒng)的實(shí)現(xiàn)
這篇文章主要介紹了如何利用Java語言編寫一個(gè)圖書管理系統(tǒng),文中采用的技術(shù)有Springboot、SpringMVC、MyBatis、ThymeLeaf 等,需要的可以參考一下2022-03-03Spring Boot啟動(dòng)及退出加載項(xiàng)的方法
這篇文章主要介紹了Spring Boot啟動(dòng)及退出加載項(xiàng)的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Java由淺入深細(xì)數(shù)數(shù)組的操作下
數(shù)組對(duì)于每一門編程語言來說都是重要的數(shù)據(jù)結(jié)構(gòu)之一,當(dāng)然不同語言對(duì)數(shù)組的實(shí)現(xiàn)及處理也不盡相同。Java?語言中提供的數(shù)組是用來存儲(chǔ)固定大小的同類型元素2022-04-04Java中反射動(dòng)態(tài)代理接口的詳解及實(shí)例
這篇文章主要介紹了Java中反射動(dòng)態(tài)代理接口的詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-04-04Maven項(xiàng)目打包成可執(zhí)行Jar文件步驟解析
這篇文章主要介紹了Maven項(xiàng)目如何打包成可執(zhí)行Jar文件,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05