Parquet檔案結構筆記 Parquet檔案結構筆記 大資料:Parquet檔案儲存格式
一個Parquet檔案是
由一個header以及一個或多個block塊組成,以一個footer結尾。
header中只包含一個4個位元組的數字PAR1用來識別整個Parquet檔案格式。
檔案中所有的metadata都存在於footer中。
footer中的metadata包含了格式的版本資訊,schema資訊、key-value paris以及所有block中的metadata資訊。
footer中最後兩個欄位為一個以4個位元組長度的footer的metadata,以及同header中包含的一樣的PAR1。
讀取一個Parquet檔案時,需要完全讀取Footer的meatadata,
Parquet格式檔案不需要讀取sync markers這樣的標記分割查詢,因為所有block的邊界都儲存於footer的metadata中(因為metadata的寫入是在所有blocks塊寫入完成之後的,所以吸入操作包含的所有block的位置資訊都是存在於記憶體直到檔案close)
這裡注意,不像sequence files以及Avro資料格式檔案的header以及sync markers是用來分割blocks。Parquet格式檔案不需要sync markers,因此block的邊界儲存與footer的meatada中。
在Parquet檔案中,每一個block都具有一組Row group,她們是由一組Column chunk組成的列資料。
繼續往下,每一個column chunk中又包含了它具有的pages。
每個page就包含了來自於相同列的值.
Parquet同時使用更緊湊形式的編碼,當寫入Parquet檔案時,它會自動基於column的型別適配一個合適的編碼,比如,一個boolean形式的值將會被用於run-length encoding。
另一方面,Parquet檔案對於每個page支援標準的壓縮演算法比如支援Snappy,gzip以及LZO壓縮格式,也支援不壓縮。
Parquet格式的資料型別:
參考: 《Hadoop:The Definitive Guide, 4th Edition》
一、Parquet的組成
Parquet僅僅是一種儲存格式,它是語言、平臺無關的,並且不需要和任何一種資料處理框架繫結,
目前能夠和Parquet適配的元件包括下面這些,可以看出基本上通常使用的查詢引擎和計算框架都已適配,並且可以很方便的將其它序列化工具生成的資料轉換成Parquet格式。
- 查詢引擎: Hive, Impala, Pig, Presto, Drill, Tajo, HAWQ, IBM Big SQL
- 計算框架: MapReduce, Spark, Cascading, Crunch, Scalding, Kite
- 資料模型: Avro, Thrift, Protocol Buffers, POJOs
專案組成
Parquet專案由以下幾個子專案組成:
- parquet-format專案由java實現,它定義了所有Parquet元資料物件,Parquet的元資料是使用Apache Thrift進行序列化並存儲在Parquet檔案的尾部。
- parquet-format專案由java實現,它包括多個模組,包括實現了讀寫Parquet檔案的功能,並且提供一些和其它元件適配的工具,例如Hadoop Input/Output Formats、Hive Serde(目前Hive已經自帶Parquet了)、Pig loaders等。
- parquet-compatibility專案,包含不同程式語言之間(JAVA和C/C++)讀寫檔案的測試程式碼。
- parquet-cpp專案,它是用於用於讀寫Parquet檔案的C++庫。
下圖展示了Parquet各個元件的層次以及從上到下互動的方式。
- 資料儲存層定義了Parquet的檔案格式,其中元資料在parquet-format中定義,包括Parquet原始型別定義、Page型別、編碼型別、壓縮型別等等。
- 物件轉換層完成其他物件模型與Parquet內部資料模型的對映和轉換,Parquet的編碼方式使用的是striping and assembly演算法。
- 物件模型層定義了如何讀取Parquet檔案的內容,這一層轉換包括Avro、Thrift、PB等序列化格式、Hive serde等的適配。並且為了幫助大家理解和使用,Parquet提供了org.apache.parquet.example包實現了java物件和Parquet檔案的轉換。
資料模型
Parquet支援巢狀的資料模型,類似於Protocol Buffers,
每一個數據模型的schema包含多個欄位,每一個欄位又可以包含多個欄位,
每一個欄位有三個屬性:重複數、資料型別和欄位名,
重複數可以是以下三種:required(出現1次),repeated(出現0次或多次),optional(出現0次或1次)。
每一個欄位的資料型別可以分成兩種:group(複雜型別)和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; } }
可以把這個Schema轉換成樹狀結構,根節點可以理解為repeated型別,如下圖:
可以看出在Schema中所有的基本型別欄位都是葉子節點,在這個Schema中一共存在6個葉子節點,如果把這樣的Schema轉換成扁平式的關係模型,就可以理解為該表包含六個列。
Parquet中沒有Map、Array這樣的複雜資料結構,但是可以通過repeated和group組合來實現這樣的需求。
在這個包含6個欄位的表中有以下幾個欄位和每一條記錄中它們可能出現的次數:
DocId int64 只能出現一次 Links.Backward int64 可能出現任意多次,但是如果出現0次則需要使用NULL標識 Links.Forward int64 同上 Name.Language.Code string 同上 Name.Language.Country string 同上 Name.Url string 同上
由於在一個表中可能存在出現任意多次的列,對於這些列需要標示出現多次或者等於NULL的情況,它是由Striping/Assembly演算法實現的。
Striping/Assembly演算法
上文介紹了Parquet的資料模型,在Document中存在多個非required列,由於Parquet一條記錄的資料分散的儲存在不同的列中,如何組合不同的列值組成一條記錄是由Striping/Assembly演算法決定的,
在該演算法中列的每一個值都包含三部分:value、repetition level和definition level。
Repetition Levels
為了支援repeated型別的節點,在寫入的時候該值等於它和前面的值在哪一層節點是不共享的。在讀取的時候根據該值可以推匯出哪一層上需要建立一個新的節點,例如對於這樣的一個schema和兩條記錄。
message nested { repeated group leve1 { repeated string leve2; } } r1:[[a,b,c,] , [d,e,f,g]] r2:[[h] , [i,j]]
計算repetition level值的過程如下:
- value=a是一條記錄的開始,和前面的值(已經沒有值了)在根節點(第0層)上是不共享的,所以repeated level=0.
- value=b它和前面的值共享了level1這個節點,但是level2這個節點上是不共享的,所以repeated level=2.
- 同理value=c, repeated level=2.
- value=d和前面的值共享了根節點(屬於相同記錄),但是在level1這個節點上是不共享的,所以repeated level=1.
- value=h和前面的值不屬於同一條記錄,也就是不共享任何節點,所以repeated level=0.
根據以上的分析每一個value需要記錄的repeated level值如下:
在讀取的時候,順序的讀取每一個值,然後根據它的repeated level建立物件,
當讀取value=a時repeated level=0,表示需要建立一個新的根節點(新記錄),
value=b時repeated level=2,表示需要建立一個新的level2節點,
value=d時repeated level=1,表示需要建立一個新的level1節點,
當所有列讀取完成之後可以建立一條新的記錄。
本例中當讀取檔案構建每條記錄的結果如下:
可以看出
repeated level=0表示一條記錄的開始,
並且repeated level的值只是針對路徑上的repeated型別的節點,因此在計算該值的時候可以忽略非repeated型別的節點,
在寫入的時候
將其理解為該節點和路徑上的哪一個repeated節點是不共享的,
讀取的時候將
其理解為需要在哪一層建立一個新的repeated節點,
這樣的話每一列最大的repeated level值就等於路徑上的repeated節點的個數(不包括根節點)。
減小repeated level的好處能夠使得在儲存使用更加緊湊的編碼方式,節省儲存空間。
Definition Levels
有了repeated level我們就可以構造出一個記錄了,為什麼還需要definition levels呢?
由於repeated和optional型別的存在,可能一條記錄中某一列是沒有值的,
假設我們不記錄這樣的值就會導致本該屬於下一條記錄的值被當做當前記錄的一部分,從而造成資料的錯誤,
因此對於這種情況需要一個佔位符標示這種情況。
definition level的值
僅僅對於空值是有效的,
表示在該值的路徑上第幾層開始是未定義的,對於非空的值它是沒有意義的,因為非空值在葉子節點是定義的,所有的父節點也肯定是定義的,因此它總是等於該列最大的definition levels。
例如下面的schema。
message ExampleDefinitionLevel { optional group a { optional group b { optional string c; } } }
它包含一個列a.b.c,這個列的的每一個節點都是optional型別的,當c被定義時a和b肯定都是已定義的,
當c未定義時我們就需要標示出在從哪一層開始時未定義的,如下面的值:
由於definition level只需要考慮未定義的值,而對於repeated型別的節點,只要父節點是已定義的,該節點就必須定義
(例如Document中的DocId,每一條記錄都該列都必須有值,同樣對於Language節點,只要它定義了Code必須有值),
所以計算definition level的值時可以忽略路徑上的required節點,這樣可以減小definition level的最大值,優化儲存。
一個完整的例子
本節我們使用Dremel論文中給的Document示例和給定的兩個值r1和r2展示計算repeated level和definition level的過程,
這裡把未定義的值記錄為NULL,使用R表示repeated level,D表示definition level。
首先看DocuId這一列,對於r1,DocId=10,由於它是記錄的開始並且是已定義的,所以R=0,D=0,同樣r2中的DocId=20,R=0,D=0。
對於Links.Forward這一列,在r1中,它是未定義的但是Links是已定義的,並且是該記錄中的第一個值,所以R=0,D=1,在r1中該列有兩個值,value1=10,R=0(記錄中該列的第一個值),D=2(該列的最大definition level)。
對於Name.Url這一列,r1中它有三個值,分別為url1=’http://A‘,它是r1中該列的第一個值並且是定義的,所以R=0,D=2;value2=’http://B‘,和上一個值value1在Name這一層是不相同的,所以R=1,D=2;value3=NULL,和上一個值value2在Name這一層是不相同的,所以R=1,但它是未定義的,而Name這一層是定義的,所以D=1。r2中該列只有一個值value3=’http://C‘,R=0,D=2.
最後看一下Name.Language.Code這一列,r1中有4個值,value1=’en-us’,它是r1中的第一個值並且是已定義的,所以R=0,D=2(由於Code是required型別,這一列repeated level的最大值等於2);value2=’en’,它和value1在Language這個節點是不共享的,所以R=2,D=2;value3=NULL,它是未定義的,但是它和前一個值在Name這個節點是不共享的,在Name這個節點是已定義的,所以R=1,D=1;value4=’en-gb’,它和前一個值在Name這一層不共享,所以R=1,D=2。在r2中該列有一個值,它是未定義的,但是Name這一層是已定義的,所以R=0,D=1.
Parquet檔案格式
Parquet檔案是
以二進位制方式儲存的,所以是不可以直接讀取的,
檔案中包括該檔案的資料和元資料,因此Parquet格式檔案是自解析的。
在HDFS檔案系統和Parquet檔案中存在如下幾個概念。
- HDFS塊(Block):它是HDFS上的最小的副本單位,HDFS會把一個Block儲存在本地的一個檔案並且維護分散在不同的機器上的多個副本,通常情況下一個Block的大小為256M、512M等。
- HDFS檔案(File):一個HDFS的檔案,包括資料和元資料,資料分散儲存在多個Block中。
- 行組(Row Group):按照行將資料物理上劃分為多個單元,每一個行組包含一定的行數,在一個HDFS檔案中至少儲存一個行組,Parquet讀寫的時候會將整個行組快取在記憶體中,所以如果每一個行組的大小是由記憶體大的小決定的,例如記錄佔用空間比較小的Schema可以在每一個行組中儲存更多的行。
- 列塊(Column Chunk):在一個行組中每一列儲存在一個列塊中,行組中的所有列連續的儲存在這個行組檔案中。一個列塊中的值都是相同型別的,不同的列塊可能使用不同的演算法進行壓縮。
- 頁(Page):每一個列塊劃分為多個頁,一個頁是最小的編碼的單位,在同一個列塊的不同頁可能使用不同的編碼方式。
檔案格式
通常情況下,在儲存Parquet資料的時候會按照Block大小設定行組的大小,由於一般情況下每一個Mapper任務處理資料的最小單位是一個Block,這樣可以把每一個行組由一個Mapper任務處理,增大任務執行並行度。
Parquet檔案的格式如下圖所示
上圖展示了一個Parquet檔案的內容,
一個檔案中
可以儲存多個行組,
檔案的首位都是該檔案的Magic Code,用於校驗它是否是一個Parquet檔案,
Footer length了檔案元資料的大小,通過該值和檔案長度可以計算出元資料的偏移量,
檔案的元資料中包括每一個行組的元資料資訊和該檔案儲存資料的Schema資訊。
除了檔案中每一個行組的元資料,每一頁的開始都會儲存該頁的元資料,
在Parquet中,有三種類型的頁:資料頁、字典頁和索引頁。
資料頁用於儲存當前行組中該列的值,
字典頁儲存該列值的編碼字典,每一個列塊中最多包含一個字典頁,
索引頁用來儲存當前行組下該列的索引,
目前Parquet中還不支援索引頁,但是在後面的版本中增加。
在執行MR任務的時候可能存在多個Mapper任務的輸入是同一個Parquet檔案的情況,
每一個Mapper通過InputSplit標示處理的檔案範圍,如果多個InputSplit跨越了一個Row Group,Parquet能夠保證一個Row Group只會被一個Mapper任務處理。
對映下推(Project PushDown)
說到列式儲存的優勢,對映下推是最突出的,它意味著在獲取表中原始資料時只需要掃描查詢中需要的列,由於每一列的所有值都是連續儲存的,所以分割槽取出每一列的所有值就可以實現TableScan運算元,而避免掃描整個表文件內容。
在Parquet中原生就支援對映下推,執行查詢的時候可以通過Configuration傳遞需要讀取的列的資訊,這些列必須是Schema的子集,對映每次會掃描一個Row Group的資料,然後一次性得將該Row Group裡所有需要的列的Cloumn Chunk都讀取到記憶體中,每次讀取一個Row Group的資料能夠大大降低隨機讀的次數,除此之外,Parquet在讀取的時候會考慮列是否連續,如果某些需要的列是儲存位置是連續的,那麼一次讀操作就可以把多個列的資料讀取到記憶體。
謂詞下推(Predicate PushDown)
在資料庫之類的查詢系統中最常用的優化手段就是謂詞下推了,通過將一些過濾條件儘可能的在最底層執行可以減少每一層互動的資料量,從而提升效能,
例如”select count(1) from A Join B on A.id = B.id where A.a > 10 and B.b < 100”SQL查詢中,在處理Join操作之前需要首先對A和B執行TableScan操作,然後再進行Join,再執行過濾,最後計算聚合函式返回,但是如果把過濾條件A.a > 10和B.b < 100分別移到A表的TableScan和B表的TableScan的時候執行,可以大大降低Join操作的輸入資料。
無論是行式儲存還是列式儲存,都可以在將過濾條件在讀取一條記錄之後執行以判斷該記錄是否需要返回給呼叫者,在Parquet做了更進一步的優化,優化的方法時對每一個Row Group的每一個Column Chunk在儲存的時候都計算對應的統計資訊,包括該Column Chunk的最大值、最小值和空值個數。通過這些統計值和該列的過濾條件可以判斷該Row Group是否需要掃描。另外Parquet未來還會增加諸如Bloom Filter和Index等優化資料,更加有效的完成謂詞下推。
在使用Parquet的時候可以通過如下兩種策略提升查詢效能:
1、類似於關係資料庫的主鍵,對需要頻繁過濾的列設定為有序的,這樣在匯入資料的時候會根據該列的順序儲存資料,這樣可以最大化的利用最大值、最小值實現謂詞下推。
2、減小行組大小和頁大小,這樣增加跳過整個行組的可能性,但是此時需要權衡由於壓縮和編碼效率下降帶來的I/O負載。
效能
相比傳統的行式儲存,Hadoop生態圈近年來也湧現出諸如RC、ORC、Parquet的列式儲存格式,它們的效能優勢主要體現在兩個方面:
1、更高的壓縮比,由於相同型別的資料更容易針對不同型別的列使用高效的編碼和壓縮方式。
2、更小的I/O操作,由於對映下推和謂詞下推的使用,可以減少一大部分不必要的資料掃描,尤其是表結構比較龐大的時候更加明顯,由此也能夠帶來更好的查詢效能
上圖是展示了使用不同格式儲存TPC-H和TPC-DS資料集中兩個表資料的檔案大小對比,可以看出Parquet較之於其他的二進位制檔案儲存格式能夠更有效的利用儲存空間,而新版本的Parquet(2.0版本)使用了更加高效的頁儲存方式,進一步的提升儲存空間
上圖展示了Twitter在Impala中使用不同格式檔案執行TPC-DS基準測試的結果,測試結果可以看出Parquet較之於其他的行式儲存格式有較明顯的效能提升。
上圖展示了criteo公司在Hive中使用ORC和Parquet兩種列式儲存格式執行TPC-DS基準測試的結果,測試結果可以看出在資料儲存方面,兩種儲存格式在都是用snappy壓縮的情況下量中儲存格式佔用的空間相差並不大,查詢的結果顯示Parquet格式稍好於ORC格式,兩者在功能上也都有優缺點,Parquet原生支援巢狀式資料結構,而ORC對此支援的較差,這種複雜的Schema查詢也相對較差;而Parquet不支援資料的修改和ACID,但是ORC對此提供支援,但是在OLAP環境下很少會對單條資料修改,更多的則是批量匯入。
專案發展
自從2012年由Twitter和Cloudera共同研發Parquet開始,該專案一直處於高速發展之中,並且在專案之初就將其貢獻給開源社群,2013年,Criteo公司加入開發並且向Hive社群提交了向hive整合Parquet的patch(HIVE-5783),在Hive 0.13版本之後正式加入了Parquet的支援;之後越來越多的查詢引擎對此進行支援,也進一步帶動了Parquet的發展。
目前Parquet正處於向2.0版本邁進的階段,在新的版本中實現了新的Page儲存格式,針對不同的型別優化編碼演算法,另外豐富了支援的原始型別,增加了Decimal、Timestamp等型別的支援,增加更加豐富的統計資訊,例如Bloon Filter,能夠儘可能得將謂詞下推在元資料層完成。
總結
本文介紹了一種支援巢狀資料模型對的列式儲存系統Parquet,作為大資料系統中OLAP查詢的優化方案,它已經被多種查詢引擎原生支援,並且部分高效能引擎將其作為預設的檔案儲存格式。通過資料編碼和壓縮,以及對映下推和謂詞下推功能,Parquet的效能也較之其它檔案格式有所提升,可以預見,隨著資料模型的豐富和Ad hoc查詢的需求,Parquet將會被更廣泛的使用。
參考
- Dremel: Interactive Analysis of Web-Scale Datasets
- Dremel made simple with Parquet
- Parquet: Columnar storage for the people
- Efficient Data Storage for Analytics with Apache Parquet 2.0
- 深入分析Parquet列式儲存格式
- Apache Parquet Document
- http://blog.csdn.net/yu616568/article/details/50993491
- http://blog.csdn.net/yu616568/article/details/51188479