深刻分析Parquet列式存儲格式

深刻分析Parquet列式存儲格式

做者 梁堰波                發佈於        2015年8月7日        |                                                        討論                             html

     

Parquet是面向分析型業務的列式存儲格式,由Twitter和Cloudera合做開發,2015年5月從Apache的孵化器裏畢業成爲Apache頂級項目,最新的版本是1.8.0。算法

列式存儲

列式存儲和行式存儲相比有哪些優點呢?apache

  1. 能夠跳過不符合條件的數據,只讀取須要的數據,下降IO數據量。緩存

  2. 壓縮編碼能夠下降磁盤存儲空間。因爲同一列的數據類型是同樣的,可使用更高效的壓縮編碼(例如Run Length Encoding和Delta Encoding)進一步節約存儲空間。微信

  3. 只讀取須要的列,支持向量運算,可以獲取更好的掃描性能。app

當時Twitter的日增數據量達到壓縮以後的100TB+,存儲在HDFS上,工程師會使用多種計算框架(例如MapReduce, Hive, Pig等)對這些數據作分析和挖掘;日誌結構是複雜的嵌套數據類型,例如一個典型的日誌的schema有87列,嵌套了7層。因此須要設計一種列式存儲格 式,既能支持關係型數據(簡單數據類型),又能支持複雜的嵌套類型的數據,同時可以適配多種數據處理框架。框架

關係型數據的列式存儲,能夠將每一列的值直接排列下來,不用引入其餘的概念,也不會丟失數據。關係型數據的列式存儲比較好理解,而嵌套類型數據的列 存儲則會遇到一些麻煩。如圖1所示,咱們把嵌套數據類型的一行叫作一個記錄(record),嵌套數據類型的特色是一個record中的column除了 能夠是Int, Long, String這樣的原語(primitive)類型之外,還能夠是List, Map, Set這樣的複雜類型。在行式存儲中一行的多列是連續的寫在一塊兒的,在列式存儲中數據按列分開存儲,例如能夠只讀取A.B.C這一列的數據而不去讀A.E 和A.B.D,那麼如何根據讀取出來的各個列的數據重構出一行記錄呢?

圖1 行式存儲和列式存儲

Google的Dremel系 統解決了這個問題,核心思想是使用「record shredding and assembly algorithm」來表示複雜的嵌套數據類型,同時輔以按列的高效壓縮和編碼技術,實現下降存儲空間,提升IO效率,下降上層應用延遲。Parquet 就是基於Dremel的數據模型和算法實現的。

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是如何與這些組件協做的呢?這個能夠經過圖2來講明。數據從內存到Parquet文件或者反過來的過程主要由如下三個部分組成:

1, 存儲格式(storage format)

parquet-format項目定義了Parquet內部的數據類型、存儲格式等。

2, 對象模型轉換器(object model converters)

這部分功能由parquet-mr項目來實現,主要完成外部對象模型與Parquet內部數據類型的映射。

3, 對象模型(object models)

對象模型能夠簡單理解爲內存中的數據表示,Avro, Thrift, Protocol Buffers, Hive SerDe, Pig Tuple, Spark SQL InternalRow等這些都是對象模型。Parquet也提供了一個example object model 幫助你們理解。

例如parquet-mr項目裏的parquet-pig項目就是負責把內存中的Pig Tuple序列化並按列存儲成Parquet格式,以及反過來把Parquet文件的數據反序列化成Pig Tuple。

這裏須要注意的是Avro, Thrift, Protocol Buffers都有他們本身的存儲格式,可是Parquet並無使用他們,而是使用了本身在parquet-format項目裏定義的存儲格式。因此若是你的應用使用了Avro等對象模型,這些數據序列化到磁盤仍是使用的parquet-mr定義的轉換器把他們轉換成Parquet本身的存儲格式。

圖2 Parquet項目的結構

Parquet數據模型

理解Parquet首先要理解這個列存儲格式的數據模型。咱們以一個下面這樣的schema和數據爲例來講明這個問題。

message AddressBook {
 required string owner;
 repeated string ownerPhoneNumbers;
 repeated group contacts {
   required string name;
   optional string phoneNumber;
 }
}

這個schema中每條記錄表示一我的的AddressBook。有且只有一個owner,owner能夠有0個或者多個 ownerPhoneNumbers,owner能夠有0個或者多個contacts。每一個contact有且只有一個name,這個contact的 phoneNumber無關緊要。這個schema能夠用圖3的樹結構來表示。

每一個schema的結構是這樣的:根叫作message,message包含多個fields。每一個field包含三個屬 性:repetition, type, name。repetition能夠是如下三種:required(出現1次),optional(出現0次或者1次),repeated(出現0次或者 屢次)。type能夠是一個group或者一個primitive類型。

Parquet格式的數據類型沒有複雜的Map, List, Set等,而是使用repeated fields 和 groups來表示。例如List和Set能夠被表示成一個repeated field,Map能夠表示成一個包含有key-value 對的repeated field,並且key是required的。

圖3 AddressBook的樹結構表示

Parquet文件的存儲格式

那麼如何把內存中每一個AddressBook對象按照列式存儲格式存儲下來呢?

在Parquet格式的存儲中,一個schema的樹結構有幾個葉子節點,實際的存儲中就會有多少column。例如上面這個schema的數據存儲實際上有四個column,如圖4所示。

圖4 AddressBook實際存儲的列

Parquet文件在磁盤上的分佈狀況如圖5所示。全部的數據被水平切分紅Row group,一個Row group包含這個Row group對應的區間內的全部列的column chunk。一個column chunk負責存儲某一列的數據,這些數據是這一列的Repetition levels, Definition levels和values(詳見後文)。一個column chunk是由Page組成的,Page是壓縮和編碼的單元,對數據模型來講是透明的。一個Parquet文件最後是Footer,存儲了文件的元數據信 息和統計信息。Row group是數據讀寫時候的緩存單元,因此推薦設置較大的Row group從而帶來較大的並行度,固然也須要較大的內存空間做爲代價。通常狀況下推薦配置一個Row group大小1G,一個HDFS塊大小1G,一個HDFS文件只含有一個塊。

圖5 Parquet文件格式在磁盤的分佈

拿咱們的這個schema爲例,在任何一個Row group內,會順序存儲四個column chunk。這四個column都是string類型。這個時候Parquet就須要把內存中的AddressBook對象映射到四個string類型的 column中。若是讀取磁盤上的4個column要可以恢復出AddressBook對象。這就用到了咱們前面提到的 「record shredding and assembly algorithm」。

Striping/Assembly算法

對於嵌套數據類型,咱們除了存儲數據的value以外還須要兩個變量Repetition Level(R), Definition Level(D) 才能存儲其完整的信息用於序列化和反序列化嵌套數據類型。Repetition Level和 Definition Level能夠說是爲了支持嵌套類型而設計的,可是它一樣適用於簡單數據類型。在Parquet中咱們只需定義和存儲schema的葉子節點所在列的 Repetition Level和Definition Level。

Definition Level

嵌套數據類型的特色是有些field能夠是空的,也就是沒有定義。若是一個field是定義的,那麼它的全部的父節點都是被定義的。從根節點開始遍 歷,當某一個field的路徑上的節點開始是空的時候咱們記錄下當前的深度做爲這個field的Definition Level。若是一個field的Definition Level等於這個field的最大Definition Level就說明這個field是有數據的。對於required類型的field必須是有定義的,因此這個Definition Level是不須要的。在關係型數據中,optional類型的field被編碼成0表示空和1表示非空(或者反之)。

Repetition Level

記錄該field的值是在哪個深度上重複的。只有repeated類型的field須要Repetition Level,optional 和 required類型的不須要。Repetition Level = 0 表示開始一個新的record。在關係型數據中,repetion level老是0。

下面用AddressBook的例子來講明Striping和assembly的過程。

對於每一個column的最大的Repetion Level和 Definition Level如圖6所示。

圖6 AddressBook的Max Definition Level和Max Repetition Level

下面這樣兩條record:

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這一列爲例,"555 987 6543"這個contacts.phoneNumber的Definition Level是最大Definition Level=2。而若是一個contact沒有phoneNumber,那麼它的Definition Level就是1。若是連contact都沒有,那麼它的Definition Level就是0。

下面咱們拿掉其餘三個column只看contacts.phoneNumber這個column,把上面的兩條record簡化成下面的樣子:

AddressBook {
 contacts: {
   phoneNumber: "555 987 6543"
 }
 contacts: {
 }
}
AddressBook {
}

這兩條記錄的序列化過程如圖7所示:

圖7 一條記錄的序列化過程

若是咱們要把這個column寫到磁盤上,磁盤上會寫入這樣的數據(圖8):

圖8 一條記錄的磁盤存儲

注意:NULL實際上不會被存儲,若是一個column value的Definition Level小於該column最大Definition Level的話,那麼就表示這是一個空值。

下面是從磁盤上讀取數據並反序列化成AddressBook對象的過程:

1,讀取第一個三元組R=0, D=2, Value=」555 987 6543」

R=0 表示是一個新的record,要根據schema建立一個新的nested record直到Definition Level=2。

D=2 說明Definition Level=Max Definition Level,那麼這個Value就是contacts.phoneNumber這一列的值,賦值操做contacts.phoneNumber=」555 987 6543」。

2,讀取第二個三元組 R=1, D=1

R=1 表示不是一個新的record,是上一個record中一個新的contacts。

D=1 表示contacts定義了,可是contacts的下一個級別也就是phoneNumber沒有被定義,因此建立一個空的contacts。

3,讀取第三個三元組 R=0, D=0

R=0 表示一個新的record,根據schema建立一個新的nested record直到Definition Level=0,也就是建立一個AddressBook根節點。

能夠看出在Parquet列式存儲中,對於一個schema的全部葉子節點會被當成column存儲,並且葉子節點必定是primitive類型的 數據。對於這樣一個primitive類型的數據會衍生出三個sub columns (R, D, Value),也就是從邏輯上看除了數據自己之外會存儲大量的Definition Level和Repetition Level。那麼這些Definition Level和Repetition Level是否會帶來額外的存儲開銷呢?實際上這部分額外的存儲開銷是能夠忽略的。由於對於一個schema來講level都是有上限的,並且非 repeated類型的field不須要Repetition Level,required類型的field不須要Definition Level,也能夠縮短這個上限。例如對於Twitter的7層嵌套的schema來講,只須要3個bits就能夠表示這兩個Level了。

對於存儲關係型的record,record中的元素都是非空的(NOT NULL in SQL)。Repetion Level和Definition Level都是0,因此這兩個sub column就徹底不須要存儲了。因此在存儲非嵌套類型的時候,Parquet格式也是同樣高效的。

上面演示了一個column的寫入和重構,那麼在不一樣column之間是怎麼跳轉的呢,這裏用到了有限狀態機的知識,詳細介紹能夠參考Dremel

數據壓縮算法

列式存儲給數據壓縮也提供了更大的發揮空間,除了咱們常見的snappy, gzip等壓縮方法之外,因爲列式存儲同一列的數據類型是一致的,因此可使用更多的壓縮算法。

壓縮算法

使用場景

Run Length Encoding

重複數據

Delta Encoding

有序數據集,例如timestamp,自動生成的ID,以及監控的各類metrics

Dictionary Encoding

小規模的數據集合,例如IP地址

Prefix Encoding

Delta Encoding for strings

性能

Parquet列式存儲帶來的性能上的提升在業內已經獲得了充分的承認,特別是當大家的表很是寬(column很是多)的時候,Parquet不管在資源利用率仍是性能上都優點明顯。具體的性能指標詳見參考文檔。

Spark已經將Parquet設爲默認的文件存儲格式,Cloudera投入了不少工程師到Impala+Parquet相關開發 中,Hive/Pig都原生支持Parquet。Parquet如今爲Twitter至少節省了1/3的存儲空間,同時節省了大量的表掃描和反序列化的時 間。這兩方面直接反應就是節約成本和提升性能。

若是說HDFS是大數據時代文件系統的事實標準的話,Parquet就是大數據時代存儲格式的事實標準。

參考文檔

  1. http://parquet.apache.org/

  2. https://blog.twitter.com/2013/dremel-made-simple-with-parquet

  3. http://blog.cloudera.com/blog/2015/04/using-apache-parquet-at-appnexus/

  4. http://blog.cloudera.com/blog/2014/05/using-impala-at-scale-at-allstate/

做者簡介

梁堰波,現就任於明略數據,開源愛好者,Apache Hadoop & Spark contributor。北京航空航天大學計算機碩士,曾就任於Yahoo!、美團網、法國電信,具有豐富的大數據、數據挖掘和機器學習領域的項目經驗。

相關文章
相關標籤/搜索