大數據技術棧速覽之:Parquet

幾種hdfs文件存儲格式的區別

Text:原始存儲,

RCFile:結合列存儲和行存儲的優缺點,Facebook於是提出了基於行列混合存儲的RCFile,它是基於SEQUENCEFILE實現的列存儲格式,它即滿足快速數據加載和動態負載高適應的需求外,也解決了SEQUENCEFILE的一些瓶頸。該存儲結構遵循的是“先水平劃分,再垂直劃分”的設計理念。先將數據按行水平劃分爲行組,這樣一行的數據就可以保證存儲在同一個集羣節點;然後在對行進行垂直劃分。 

ORC:ORC File,它的全名是Optimized Row Columnar (ORC) file,其實就是對RCFile做了一些優化。據官方文檔介紹,這種文件格式可以提供一種高效的方法來存儲Hive數據。它的設計目標是來克服Hive其他格式的缺陷。運用ORC File可以提高Hive的讀、寫以及處理數據的性能。ORC File包含一組組的行數據,稱爲stripes,除此之外,ORC File的file footer還包含一些額外的輔助信息。在ORC File文件的最後,有一個被稱爲postscript的區,它主要是用來存儲壓縮參數及壓縮頁腳的大小。

ORCFile的主要優點爲:

(1)、每個task只輸出單個文件,這樣可以減少NameNode的負載;

(2)、支持各種複雜的數據類型,比如:datetime, decimal, 以及一些複雜類型(struct、list、 map和 union);

(3)、在文件中存儲了一些輕量級的索引數據;

(4)、基於數據類型的塊模式壓縮:

            Integer類型的列用行程長度編碼(run-length encoding)

            String類型的列用字典編碼(dictionary encoding)

(5)、用多個互相獨立的RecordReaders並行讀相同的文件;

(6)、無需掃描markers就可以分割文件;

(7)、綁定讀寫所需要的內存;

(8)、metadata的存儲是用 Protocol Buffers的,所以它支持添加和刪除一些列。

avro:它可以提供:
1 豐富的數據結構類型
2 快速可壓縮的二進制數據形式
3 存儲持久數據的文件容器
4 遠程過程調用RPC
5 簡單的動態語言結合功能,Avro和動態語言結合後,讀寫數據文件和使用RPC協議都不需要生成代碼,而代碼生成作爲一種可選的優化只值得在靜態類型語言中實現。
Avro依賴於模式(Schema)。Avro數據的讀寫操作是很頻繁的,而這些操作都需要使用模式,這樣就減少寫入每個數據資料的開銷,使得序列化快速而又輕巧。這種數據及其模式的自我描述方便於動態腳本語言的使用。

Parquet:Parquet是面向分析型業務的列式存儲格式,由Twitter和Cloudera合作開發。一個Parquet文件是由一個header以及一個或多個block塊組成,以一個footer結尾。header中只包含一個4個字節的數字PAR1用來識別整個Parquet文件格式。文件中所有的metadata都存在於footer中。footer中的metadata包含了格式的版本信息,schema信息、key-value paris以及所有block中的metadata信息。footer中最後兩個字段爲一個以4個字節長度的footer的metadata,以及同header中包含的一樣的PAR1。

之前新統計系統的日誌都是用Avro做序列化和存儲,鑑於Parquet的優勢和對Avro的兼容,將HDFS上的存儲格式改爲Paruqet,並且只需做很小的改動就用原讀取Avro的API讀取Parquet,單列查詢效率可以提高近一個數量級。

 

在互聯網大數據應用場景下,通常數據量很大且字段很多,

但每次查詢數據只針對其中的少數幾個字段,這時候列式存儲是極佳的選擇。列式存儲,天然擅長分析,千萬級別的表,count,sum,group by ,秒出結果!!

列式存儲要解決的問題:

  • 把IO只給查詢需要用到的數據
    • 只加載需要被計算的列
  • 空間節省
    • 列式的壓縮效果更好
    • 可以針對數據類型進行編碼
  • 開啓矢量化的執行引擎(不再1條1條的處理數據,而是一次處理1024條數據)

 

Parquet是列式存儲的一種文件類型,是Hadoop生態系統中任何項目可用的列式存儲格式。spark天然支持parquet,併爲其推薦的存儲格式,hive 也支持parquet格式存儲。 被多種查詢引擎支持(Hive、Impala、Drill等)

Parquet的靈感來自於2010年Google發表的Dremel論文,文中介紹了一種支持嵌套結構的存儲格式,並且使用了列式存儲的方式提升查詢性能,在Dremel論文中還介紹了Google如何使用這種存儲格式實現並行查詢的,如果對此感興趣可以參考論文和開源實現Apache Drill。

嵌套數據模型

在接觸大數據之前,我們簡單的將數據劃分爲結構化數據和非結構化數據,通常我們使用關係數據庫存儲結構化數據,而關係數據庫中使用數據模型都是扁平式的,遇到諸如List、Map和自定義Struct的時候就需要用戶在應用層解析。但是在大數據環境下,通常數據的來源是服務端的埋點數據,很可能需要把程序中的某些對象內容作爲輸出的一部分,而每一個對象都可能是嵌套的,所以如果能夠原生的支持這種數據,這樣在查詢的時候就不需要額外的解析便能獲得想要的結果。

另外,隨着嵌套格式的數據的需求日益增加,目前Hadoop生態圈中主流的查詢引擎都支持更豐富的數據類型,例如Hive、SparkSQL、Impala等都原生的支持諸如struct、map、array這樣的複雜數據類型,這樣也就使得諸如Parquet這種原生支持嵌套數據類型的存儲格式也變得至關重要,性能也會更好。

列式存儲

列式存儲,顧名思義就是按照列進行存儲數據,把某一列的數據連續的存儲,每一行中的不同列的值離散分佈。OLAP場景下的數據大部分情況下都是批量導入,而查詢的時候大多數都是隻使用部分列進行過濾、聚合,對少數列進行計算。列式存儲可以大大提升這類查詢的性能,較之於行是存儲,列式存儲能夠帶來這些優化:

  • 由於每一列中的數據類型相同,所以可以針對不同類型的列使用不同的編碼和壓縮方式,這樣可以大大降低數據存儲空間。

  • 讀取數據的時候可以把映射(Project)下推,只需要讀取需要的列,這樣可以大大減少每次查詢的I/O數據量,更甚至可以支持謂詞下推,跳過不滿足條件的列。

  • 由於每一列的數據類型相同,可以使用更加適合CPU pipeline的編碼方式,減小CPU的緩存失效。

特點:

    ---> 可以跳過不符合條件的數據,只讀取需要的數據,降低 IO 數據量

    ---> 壓縮編碼可以降低磁盤存儲空間(由於同一列的數據類型是一樣的,可以使用更高效的壓縮編碼(如 Run Length Encoding t  Delta Encoding)進一步節約存儲空間)

    ---> 只讀取需要的列,支持向量運算,能夠獲取更好的掃描性能

    ---> Parquet 格式是 Spark SQL 的默認數據源,可通過 spark.sql.sources.default 配置

Parquet的設計目標:

  • 適配通用性
  • 存儲空間優化
  • 計算時間優化

適配通用性

Parquet只是一種存儲格式,它與上層平臺、語言無關,不需要與任何一種數據處理框架綁定,基本上通常使用的查詢引擎和計算框架都已適配,並且可以很方便的將其它序列化工具生成的數據轉換成Parquet格式。目前已經適配的組件包括:

  • 查詢引擎:Hive\Impala\Pig\Presto\Drill\Tajo\HAWQ\IBM Big SQL
  • 計算引擎:MapReduce\Spark\Cascading\Crunch\Scalding\Kite
  • 數據模型:Avro\Thrift\Protocol Buffers

存儲空間優化

Parquet的數據模型

每條記錄中的字段可以包含三種類型:required, repeated, optional。最終由所有葉子節點來代表整個schema。

  • 元組的Schema可以轉換成樹狀結構,根節點可以理解爲repeated類型
  • 所有葉子結點都是基本類型
  • 沒有Map、Array這樣的複雜數據結構,但是可以通過repeated和group組合來實現這樣的需求

Striping/Assembly算法

Parquet的一條記錄的數據如何分成多少列,又如何組裝回來?是由Striping/Assembly算法決定的。

在該算法中,列的每一個值都包含三個部分:

  • value : 字段值
  • repetition level : 重複級別
  • definition level : 定義級別

Repetition Levels

repetition level的設計目標是爲了支持repeated類型的節點:

  • 在寫入時該值等於它和前面的值從哪一層節點開始是不共享的。
  • 在讀取的時候根據該值可以推導出哪一層上需要創建一個新的節點。

例子:對於這樣的schema和兩條記錄:

message nested {
 repeated group leve1 {
  repeated string leve2;
 }
}
r1:[[a,b,c,] , [d,e,f,g]]
r2:[[h] , [i,j]]

計算一下各個值的repetition level。
repetition level計算過程:

  • value=a是一條記錄的開始,和前面的值在根結點上是不共享的,因此repetition level=0
  • value=b和前面的值共享了level1這個節點,但是在level2這個節點上不共享,因此repetition level=2
  • 同理,value=c的repetition value=2
  • value=d和前面的值共享了根節點,在level1這個節點是不共享的,因此repetition level=1
  • 同理,value=e,f,g都和自己前面的佔共享了level1,沒有共享level2,因此repetition level=2
  • value=h屬於另一條記錄,和前面不共享任何節點,因此,repetition level=0
  • value=i跟前面的結點共享了根,但是沒有共享level1節點,因此repetition level=1
  • value-j跟前面的節點共享了level1,但是沒有共享level2,因此repetition level=2

在讀取時,會順序讀取每個值,然後根據它的repetition level創建對象

  • 當讀取value=a時,repeatition level=0,表示需要創建一個新的根節點,
  • 當讀取value=b時,repeatition level=2,表示需要創建level2節點
  • 當讀取value=c時,repeatition level=2,表示需要創建level2節點
  • 當讀取value=d時,repeatition level=1,表示需要創建level1節點
  • 剩下的節點依此類推

幾點規律:

  • repetition level=0表示一條記錄的開始
  • repetition level的值只是針對路徑上repeated類型的節點,因此在計算時可以忽略非repeated類型的節點
  • 在寫入的時候將其理解爲該節點和路徑上的哪一個repeated節點是不共享的
  • 讀取的時候將其理解爲需要在哪一層創建一個新的repeated節點

Definition Levels

有了repetition levle就可以構造出一條記錄了,那麼爲什麼還需要definition level呢?

是因爲repeated和optional類型的存在,可以一條記錄中的某些列是沒有值的,如果不記錄這樣的值,就會導致本該屬於下一條記錄的值被當做當前記錄中的一部分,從而導致數據錯誤,因此,對於這種情況,需要一個佔位符來表示。

definition level的值僅對空值是有效的,表示該值的路徑上第幾層開始是未定義的;對於非空值它是沒有意義的,因爲非空值在葉子節點上是有定義的,所有的父節點也一定是有定義的,因此它的值總是等於該列最大的definition level。
例子:對於這樣的schema:

message ExampleDefinitionLevel {
 optional group a {
  optional group b {
   optional string c;
  }
 }
}

它包含一個列a.b.c,這個列的的每一個節點都是optional類型的,當c被定義時a和b肯定都是已定義的,當c未定義時我們就需要標示出在從哪一層開始時未定義的
一條記錄的definition level的幾種可能的情況如下表:

由於definition level只需要考慮未定義的值,對於required類型的節點,只要父親節點定義了,該節點就必須定義,因此計算時可以忽略路徑上的required類型的節點,這樣可以減少definition level的最大值,優化存儲。

一個完整的例子

下面使用Dremel論文中給的Document示例和給定的兩個值展示計算repeated level和definition level的過程,這裏把未定義的值記錄爲NULL,使用R表示repeated level,D表示definition level。

 

  • 首先看DocId這一列,r1和r2都只有一值分別是:
    • id1=10,由於它是記錄開始,並且是已定義的,因此R=0,D=0
    • id2=20,由於是新記錄的開始,並且是已經定義的,因此R=0,D=0
  • 對於Name.Url這一列,r1中它有三個值,r2中有一個值分別是:

    • url1=’http://A’ ,它是r1中該列的第一個值,並且是定義的,所以R=0,D=2
    • url2=’http://B’ ,它跟上一個值在Name這層是不同的,並且是定義的,所以R=1,D=2
    • url3=NULL,它跟上一個值在Name這層是不同的,並且是未定義的,所以R=1,D=1
    • url4=’http://C’ ,它跟上一個值屬於不同記錄,並且是定義的,所以R=0,D=2
  • 對於Links.Forward這一列,在r1中有三個值,在r2中有1個值,分別是:

    • value1=20,它是r1中該列的第一個值,並且是定義的,所以R=0,D=2
    • value2=40,它跟上一個值在Links這層是相同的,並且是定義的,所以R=1,D=2
    • value3=60,它跟上一個值在Links這層是相同的,並且是定義的,所以R=1,D=2
    • value4=80,它是一條新的記錄,並且是定義的,所以R=0,D=2
  • 對於Links.Backward這一列,在r1中有一個空值,在r2中兩個值,分別是:
    • value1=NULL,它是一條新記錄,並且是未定義的,父節點Links是定義的,所以R=0,D=1
    • value2=10,是一條新記錄,並且是定義的,所以R=0,D=2
    • value3=30,跟上個值共享父節點,並且是定義的,所以R=1,D=2

Parquet文件格式

Parquet文件是二進制方式存儲的,所以是不可以直接讀取的,文件中包含數據和元數據,因此Parquet格式文件是自解析的。

先了解一下關於Parquet文件的幾個基本概念:

  • 行組(Row Group):每一個行組包含一定的行數,一般對應一個HDFS文件塊,Parquet讀寫的時候會將整個行組緩存在內存中。
  • 列塊(Column Chunk):在一個行組中每一列保存在一個列塊中,一個列塊中的值都是相同類型的,不同的列塊可能使用不同的算法進行壓縮。
  • 頁(Page):每一個列塊劃分爲多個頁,一個頁是最小的編碼的單位,在同一個列塊的不同頁可能使用不同的編碼方式。

Parquet文件組成:

  • 文件開始和結束的4個字節都是Magic Code,用於校驗它是否是一個Parquet文件
  • 結束MagicCode前的Footer length是文件元數據的大小,通過該值和文件長度可以計算出元數據Footer的偏移量
  • 再往前推是Footer文件的元數據,裏面包含:

    • 文件級別的信息:版本,Schema,Extra key/value對等
    • 每個行組的元信息,每個行組是由多個列塊組成的:
      • 每個列塊的元信息:類型,路徑,編碼方式,第1個數據頁的位置,第1個索引頁的位置,壓縮的、未壓縮的尺寸,額外的KV
  • 文件中大部分內容是各個行組信息:

    • 一個行組由多個列塊組成
      • 一個列塊由多個頁組成,在Parquet中有三種頁:
        • 數據頁
          • 一個頁由頁頭、repetition levels\definition levles\valus組成
        • 字典頁
          • 存儲該列值的編碼字典,每一個列塊中最多包含一個字典頁
        • 索引頁
          • 用來存儲當前行組下該列的索引,目前Parquet中還不支持索引頁,但是在後面的版本中增加

 

計算時間優化

Parquet的最大價值在於,它提供了一種把IO奉獻給查詢需要用到的數據。主要的優化有兩種:

  • 映射下推(Project PushDown)
  • 謂詞下推(Predicate PushDown)

映射下推

列式存儲的最大優勢是映射下推,它意味着在獲取表中原始數據時只需要掃描查詢中需要的列。

Parquet中原生就支持映射下推,執行查詢的時候可以通過Configuration傳遞需要讀取的列的信息,在掃描一個行組時,只掃描對應的列。

除此之外,Parquet在讀取數據時,會考慮列的存儲是否是連接的,對於連續的列,一次讀操作就可以把多個列的數據讀到內存。

謂詞下推

在RDB中謂詞下推是一項非常通用的技術,通過將一些過濾條件儘可能的在最底層執行可以減少每一層交互的數據量,從而提升性能。

例如,

select count(1)
from A Join B
on A.id = B.id
where A.a > 10 and B.b < 100

SQL查詢中,如果把過濾條件A.a > 10和B.b < 100分別移到TableScan的時候執行,可以大大降低Join操作的輸入數據。

無論是行式存儲還是列式存儲,都可以做到上面提到的將一些過濾條件儘可能的在最底層執行。

但是Parquet做了更進一步的優化,它對於每個行組中的列都在存儲時進行了統計信息的記錄,包括最小值,最大值,空值個數。通過這些統計值和該列的過濾條件可以直接判斷此行組是否需要掃描。

另外,未來還會增加Bloom Filter和Index等優化數據,更加有效的完成謂詞下推。

在使用Parquet的時候可以通過如下兩種策略提升查詢性能:

  1. 類似於關係數據庫的主鍵,對需要頻繁過濾的列設置爲有序的,這樣在導入數據的時候會根據該列的順序存儲數據,這樣可以最大化的利用最大值、最小值實現謂詞下推。
  2. 減小行組大小和頁大小,這樣增加跳過整個行組的可能性,但是此時需要權衡由於壓縮和編碼效率下降帶來的I/O負載。

詳細介紹參考:https://www.cnblogs.com/ITtangtang/p/7681019.html

常用parquet文件讀寫的幾種方式
1.用spark的hadoopFile api讀取hive中的parquet格式
2.用sparkSql讀寫hive中的parquet
3.用新舊MapReduce讀寫parquet格式文件

 

分區過濾與列修剪

 分區過濾

parquet結合spark,可以完美的實現支持分區過濾。如,需要某個產品某段時間的數據,則hdfs只取這個文件夾。

spark sql、rdd 等的filter、where關鍵字均能達到分區過濾的效果。

使用spark的partitionBy 可以實現分區,若傳入多個參數,則創建多級分區。第一個字段作爲一級分區,第二個字段作爲2級分區。 

 列修剪

列修剪:其實說簡單點就是我們要取回的那些列的數據。

當取得列越少,速度越快。當取所有列的數據時,比如我們的120列數據,這時效率將極低。同時,也就失去了使用parquet的意義。

結論

  • parquet的gzip的壓縮比率最高,若不考慮備份可以達到27倍。可能這也是spar parquet默認採用gzip壓縮的原因吧。
  • 分區過濾和列修剪可以幫助我們大幅節省磁盤IO。以減輕對服務器的壓力。
  • 如果你的數據字段非常多,但實際應用中,每個業務僅讀取其中少量字段,parquet將是一個非常好的選擇。

 

更多參考:

深入分析Parquet列式存儲格式

http://www.infoq.com/cn/articles/in-depth-analysis-of-parquet-column-storage-format

第二篇文章裏面的示例比較豐富,交叉比較來學習效果比較好.但是怎麼持久化就沒有說明.

Dremel made simple with Parquet

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

第三篇文章裏面可以瞭解到如何在形成Parquet的樹狀結構後,以列式的方式持久化到磁盤.

Apache Drill學習筆記二:Dremel原理(上)

http://www.tuicool.com/articles/u6bMnuZ

Presentations

https://parquet.apache.org/presentations/

Spark 編程指南簡體中文版

https://www.kancloud.cn/kancloud/spark-programming-guide/51541

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章