Parquet 是面向分析型業務的列式存儲格式,由 Twitter 和 Cloudera 合做開發,2015 年 5 月從 Apache 的孵化器裏畢業成爲 Apache 頂級項目,最新的版本是 1.8.0。算法
列式存儲
列式存儲和行式存儲相比有哪些優點呢?apache
能夠跳過不符合條件的數據,只讀取須要的數據,下降 IO 數據量。緩存
壓縮編碼能夠下降磁盤存儲空間。因爲同一列的數據類型是同樣的,可使用更高效的壓縮編碼(例如 Run Length Encoding 和 Delta Encoding)進一步節約存儲空間。微信
只讀取須要的列,支持向量運算,可以獲取更好的掃描性能。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 的數據模型和算法實現的。ui
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 和數據爲例來講明這個問題。
這個 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:
以 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 簡化成下面的樣子:
這兩條記錄的序列化過程如圖 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 就是大數據時代存儲格式的事實標準。
參考文檔
http://parquet.apache.org/
https://blog.twitter.com/2013/dremel-made-simple-with-parquet
http://blog.cloudera.com/blog/2015/04/using-apache-parquet-at-appnexus/
http://blog.cloudera.com/blog/2014/05/using-impala-at-scale-at-allstate/
本文分享自微信公衆號 - ApacheHudi(ApacheHudi)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。