大数据:Parquet文件存储格式
一、Parquet的組成
Parquet僅僅是一種存儲(chǔ)格式,它是語(yǔ)言、平臺(tái)無(wú)關(guān)的,并且不需要和任何一種數(shù)據(jù)處理框架綁定,目前能夠和Parquet適配的組件包括下面這些,可以看出基本上通常使用的查詢引擎和計(jì)算框架都已適配,并且可以很方便的將其它序列化工具生成的數(shù)據(jù)轉(zhuǎn)換成Parquet格式。
- 查詢引擎: Hive, Impala, Pig, Presto, Drill, Tajo, HAWQ, IBM Big SQL
- 計(jì)算框架: MapReduce, Spark, Cascading, Crunch, Scalding, Kite
- 數(shù)據(jù)模型: Avro, Thrift, Protocol Buffers, POJOs
項(xiàng)目組成
Parquet項(xiàng)目由以下幾個(gè)子項(xiàng)目組成:
- parquet-format項(xiàng)目由java實(shí)現(xiàn),它定義了所有Parquet元數(shù)據(jù)對(duì)象,Parquet的元數(shù)據(jù)是使用Apache Thrift進(jìn)行序列化并存儲(chǔ)在Parquet文件的尾部。
- parquet-format項(xiàng)目由java實(shí)現(xiàn),它包括多個(gè)模塊,包括實(shí)現(xiàn)了讀寫Parquet文件的功能,并且提供一些和其它組件適配的工具,例如Hadoop Input/Output Formats、Hive Serde(目前Hive已經(jīng)自帶Parquet了)、Pig loaders等。
- parquet-compatibility項(xiàng)目,包含不同編程語(yǔ)言之間(JAVA和C/C++)讀寫文件的測(cè)試代碼。
- parquet-cpp項(xiàng)目,它是用于用于讀寫Parquet文件的C++庫(kù)。
下圖展示了Parquet各個(gè)組件的層次以及從上到下交互的方式。
- 數(shù)據(jù)存儲(chǔ)層定義了Parquet的文件格式,其中元數(shù)據(jù)在parquet-format中定義,包括Parquet原始類型定義、Page類型、編碼類型、壓縮類型等等。
- 對(duì)象轉(zhuǎn)換層完成其他對(duì)象模型與Parquet內(nèi)部數(shù)據(jù)模型的映射和轉(zhuǎn)換,Parquet的編碼方式使用的是striping and assembly算法。
- 對(duì)象模型層定義了如何讀取Parquet文件的內(nèi)容,這一層轉(zhuǎn)換包括Avro、Thrift、PB等序列化格式、Hive serde等的適配。并且為了幫助大家理解和使用,Parquet提供了org.apache.parquet.example包實(shí)現(xiàn)了java對(duì)象和Parquet文件的轉(zhuǎn)換。
數(shù)據(jù)模型
Parquet支持嵌套的數(shù)據(jù)模型,類似于Protocol Buffers,每一個(gè)數(shù)據(jù)模型的schema包含多個(gè)字段,每一個(gè)字段又可以包含多個(gè)字段,每一個(gè)字段有三個(gè)屬性:重復(fù)數(shù)、數(shù)據(jù)類型和字段名,重復(fù)數(shù)可以是以下三種:required(出現(xiàn)1次),repeated(出現(xiàn)0次或多次),optional(出現(xiàn)0次或1次)。每一個(gè)字段的數(shù)據(jù)類型可以分成兩種:group(復(fù)雜類型)和primitive(基本類型)。例如Dremel中提供的Document的schema示例,它的定義如下:
message Document {required int64 DocId;optional group Links {repeated int64 Backward;repeated int64 Forward;}repeated group Name {repeated group Language {required string Code;optional string Country;}optional string Url;} }?
可以把這個(gè)Schema轉(zhuǎn)換成樹狀結(jié)構(gòu),根節(jié)點(diǎn)可以理解為repeated類型,如下圖:?
可以看出在Schema中所有的基本類型字段都是葉子節(jié)點(diǎn),在這個(gè)Schema中一共存在6個(gè)葉子節(jié)點(diǎn),如果把這樣的Schema轉(zhuǎn)換成扁平式的關(guān)系模型,就可以理解為該表包含六個(gè)列。Parquet中沒(méi)有Map、Array這樣的復(fù)雜數(shù)據(jù)結(jié)構(gòu),但是可以通過(guò)repeated和group組合來(lái)實(shí)現(xiàn)這樣的需求。在這個(gè)包含6個(gè)字段的表中有以下幾個(gè)字段和每一條記錄中它們可能出現(xiàn)的次數(shù):
DocId int64 只能出現(xiàn)一次 Links.Backward int64 可能出現(xiàn)任意多次,但是如果出現(xiàn)0次則需要使用NULL標(biāo)識(shí) Links.Forward int64 同上 Name.Language.Code string 同上 Name.Language.Country string 同上 Name.Url string 同上由于在一個(gè)表中可能存在出現(xiàn)任意多次的列,對(duì)于這些列需要標(biāo)示出現(xiàn)多次或者等于NULL的情況,它是由Striping/Assembly算法實(shí)現(xiàn)的。
Striping/Assembly算法
上文介紹了Parquet的數(shù)據(jù)模型,在Document中存在多個(gè)非required列,由于Parquet一條記錄的數(shù)據(jù)分散的存儲(chǔ)在不同的列中,如何組合不同的列值組成一條記錄是由Striping/Assembly算法決定的,在該算法中列的每一個(gè)值都包含三部分:value、repetition level和definition level。
Repetition Levels
為了支持repeated類型的節(jié)點(diǎn),在寫入的時(shí)候該值等于它和前面的值在哪一層節(jié)點(diǎn)是不共享的。在讀取的時(shí)候根據(jù)該值可以推導(dǎo)出哪一層上需要?jiǎng)?chuàng)建一個(gè)新的節(jié)點(diǎn),例如對(duì)于這樣的一個(gè)schema和兩條記錄。
message nested {repeated group leve1 {repeated string leve2;} } r1:[[a,b,c,] , [d,e,f,g]] r2:[[h] , [i,j]]計(jì)算repetition level值的過(guò)程如下:
- value=a是一條記錄的開始,和前面的值(已經(jīng)沒(méi)有值了)在根節(jié)點(diǎn)(第0層)上是不共享的,所以repeated level=0.
- value=b它和前面的值共享了level1這個(gè)節(jié)點(diǎn),但是level2這個(gè)節(jié)點(diǎn)上是不共享的,所以repeated level=2.
- 同理value=c, repeated level=2.
- value=d和前面的值共享了根節(jié)點(diǎn)(屬于相同記錄),但是在level1這個(gè)節(jié)點(diǎn)上是不共享的,所以repeated level=1.
- value=h和前面的值不屬于同一條記錄,也就是不共享任何節(jié)點(diǎn),所以repeated level=0.
根據(jù)以上的分析每一個(gè)value需要記錄的repeated level值如下:
在讀取的時(shí)候,順序的讀取每一個(gè)值,然后根據(jù)它的repeated level創(chuàng)建對(duì)象,當(dāng)讀取value=a時(shí)repeated level=0,表示需要?jiǎng)?chuàng)建一個(gè)新的根節(jié)點(diǎn)(新記錄),value=b時(shí)repeated level=2,表示需要?jiǎng)?chuàng)建一個(gè)新的level2節(jié)點(diǎn),value=d時(shí)repeated level=1,表示需要?jiǎng)?chuàng)建一個(gè)新的level1節(jié)點(diǎn),當(dāng)所有列讀取完成之后可以創(chuàng)建一條新的記錄。本例中當(dāng)讀取文件構(gòu)建每條記錄的結(jié)果如下:
可以看出repeated level=0表示一條記錄的開始,并且repeated level的值只是針對(duì)路徑上的repeated類型的節(jié)點(diǎn),因此在計(jì)算該值的時(shí)候可以忽略非repeated類型的節(jié)點(diǎn),在寫入的時(shí)候?qū)⑵淅斫鉃樵摴?jié)點(diǎn)和路徑上的哪一個(gè)repeated節(jié)點(diǎn)是不共享的,讀取的時(shí)候?qū)⑵淅斫鉃樾枰谀囊粚觿?chuàng)建一個(gè)新的repeated節(jié)點(diǎn),這樣的話每一列最大的repeated level值就等于路徑上的repeated節(jié)點(diǎn)的個(gè)數(shù)(不包括根節(jié)點(diǎn))。減小repeated level的好處能夠使得在存儲(chǔ)使用更加緊湊的編碼方式,節(jié)省存儲(chǔ)空間。
Definition Levels
有了repeated level我們就可以構(gòu)造出一個(gè)記錄了,為什么還需要definition levels呢?由于repeated和optional類型的存在,可能一條記錄中某一列是沒(méi)有值的,假設(shè)我們不記錄這樣的值就會(huì)導(dǎo)致本該屬于下一條記錄的值被當(dāng)做當(dāng)前記錄的一部分,從而造成數(shù)據(jù)的錯(cuò)誤,因此對(duì)于這種情況需要一個(gè)占位符標(biāo)示這種情況。
definition level的值僅僅對(duì)于空值是有效的,表示在該值的路徑上第幾層開始是未定義的,對(duì)于非空的值它是沒(méi)有意義的,因?yàn)榉强罩翟谌~子節(jié)點(diǎn)是定義的,所有的父節(jié)點(diǎn)也肯定是定義的,因此它總是等于該列最大的definition levels。例如下面的schema。
message ExampleDefinitionLevel {optional group a {optional group b {optional string c;}} }它包含一個(gè)列a.b.c,這個(gè)列的的每一個(gè)節(jié)點(diǎn)都是optional類型的,當(dāng)c被定義時(shí)a和b肯定都是已定義的,當(dāng)c未定義時(shí)我們就需要標(biāo)示出在從哪一層開始時(shí)未定義的,如下面的值:
由于definition level只需要考慮未定義的值,而對(duì)于repeated類型的節(jié)點(diǎn),只要父節(jié)點(diǎn)是已定義的,該節(jié)點(diǎn)就必須定義(例如Document中的DocId,每一條記錄都該列都必須有值,同樣對(duì)于Language節(jié)點(diǎn),只要它定義了Code必須有值),所以計(jì)算definition level的值時(shí)可以忽略路徑上的required節(jié)點(diǎn),這樣可以減小definition level的最大值,優(yōu)化存儲(chǔ)。
一個(gè)完整的例子
本節(jié)我們使用Dremel論文中給的Document示例和給定的兩個(gè)值r1和r2展示計(jì)算repeated level和definition level的過(guò)程,這里把未定義的值記錄為NULL,使用R表示repeated level,D表示definition level。
首先看DocuId這一列,對(duì)于r1,DocId=10,由于它是記錄的開始并且是已定義的,所以R=0,D=0,同樣r2中的DocId=20,R=0,D=0。
對(duì)于Links.Forward這一列,在r1中,它是未定義的但是Links是已定義的,并且是該記錄中的第一個(gè)值,所以R=0,D=1,在r1中該列有兩個(gè)值,value1=10,R=0(記錄中該列的第一個(gè)值),D=2(該列的最大definition level)。
對(duì)于Name.Url這一列,r1中它有三個(gè)值,分別為url1=’http://A‘,它是r1中該列的第一個(gè)值并且是定義的,所以R=0,D=2;value2=’http://B‘,和上一個(gè)值value1在Name這一層是不相同的,所以R=1,D=2;value3=NULL,和上一個(gè)值value2在Name這一層是不相同的,所以R=1,但它是未定義的,而Name這一層是定義的,所以D=1。r2中該列只有一個(gè)值value3=’http://C‘,R=0,D=2.
最后看一下Name.Language.Code這一列,r1中有4個(gè)值,value1=’en-us’,它是r1中的第一個(gè)值并且是已定義的,所以R=0,D=2(由于Code是required類型,這一列repeated level的最大值等于2);value2=’en’,它和value1在Language這個(gè)節(jié)點(diǎn)是不共享的,所以R=2,D=2;value3=NULL,它是未定義的,但是它和前一個(gè)值在Name這個(gè)節(jié)點(diǎn)是不共享的,在Name這個(gè)節(jié)點(diǎn)是已定義的,所以R=1,D=1;value4=’en-gb’,它和前一個(gè)值在Name這一層不共享,所以R=1,D=2。在r2中該列有一個(gè)值,它是未定義的,但是Name這一層是已定義的,所以R=0,D=1.
?
Parquet文件格式
Parquet文件是以二進(jìn)制方式存儲(chǔ)的,所以是不可以直接讀取的,文件中包括該文件的數(shù)據(jù)和元數(shù)據(jù),因此Parquet格式文件是自解析的。在HDFS文件系統(tǒng)和Parquet文件中存在如下幾個(gè)概念。
- HDFS塊(Block):它是HDFS上的最小的副本單位,HDFS會(huì)把一個(gè)Block存儲(chǔ)在本地的一個(gè)文件并且維護(hù)分散在不同的機(jī)器上的多個(gè)副本,通常情況下一個(gè)Block的大小為256M、512M等。
- HDFS文件(File):一個(gè)HDFS的文件,包括數(shù)據(jù)和元數(shù)據(jù),數(shù)據(jù)分散存儲(chǔ)在多個(gè)Block中。
- 行組(Row Group):按照行將數(shù)據(jù)物理上劃分為多個(gè)單元,每一個(gè)行組包含一定的行數(shù),在一個(gè)HDFS文件中至少存儲(chǔ)一個(gè)行組,Parquet讀寫的時(shí)候會(huì)將整個(gè)行組緩存在內(nèi)存中,所以如果每一個(gè)行組的大小是由內(nèi)存大的小決定的,例如記錄占用空間比較小的Schema可以在每一個(gè)行組中存儲(chǔ)更多的行。
- 列塊(Column Chunk):在一個(gè)行組中每一列保存在一個(gè)列塊中,行組中的所有列連續(xù)的存儲(chǔ)在這個(gè)行組文件中。一個(gè)列塊中的值都是相同類型的,不同的列塊可能使用不同的算法進(jìn)行壓縮。
- 頁(yè)(Page):每一個(gè)列塊劃分為多個(gè)頁(yè),一個(gè)頁(yè)是最小的編碼的單位,在同一個(gè)列塊的不同頁(yè)可能使用不同的編碼方式。
文件格式
通常情況下,在存儲(chǔ)Parquet數(shù)據(jù)的時(shí)候會(huì)按照Block大小設(shè)置行組的大小,由于一般情況下每一個(gè)Mapper任務(wù)處理數(shù)據(jù)的最小單位是一個(gè)Block,這樣可以把每一個(gè)行組由一個(gè)Mapper任務(wù)處理,增大任務(wù)執(zhí)行并行度。Parquet文件的格式如下圖所示
上圖展示了一個(gè)Parquet文件的內(nèi)容,一個(gè)文件中可以存儲(chǔ)多個(gè)行組,文件的首位都是該文件的Magic Code,用于校驗(yàn)它是否是一個(gè)Parquet文件,Footer length了文件元數(shù)據(jù)的大小,通過(guò)該值和文件長(zhǎng)度可以計(jì)算出元數(shù)據(jù)的偏移量,文件的元數(shù)據(jù)中包括每一個(gè)行組的元數(shù)據(jù)信息和該文件存儲(chǔ)數(shù)據(jù)的Schema信息。除了文件中每一個(gè)行組的元數(shù)據(jù),每一頁(yè)的開始都會(huì)存儲(chǔ)該頁(yè)的元數(shù)據(jù),在Parquet中,有三種類型的頁(yè):數(shù)據(jù)頁(yè)、字典頁(yè)和索引頁(yè)。數(shù)據(jù)頁(yè)用于存儲(chǔ)當(dāng)前行組中該列的值,字典頁(yè)存儲(chǔ)該列值的編碼字典,每一個(gè)列塊中最多包含一個(gè)字典頁(yè),索引頁(yè)用來(lái)存儲(chǔ)當(dāng)前行組下該列的索引,目前Parquet中還不支持索引頁(yè),但是在后面的版本中增加。
在執(zhí)行MR任務(wù)的時(shí)候可能存在多個(gè)Mapper任務(wù)的輸入是同一個(gè)Parquet文件的情況,每一個(gè)Mapper通過(guò)InputSplit標(biāo)示處理的文件范圍,如果多個(gè)InputSplit跨越了一個(gè)Row Group,Parquet能夠保證一個(gè)Row Group只會(huì)被一個(gè)Mapper任務(wù)處理。
映射下推(Project PushDown)
說(shuō)到列式存儲(chǔ)的優(yōu)勢(shì),映射下推是最突出的,它意味著在獲取表中原始數(shù)據(jù)時(shí)只需要掃描查詢中需要的列,由于每一列的所有值都是連續(xù)存儲(chǔ)的,所以分區(qū)取出每一列的所有值就可以實(shí)現(xiàn)TableScan算子,而避免掃描整個(gè)表文件內(nèi)容。
在Parquet中原生就支持映射下推,執(zhí)行查詢的時(shí)候可以通過(guò)Configuration傳遞需要讀取的列的信息,這些列必須是Schema的子集,映射每次會(huì)掃描一個(gè)Row Group的數(shù)據(jù),然后一次性得將該Row Group里所有需要的列的Cloumn Chunk都讀取到內(nèi)存中,每次讀取一個(gè)Row Group的數(shù)據(jù)能夠大大降低隨機(jī)讀的次數(shù),除此之外,Parquet在讀取的時(shí)候會(huì)考慮列是否連續(xù),如果某些需要的列是存儲(chǔ)位置是連續(xù)的,那么一次讀操作就可以把多個(gè)列的數(shù)據(jù)讀取到內(nèi)存。
謂詞下推(Predicate PushDown)
在數(shù)據(jù)庫(kù)之類的查詢系統(tǒng)中最常用的優(yōu)化手段就是謂詞下推了,通過(guò)將一些過(guò)濾條件盡可能的在最底層執(zhí)行可以減少每一層交互的數(shù)據(jù)量,從而提升性能,例如”select count(1) from A Join B on A.id = B.id where A.a > 10 and B.b < 100”SQL查詢中,在處理Join操作之前需要首先對(duì)A和B執(zhí)行TableScan操作,然后再進(jìn)行Join,再執(zhí)行過(guò)濾,最后計(jì)算聚合函數(shù)返回,但是如果把過(guò)濾條件A.a > 10和B.b < 100分別移到A表的TableScan和B表的TableScan的時(shí)候執(zhí)行,可以大大降低Join操作的輸入數(shù)據(jù)。
無(wú)論是行式存儲(chǔ)還是列式存儲(chǔ),都可以在將過(guò)濾條件在讀取一條記錄之后執(zhí)行以判斷該記錄是否需要返回給調(diào)用者,在Parquet做了更進(jìn)一步的優(yōu)化,優(yōu)化的方法時(shí)對(duì)每一個(gè)Row Group的每一個(gè)Column Chunk在存儲(chǔ)的時(shí)候都計(jì)算對(duì)應(yīng)的統(tǒng)計(jì)信息,包括該Column Chunk的最大值、最小值和空值個(gè)數(shù)。通過(guò)這些統(tǒng)計(jì)值和該列的過(guò)濾條件可以判斷該Row Group是否需要掃描。另外Parquet未來(lái)還會(huì)增加諸如Bloom Filter和Index等優(yōu)化數(shù)據(jù),更加有效的完成謂詞下推。
在使用Parquet的時(shí)候可以通過(guò)如下兩種策略提升查詢性能:1、類似于關(guān)系數(shù)據(jù)庫(kù)的主鍵,對(duì)需要頻繁過(guò)濾的列設(shè)置為有序的,這樣在導(dǎo)入數(shù)據(jù)的時(shí)候會(huì)根據(jù)該列的順序存儲(chǔ)數(shù)據(jù),這樣可以最大化的利用最大值、最小值實(shí)現(xiàn)謂詞下推。2、減小行組大小和頁(yè)大小,這樣增加跳過(guò)整個(gè)行組的可能性,但是此時(shí)需要權(quán)衡由于壓縮和編碼效率下降帶來(lái)的I/O負(fù)載。
性能
相比傳統(tǒng)的行式存儲(chǔ),Hadoop生態(tài)圈近年來(lái)也涌現(xiàn)出諸如RC、ORC、Parquet的列式存儲(chǔ)格式,它們的性能優(yōu)勢(shì)主要體現(xiàn)在兩個(gè)方面:1、更高的壓縮比,由于相同類型的數(shù)據(jù)更容易針對(duì)不同類型的列使用高效的編碼和壓縮方式。2、更小的I/O操作,由于映射下推和謂詞下推的使用,可以減少一大部分不必要的數(shù)據(jù)掃描,尤其是表結(jié)構(gòu)比較龐大的時(shí)候更加明顯,由此也能夠帶來(lái)更好的查詢性能
上圖是展示了使用不同格式存儲(chǔ)TPC-H和TPC-DS數(shù)據(jù)集中兩個(gè)表數(shù)據(jù)的文件大小對(duì)比,可以看出Parquet較之于其他的二進(jìn)制文件存儲(chǔ)格式能夠更有效的利用存儲(chǔ)空間,而新版本的Parquet(2.0版本)使用了更加高效的頁(yè)存儲(chǔ)方式,進(jìn)一步的提升存儲(chǔ)空間
上圖展示了Twitter在Impala中使用不同格式文件執(zhí)行TPC-DS基準(zhǔn)測(cè)試的結(jié)果,測(cè)試結(jié)果可以看出Parquet較之于其他的行式存儲(chǔ)格式有較明顯的性能提升。
上圖展示了criteo公司在Hive中使用ORC和Parquet兩種列式存儲(chǔ)格式執(zhí)行TPC-DS基準(zhǔn)測(cè)試的結(jié)果,測(cè)試結(jié)果可以看出在數(shù)據(jù)存儲(chǔ)方面,兩種存儲(chǔ)格式在都是用snappy壓縮的情況下量中存儲(chǔ)格式占用的空間相差并不大,查詢的結(jié)果顯示Parquet格式稍好于ORC格式,兩者在功能上也都有優(yōu)缺點(diǎn),Parquet原生支持嵌套式數(shù)據(jù)結(jié)構(gòu),而ORC對(duì)此支持的較差,這種復(fù)雜的Schema查詢也相對(duì)較差;而Parquet不支持?jǐn)?shù)據(jù)的修改和ACID,但是ORC對(duì)此提供支持,但是在OLAP環(huán)境下很少會(huì)對(duì)單條數(shù)據(jù)修改,更多的則是批量導(dǎo)入。
項(xiàng)目發(fā)展
自從2012年由Twitter和Cloudera共同研發(fā)Parquet開始,該項(xiàng)目一直處于高速發(fā)展之中,并且在項(xiàng)目之初就將其貢獻(xiàn)給開源社區(qū),2013年,Criteo公司加入開發(fā)并且向Hive社區(qū)提交了向hive集成Parquet的patch(HIVE-5783),在Hive 0.13版本之后正式加入了Parquet的支持;之后越來(lái)越多的查詢引擎對(duì)此進(jìn)行支持,也進(jìn)一步帶動(dòng)了Parquet的發(fā)展。
目前Parquet正處于向2.0版本邁進(jìn)的階段,在新的版本中實(shí)現(xiàn)了新的Page存儲(chǔ)格式,針對(duì)不同的類型優(yōu)化編碼算法,另外豐富了支持的原始類型,增加了Decimal、Timestamp等類型的支持,增加更加豐富的統(tǒng)計(jì)信息,例如Bloon Filter,能夠盡可能得將謂詞下推在元數(shù)據(jù)層完成。
總結(jié)
本文介紹了一種支持嵌套數(shù)據(jù)模型對(duì)的列式存儲(chǔ)系統(tǒng)Parquet,作為大數(shù)據(jù)系統(tǒng)中OLAP查詢的優(yōu)化方案,它已經(jīng)被多種查詢引擎原生支持,并且部分高性能引擎將其作為默認(rèn)的文件存儲(chǔ)格式。通過(guò)數(shù)據(jù)編碼和壓縮,以及映射下推和謂詞下推功能,Parquet的性能也較之其它文件格式有所提升,可以預(yù)見(jiàn),隨著數(shù)據(jù)模型的豐富和Ad hoc查詢的需求,Parquet將會(huì)被更廣泛的使用。
參考
?
轉(zhuǎn)載于:https://www.cnblogs.com/ITtangtang/p/7681019.html
總結(jié)
以上是生活随笔為你收集整理的大数据:Parquet文件存储格式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: CorelDRAWX4的VBA插件开发(
- 下一篇: winform中listView