一文講透大資料列存標準格式:Parquet

語言: CN / TW / HK

導讀:

今天介紹一種大資料時代有名的列式儲存檔案格式:Parquet,被廣泛用於 Spark、Hadoop 資料儲存。Parquet 中文直譯是鑲木地板,意思是結構緊湊,空間佔用率高。

1

概念

大規模分析型資料處理在網際網路乃至其他行業中應用 都已 越來越廣泛,尤其是當前已經可以用廉價的儲存來收集、儲存海量的業務資料情況下。如何讓分析師和工程師便捷的利用這些資料也變得越來越重要。列式儲存(Column-oriented Storage)是大資料場景面向分析型資料的主流儲存方式。與行式儲存相比,列存由於可以只提取部分資料列、同列同質資料擁有更好的編碼及壓縮方式,因此在 OLAP 場景下能提供更好的 IO 效能。

Apache Parquet 是由 Twitter 和 Cloudera 最先發起併合作開發的列存專案,也是 2010 年 Google 發表的 Dremel 論文中描述的內部列存格式的開源實現。和一些傳統的列式儲存(C-Store、MonetDB 等)系統相比,Dremel/Parquet 最大的貢獻是支援巢狀格式資料(Nested Data)的列式儲存。巢狀格式可以很自然的描述網際網路和科學計算等領域的資料,Dremel/Parquet “原生”的支援巢狀格式資料減少了規則化、重新組合這些大規模資料的代價。

Parquet 的設計與計算框架、資料模型以及程式語言無關,可以與任意專案整合,因此應用廣泛。目前已經是 Hadoop 大資料生態圈列式儲存的事實標準。

2

原理

2.1 行存 VS 列存

例如,下圖是擁有 A/B/C 3 個欄位的簡單示意表:

在面向行的儲存中,每列的資料依次排成一行,如下所示:

而在面向列的儲存中,相同列的資料儲存在一起:

顯而易見,行存適用於資料整行讀取場景,而列存適用於只讀取部分列資料(如統計分析等)場景。

2.2 資料模型

(1)schema 協議

想要深入的瞭解 Parquet 儲存格式首先需要理解它的資料模型。Parquet 採用了一個類似 Google Protobuf 的協議來描述儲存資料的 schema。下面是 Parquet 資料 schema 的一個簡單示例:

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

schema 的最上層是 message,裡面可以包含一系列欄位。每個欄位都擁有3個屬性:重複性(repetition)、型別(type)以及名稱(name)。欄位型別可以是一個 group 或者原子型別(如 int/boolean/string 等),group 可以用來表示資料的巢狀結構。欄位的重複性有三種情況:

  • required:有且只有一次

  • optional:0或1次

  • repeated:0或多次

這個模型非常的簡潔。一些複雜的資料型別如:Map/List/Set 也可以用重複的欄位(repeated fields) + groups 來表達,因此也就不用再單獨定義這些型別。

採用 repeated field 表達 List 或者 Set 的示例:

採用 repeated group(包含 key 和 value,其中 key 是 required) 來表達 Map 的示例:

(2)列式儲存格式

試想一下,為了使資料能夠按列儲存,對於一條記錄(Record),首先要將其按列(Column)進行拆分。對於扁平(Flat)結構資料,拆分比較直觀,一個欄位即對應一列,而巢狀格式資料會複雜些。Dremel/Parquet 中,提出以樹狀層級的形式組織 schema 中的欄位(Field),樹的葉子結點對應一個原子型別欄位,這樣這個模型能同時覆蓋扁平結構和巢狀結構資料(扁平結構只是巢狀結構的一種特例)。巢狀欄位的完整路徑使用簡單的點分符號表示,如 contacts. name。

AddressBook 例子以樹狀結構展示的樣式:

列存連續的儲存一個欄位的值,以便進行高效的編碼壓縮及快速的讀取。Dremel 中行存 vs 列存的圖示:

(3)Repetition and Definition Levels

對於巢狀格式列存,除了按列拆分進行連續的儲存,還需要能夠“無損”的保留巢狀格式的結構化資訊,以便正確的重建記錄。

只有欄位值不能表達清楚記錄的結構。給定一個重複欄位的兩個值,我們不知道此值是在什麼“級別”被重複的(比如,這些值是來自兩個不同的記錄,還是相同的記錄中兩個重複的值)。同樣的,給出一個缺失的可選欄位,我們不知道整個路徑有多少欄位被顯示定義了。

Dremel 提出了 Repetition Level(重複級別)和 Definition Level(定義級別)兩個概念,用以解決這個問題。並實現了記錄中任意一個欄位的恢復都不需要依賴其它欄位,且可以對任意欄位子集按原始巢狀格式進行重建。

Repetition levels:用以表示在該欄位路徑上哪個節點進行了重複 (at what repeated field in the field’s path the value has repeated)。

一個重複欄位儲存的列值,有可能來自不同記錄,也可能由同一記錄的不同層級節點重複導致。如上圖中的 Code 欄位,他在 r1 記錄中出現了 3 次,分別是欄位 Name 和 Language 重複導致的,其中 Language 先重複了 2 次,Name 欄位再重複了 1 次。

Repetition Levels 採用數字代表重複節點的層級。根據樹形層次結構,根結點為 0、下一層級為 1… 依次類推。根結點的重複暗含了記錄的重複,也即 r=0 代表新記錄的開始。required 和 optional 欄位不需要 repetition level,只有可重複的欄位需要。因此,上述 Code 欄位的 repetition levels 範圍為 0-2。當我們從上往下掃描 r1 記錄,首先遇到 Code 的值是“en-us”,由於它之前沒有該欄位路徑相關的欄位出現,因此 r=0;其次遇到“en”,是 Language 層級重複導致的,r=2;最後遇到“en-gb”,是 Name 層級重複導致的,因此 r=1。所以,Code 欄位的 repetition levels 在 r1 記錄中是“0,2,1”。

需要注意的是,r1 記錄中的第二個重複 Name,由於其不包含 Code 欄位,為了區分“en-gb”值是來自記錄中的第三個 Name 而不是第二個,我們需要在“en”和“en-gb”之間插入一個值“null”。由於它是 Name 級重複的,因此它的 r=1。另外還需要注意一些隱含資訊,比如 Code 是 required 欄位型別,因此一旦 Code 出現未定義,則隱含表明其上級 Language 也肯定未定義。

Definition Levels:用以表示該欄位路徑上有多少可選的欄位實際進行了定義(how many fields in p that could be undefined (because they are optional or repeated) are actually present)。

光有 Repetition Levels 尚無法完全保留巢狀結構資訊,考慮上述圖中 r1 記錄的 Backward 欄位。由於 r1 中未定義 Backward 欄位,因此我們插入一個“null”並設定 r=0。但 Backward 的上級 Links 欄位在 r1 中顯式的進行了定義,null 和 r=0 無法再表達出這一層資訊。因此需要額外再新增 Definition Levels 定義記錄可選欄位出現的個數,Backward 的路徑上出現 1 個可選欄位 Links,因此它的 d=1。

有了 Definition Levels 我們就可以清楚的知道該值出現在欄位路徑的第幾層,對未定義欄位的 null 和欄位實際的值為 null 也能進行區分。只有 optional 和 repeated 欄位需要 Definition Levels 定義,因為 required 欄位已經隱含了欄位肯定被定義(這可以減少 Definition Levels 需要描述的數字,並在一定程度上節省後續的儲存空間)。另外一些其他的隱含資訊:如果 Definition Levels 小於路徑中 optional + repeated 欄位的數量,則該欄位的值肯定為 null;Definition Levels 的值為 0 隱含了 Repeated Levels 也為 0(路徑中沒有 optional/repeated 欄位或整個路徑未定義)。

(4)striping and assembly 演算法

現在把 Repetition Levels 和 Definition Levels 兩個概念一起考慮。還是沿用上述 AddressBook 例子。下表顯示了 AddressBook 中每個欄位的最大重複和定義級別,並解釋了為什麼它們小於列的深度:

假設這是兩條真實的 AddressBook 資料:

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 欄位來演示一下拆解和重組記錄的 striping and assembly 演算法。

僅針對 contacts.phoneNumber 欄位投影后,資料具有如下結構:

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

計算可得該該欄位對應的資料如下(R =重複級別,D =定義級別):

因此我們最終儲存的記錄資料如下:

contacts.phoneNumber: “555 987 6543”
new record: R = 0
value is defined: D = maximum (2)
contacts.phoneNumber: null
repeated contacts: R = 1
only defined up to contacts: D = 1
contacts: null
new record: R = 0
only defined up to AddressBook: D = 0

使用圖表展示(注意其中的 null 值並不會實際儲存,原因如上所說只要 Definition Levels 小於其 max 值即隱含該欄位值為 null):

在重組該記錄時,我們重複讀取該欄位的值:

R=0, D=2, Value = “555 987 6543”:
R = 0 means a new record. We recreate the nested records from the root until the definition level (here 2)
D = 2 which is the maximum. The value is defined and is inserted.
R=1, D=1:
R = 1 means a new entry in the contacts list at level 1.
D = 1 means contacts is defined but not phoneNumber, so we just create an empty contacts.
R=0, D=0:
R = 0 means a new record. we create the nested records from the root until the definition level
D = 0 => contacts is actually null, so we only have an empty AddressBook

3

實現

Parquet 工程具體的實現。

3.1 Parquet 檔案儲存格式中的術語

  • Block (hdfs block):即指 HDFS Block,Parquet 的設計與 HDFS 完全相容。Block 是 HDFS 檔案儲存的基本單位,HDFS 會維護一個 Block 的多個副本。在 Hadoop 1.x 版本中 Block 預設大小 64M,Hadoop 2.x 版本中預設大小為 128M。

  • File:HDFS 檔案,儲存了該檔案的元資料資訊,但可以不包含實際資料(由 Block 儲存)。

  • Row group:按照行將資料劃分為多個邏輯水平分割槽。一個 Row group(行組)由每個列的一個列塊(Column Chunk)組成。

  • Column chunk:一個列的列塊,分佈在行組當中,並在檔案中保證是連續的。

  • Page:一個列塊切分成多個 Pages(頁面),概念上講,頁面是 Parquet 中最小的基礎單元(就壓縮和編碼方面而言)。一個列塊中可以有多個型別的頁面。

3.2 並行化執行的基本單元

  • MapReduce - File/Row Group(一個任務對應一個檔案或一個行組)

  • IO - Column chunk(任務中的 IO 以列塊為單位進行讀取)

  • Encoding/Compression - Page(編碼格式和壓縮一次以一個頁面為單位進行)

3.3 Parquet 檔案格式

Parquet 檔案格式是自解析的,採用 thrift 格式定義的檔案 schema 以及其他元資料資訊一起儲存在檔案的末尾。

檔案儲存格式示例:

4-byte magic number "PAR1"
<Column 1 Chunk 1 + Column Metadata>
<Column 2 Chunk 1 + Column Metadata>
...
<Column N Chunk 1 + Column Metadata>
<Column 1 Chunk 2 + Column Metadata>
<Column 2 Chunk 2 + Column Metadata>
...
<Column N Chunk 2 + Column Metadata>
...
<Column 1 Chunk M + Column Metadata>
<Column 2 Chunk M + Column Metadata>
...
<Column N Chunk M + Column Metadata>
File Metadata
4-byte length in bytes of file metadata
4-byte magic number "PAR1"

整個檔案(表)有 N 個列,劃分成了 M 個行組,每個行組都有所有列的一個 Chunk 和其元資料資訊。檔案的元資料資訊儲存在資料之後,包含了所有列塊元資料資訊的起始位置。讀取的時候首先從檔案末尾讀取檔案元資料資訊,再在其中找到感興趣的 Column Chunk 資訊,並依次讀取。檔案元資料資訊放在檔案最後是為了方便資料依序一次性寫入。

具體的儲存格式展示圖:

3.4 元資料資訊

Parquet 總共有 3 種類型的元資料:檔案元資料、列(塊)元資料和 page header 元資料。所有元資料都採用 thrift 協議儲存。具體資訊如下所示:

3.5 Parquet 資料型別

在實現層級上,Parquet 只保留了最精簡的部分資料型別,以方便儲存和讀寫。在其上有邏輯型別(Logical Types)以供擴充套件,比如:邏輯型別 strings 就對映為帶有 UTF8 標識的二進位制 byte arrays 進行儲存。

Types:

BOOLEAN: 1 bit boolean
INT32: 32 bit signed ints
INT64: 64 bit signed ints
INT96: 96 bit signed ints
FLOAT: IEEE 32-bit floating point values
DOUBLE: IEEE 64-bit floating point values
BYTE_ARRAY: arbitrarily long byte arrays.

邏輯型別的更多說明請參考:

http://github.com/apache/parquet-format/blob/master/LogicalTypes.md

3.6 Encoding

資料編碼的實現大部分和原理部分所闡述的一致,這裡不再重複說明,更多細節可參考: http://github.com/apache/parquet-format/blob/master/Encodings.md

3.7 Column chunks 儲存

Column chunks 由一個個 Pages 組成,Reader 在讀取的時候可以根據 page header 資訊跳過不感興趣的頁面。page header 中還儲存著頁面資料編碼和壓縮的資訊。

3.8 錯誤情況處理

如果檔案元資料損壞,則整個檔案將丟失。如果列元資料損壞,則該列塊將丟失(但其他行組中該列的列塊還可以使用)。如果 page header 損壞,則該列塊中的剩餘頁面都將丟失。如果頁面中的資料損壞,則該頁面將丟失。較小的檔案行組配置,可以更有效地抵抗損壞。

3.9 推薦配置

行組大小(Row group size): 更大的行組允許更大的列塊,這使得可以執行更大的順序 IO。不過更大的行組需要更大的寫快取。Parquet 建議使用較大的行組(512MB-1GB)。此外由於可能需要讀取整個行組,因此最好一個行組能完全適配一個 HDFS Block。因此,HDFS 塊大小也需要相應的設定更大。一個較優的讀取配置為:行組大小 1GB,HDFS 塊大小 1GB,每個 HDFS 檔案對應 1 個 HDFS 塊。

資料頁大小(Data page size): 資料頁應視為不可分割的,因此較小的資料頁可實現更細粒度的讀取(例如單行查詢)。但較大的頁面可以減少空間的開銷(減少 page header 數量)和潛在的較少的解析開銷(處理 headers)。Parquet 建議的頁面大小為 8KB。

參考資料:

[1]. Dremel: Interactive Analysis of WebScale Datasets

[2]. Dremel made simple with Parquet

[3]. 經典論文翻譯導讀之《Dremel: Interactive Analysis of WebScale Datasets》

[4]. 處理海量資料:列式儲存綜述(儲存篇)

[5]. http://parquet.apache.org/documentation/latest/

作者簡介

大鯨,網易數帆有數實時數倉開發工程師,曾從事搜尋系統、實時計算平臺等相關工作。

獲取最新動態

最新的推文無法在第一時間看到?

以前的推文還需要複雜漫長的翻閱?

進入“網易有數”公眾號介紹頁,點選右上角

“設為星標”

置頂公眾號,從此訊息不迷路

設為星標,最新推文不迷路

分享,點贊,在看,安排一下?