欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入Parquet文件格式設計原理及實現(xiàn)細節(jié)

 更新時間:2023年08月30日 09:17:59   作者:Ye?Ding  
這篇文章主要介紹了深入Parquet文件格式設計原理及實現(xiàn)細節(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

思考半天決定講一個大家既熟悉又陌生的話題:Parquet文件。相信每個做大數(shù)據(jù)的工程師肯定都接觸過Parquet文件,都知道它是一種列式存儲格式,在面對OLAP查詢時可以減少讀取的數(shù)據(jù)量,提高查詢性能。但是對于它的格式具體是如何設計的,以及更重要的:為什么這樣設計,可能就沒有那么清楚了。

這篇文章會帶你深入Parquet文件的原理和實現(xiàn)細節(jié),并試圖說明這些設計背后的意義。

Parquet解決什么問題

要理解一個系統(tǒng),首先第一個要提出的問題就是

這個系統(tǒng)為了解決什么問題?

也就是“這個系統(tǒng)提供了什么功能”。這是理解任何一個系統(tǒng)都需要關注的主線。只要心中有這條主線,就不會陷入各種細節(jié)的泥沼,而迷失了方向。

對于Parquet文件來說,這條主線在Twitter宣布Parquet開源的文章中就講得非常清楚

Parquet is an open-source columnar storage format for Hadoop.
...
Not all data people store in Hadoop is a simple table — complex nested structures abound. For example, one of Twitter’s common internal datasets has a schema nested seven levels deep, with over 80 leaf nodes.

也就是說,Twitter想在Hadoop上面設計一種新的列式存儲格式,這種格式可以保存包含嵌套結構的數(shù)據(jù)。所以Parquet文件格式試圖解決的問題,用一句話來說,就是列式存儲一個類型包含嵌套結構的數(shù)據(jù)集。

什么是“包含嵌套結構的數(shù)據(jù)集”呢?舉個例子

假設我們要存儲1000個用戶的電話簿信息,其中每個用戶的電話簿用下面的這個結構來表示

{
  "owner": "Lei Li",
  "ownerPhoneNumbers": ["13354127165", "18819972777"],
  "contacts": [
    {
      "name": "Meimei Han",
      "phoneNumber": "18561628306"
    },
    {
      "name": "Lucy",
      "phoneNumber": "14550091758"
    }
  ]
}

可以看到其中 ownerPhoneNumbers 字段是一個數(shù)組,而contacts字段更是一個對象的數(shù)組。所以這個類型就不能用簡單的二維表來存儲,因為它包含了嵌套結構。

如果把這個包含嵌套結構的類型稱為AddressBook,那么Parquet文件的目標就是以面向列的方式保存AddressBook對象所構成的數(shù)據(jù)集。

接下來再來講如何“以面向列的方式保存”。對于一個二維表的數(shù)據(jù),相信大家可以很容易地想象出怎樣列式地存儲這些數(shù)據(jù),例如

nameagephoneNumber
Lei Li1613354127165
Meimei Han1418561628306
Lucy1514550091758

把它列式存儲就變成了

"Lei Li"
"Meimei Han"
"Lucy"
16
14
15
"13354127165"
"18561628306"
"14550091758"

但如果數(shù)據(jù)是一個包含數(shù)組和對象的復雜嵌套結構呢?可能就不是這么直觀了。

在Parquet里面,保存嵌套結構的方式是把所有字段打平以后順序存儲。

什么意思呢?以電話簿的例子來說,真正有數(shù)據(jù)的其實只有4列:

  • owner
  • ownerPhoneNumbers
  • contacts.name
  • contacts.phoneNumber

所以只需要把原始數(shù)據(jù)看做是一個4列的表即可。舉個例子:

假設有2條AddressBook記錄

{
  "owner": "Lei Li",
  "ownerPhoneNumbers": ["13354127165", "18819972777"],
  "contacts": [
    {
      "name": "Meimei Han",
      "phoneNumber": "18561628306"
    },
    {
      "name": "Lucy",
      "phoneNumber": "14550091758"
    }
  ]
},
{
  "owner": "Meimei Han",
  "ownerPhoneNumbers": ["15130245254"],
  "contacts": [
    {
      "name": "Lily"
    },
    {
      "name": "Lucy",
      "phoneNumber": "14550091758"
    }
  ]
}

以列式保存之后,就會變成這樣

"Lei Li"
"Meimei Han"
"13354127165"
"18819972777"
"15130245254"
"Meimei Han"
"Lucy"
"Lily"
"Lucy"
"18561628306"
"14550091758"
"14550091758"

聰明的朋友肯定很快就發(fā)現(xiàn)了,因為原始結構里有個數(shù)組,長度是不定的,如果只是把數(shù)據(jù)按順序存放,那就無法區(qū)分record之間的邊界,也就不知道每個值究竟屬于哪條record了。所以簡單地打平是不可行的。

為了解決這個問題,Parquet的設計者引入了兩個新的概念:repetition level和definition level。這兩個值會保存額外的信息,可以用來重構出數(shù)據(jù)原本的結構。

關于repetition level和definition level具體是如何工作的,我會放到最后來講。這里只需要記住,Parquet文件對每個value,都同時保存了它們的repetition level和definition level,以便確定這個value屬于哪條record。

Parquet具體是怎么存放數(shù)據(jù)

接下來我們會深入Parquet文件的內部,講講Parquet具體是怎么存放數(shù)據(jù)的。

首先放一張Parquet文件的整體結構圖

其實Parquet還有一張更常見的結構圖,官方也經常引用,但我覺得層次不清晰,反而更讓人費解,所以就自己畫了上面這張圖。

看過Parquet的整體結構圖之后,可能你已經被這些概念搞迷糊了:Header,Row Group,Column Chunk,Page,F(xiàn)ooter……沒關系,還是回到我們的主線——列式存儲一個包含嵌套結構的數(shù)據(jù)集,我會把解決這個問題的思路自上而下地拆解,自然而然地就能產生這些概念。

Row Group

首先,因為我們要存儲的對象是一個數(shù)據(jù)集,而這個數(shù)據(jù)集往往包含上億條record,所以我們會進行一次水平切分,把這些record切成多個“分片”,每個分片被稱為Row Group。為什么要進行水平切分?雖然Parquet的官方文檔沒有解釋,但我認為主要和HDFS有關。因為HDFS存儲數(shù)據(jù)的單位是Block,默認為128m。如果不對數(shù)據(jù)進行水平切分,只要數(shù)據(jù)量足夠大(超過128m),一條record的數(shù)據(jù)就會跨越多個Block,會增加很多IO開銷。Parquet的官方文檔也建議,把HDFS的block size設置為1g,同時把Parquet的parquet.block.size也設置為1g,目的就是使一個Row Group正好存放在一個HDFS Block里面。

Column Chunk

在水平切分之后,就輪到列式存儲標志性的垂直切分了。切分方式和上文提到的一致,會把一個嵌套結構打平以后拆分成多列,其中每一列的數(shù)據(jù)所構成的分片就被稱為Column Chunk。最后再把這些Column Chunk順序地保存。

Page

把數(shù)據(jù)拆解到Column Chunk級別之后,其結構已經相當簡單了。對Column Chunk,Parquet會進行最后一次水平切分,分解成為一個個的Page。每個Page的默認大小為1m。這次的水平切分又是為了什么?盡管Parquet的官方文檔又一次地沒有解釋,我認為主要是為了讓數(shù)據(jù)讀取的粒度足夠小,便于單條數(shù)據(jù)或小批量數(shù)據(jù)的查詢。因為Page是Parquet文件最小的讀取單位,同時也是壓縮的單位,如果沒有Page這一級別,壓縮就只能對整個Column Chunk進行壓縮,而Column Chunk如果整個被壓縮,就無法從中間讀取數(shù)據(jù),只能把Column Chunk整個讀出來之后解壓,才能讀到其中的數(shù)據(jù)。

Header, Index和Footer

最后聊聊Data以外的Metadata部分,主要是:Header,Index和Footer。

Header

Header的內容很少,只有4個字節(jié),本質是一個magic number,用來指示文件類型。這個magic number目前有兩種變體,分別是“PAR1”和“PARE”。其中“PAR1”代表的是普通的Parquet文件,“PARE”代表的是加密過的Parquet文件。

Index

Index是Parquet文件的索引塊,主要為了支持“謂詞下推”(Predicate Pushdown)功能。謂詞下推是一種優(yōu)化查詢性能的技術,簡單地來說就是把查詢條件發(fā)給存儲層,讓存儲層可以做初步的過濾,把肯定不滿足查詢條件的數(shù)據(jù)排除掉,從而減少數(shù)據(jù)的讀取和傳輸量。舉個例子,對于csv文件,因為不支持謂詞下推,Spark只能把整個文件的數(shù)據(jù)全部讀出來以后,再用where條件對數(shù)據(jù)進行過濾。而如果是Parquet文件,因為自帶Max-Min索引,Spark就可以根據(jù)每個Page的max和min值,選擇是否要跳過這個Page,不用讀取這部分數(shù)據(jù),也就減少了IO的開銷。

目前Parquet的索引有兩種,一種是Max-Min統(tǒng)計信息,一種是BloomFilter。其中Max-Min索引是對每個Page都記錄它所含數(shù)據(jù)的最大值和最小值,這樣某個Page是否不滿足查詢條件就可以通過這個Page的max和min值來判斷。BloomFilter索引則是對Max-Min索引的補充,針對value比較稀疏,max-min范圍比較大的列,用Max-Min索引的效果就不太好,BloomFilter可以克服這一點,同時也可以用于單條數(shù)據(jù)的查詢。

Footer

Footer是Parquet元數(shù)據(jù)的大本營,包含了諸如schema,Block的offset和size,Column Chunk的offset和size等所有重要的元數(shù)據(jù)。另外Footer還承擔了整個文件入口的職責,讀取Parquet文件的第一步就是讀取Footer信息,轉換成元數(shù)據(jù)之后,再根據(jù)這些元數(shù)據(jù)跳轉到對應的block和column,讀取真正所要的數(shù)據(jù)。

關于Footer還有一個問題,就是為什么Parquet要把元數(shù)據(jù)放在文件的末尾而不是開頭?這主要是為了讓文件寫入的操作可以在一趟(one pass)內完成。因為很多元數(shù)據(jù)的信息需要把文件基本寫完以后才知道(例如總行數(shù),各個Block的offset等),如果要寫在文件開頭,就必須seek回文件的初始位置,大部分文件系統(tǒng)并不支持這種寫入操作(例如HDFS)。而如果寫在文件末尾,那么整個寫入過程就不需要任何回退。

Parquet如何把嵌套結構編碼進列式存儲

講完了Parquet的整體結構之后,我們還剩下最后一個問題,也就是我之前埋下的伏筆:Parquet如何把嵌套結構編碼進列式存儲。在上文中我提到了Parquet是通過repetition level和definition level來解決這個問題,接下來就會詳細地講解一下這是怎么實現(xiàn)的。

還是上文用到的例子

{
  "owner": "Lei Li",
  "ownerPhoneNumbers": ["13354127165", "18819972777"],
  "contacts": [
    {
      "name": "Meimei Han",
      "phoneNumber": "18561628306"
    },
    {
      "name": "Lucy",
      "phoneNumber": "14550091758"
    }
  ]
},
{
  "owner": "Meimei Han",
  "ownerPhoneNumbers": ["15130245254"],
  "contacts": [
    {
      "name": "Lily"
    },
    {
      "name": "Lucy",
      "phoneNumber": "14550091758"
    }
  ]
}

注意其中的第三列contacts.name,它有4個值”Meimei Han”,“Lucy”,“Lily”,“Lucy”,其中前兩個屬于前一條record,后兩個屬于后一條record。Parquet是如何表達這個信息的呢?它是用repetition level這個值來表達的。

repetition level主要用來表達數(shù)組類型字段的長度,但它并不直接記錄長度,而是通過記錄嵌套層級的變化來間接地表達長度,即如果嵌套層級不變,那么說明數(shù)組還在延續(xù),如果嵌套層級變了,說明前一個數(shù)組結束了。如果在某個值上嵌套層級由0提高到了1,則這個值的repetition level就是0。如果在某個值的位置嵌套層級不變,則這個值的repetition level就是它的嵌套層級。對于上文中的例子,對應的repetition level就是

ValueRepetition Level
Meimei Han0
Lucy1
Lily0
Lucy1

還不是很明白?換個更典型的例子

[["a", "b"], ["c", "d", "e"]]

它對應的repetition level會被編碼成

ValueRepetition Level
a0
b2
c1
d2
e2

因為這個數(shù)組的嵌套層級是2,而”a”是從level 0到level 2的邊界,所以它的repetition level是0,”c”是從level 1到level 2的邊界,所以它的repetition level是1,其他字母的嵌套層級沒有發(fā)生變化,所以它們的repetition level就是2。

總結一下,repetition level主要用來表達數(shù)組的長度。

講完了repetition level,再來講講definition level。與repetition level類似的,definition level主要用來表達null的位置。因為Parquet文件里不會顯式地存儲null,所以通過definition level來判斷某個值是否是null。例如對于下面這個例子

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

對應的definition level是這樣編碼的

ValueDefinition Level
555 987 65432
NULL1
NULL0

可以看到,凡是definition level小于嵌套層級的,都表達了這個值是null。而definition level具體的值則表達null出現(xiàn)在哪一個嵌套層級。

Parquet最難理解的部分到此就結束了。你或許會有疑問,如果對每個值都保存repetition level和definition level,那么這部分的數(shù)據(jù)量肯定不小(兩個int32整數(shù),共8個字節(jié)),搞不好比本來要存的數(shù)據(jù)還要大,是不是本末倒置了?顯然Parquet也考慮到了這個問題,所以有很多的優(yōu)化措施,例如“對非數(shù)組類型的值不保存repetition level”,“對必填字段不保存definition level”等,真正存儲這兩個level時,也使用的是bit-packing + RLE編碼,盡可能地對這部分數(shù)據(jù)進行了壓縮。篇幅有限,就不在這里展開了。

最后聊聊Parquet格式的演進

Parquet格式最初由Twitter和Cloudera提出,作為RCFile格式的替代者,和早一個月提出的ORC格式類似。聯(lián)想到ORC是由Facebook和Hortonworks提出的,這兩者的競爭關系不言自明。(有機會可以再來寫寫ORC格式)

Parquet在2013年宣布開源后,2014年被Cloudera捐給Apache基金會,進入孵化流程,并于2015年畢業(yè)成為頂級項目。Parquet的框架在進入Apache基金會之前已經基本成型,此后變化得也不快,主要新增了幾個功能:

  • Column Index
  • BloomFilter
  • 模塊化加密

這些改動主要是為了加強對謂詞下推的支持,但也有個副作用:文件體積變得更大了。

以上就是深入Parquet文件原理實現(xiàn)細節(jié)及設計意義的詳細內容,更多關于Parquet文件原理設計的資料請關注腳本之家其它相關文章!

相關文章

  • Mybatis的幾種傳參方式詳解

    Mybatis的幾種傳參方式詳解

    這篇文章主要介紹了Mybatis的幾種傳參方式詳解,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-09-09
  • 簡單了解JAVA SimpleDateFormat yyyy和YYYY的區(qū)別

    簡單了解JAVA SimpleDateFormat yyyy和YYYY的區(qū)別

    這篇文章主要介紹了簡單了解JAVA SimpleDateFormat yyyy和YYYY的區(qū)別,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-03-03
  • Java通過Freemarker模板實現(xiàn)生成Word文件

    Java通過Freemarker模板實現(xiàn)生成Word文件

    FreeMarker是一款模板引擎: 即一種基于模板和要改變的數(shù)據(jù), 并用來生成輸出文本的通用工具。本文將根據(jù)Freemarker模板實現(xiàn)生成Word文件,需要的可以參考一下
    2022-09-09
  • Java實戰(zhàn)之圖書管理系統(tǒng)的實現(xiàn)

    Java實戰(zhàn)之圖書管理系統(tǒng)的實現(xiàn)

    這篇文章主要介紹了如何利用Java語言編寫一個圖書管理系統(tǒng),文中采用的技術有Springboot、SpringMVC、MyBatis、ThymeLeaf 等,需要的可以參考一下
    2022-03-03
  • Spring Boot啟動及退出加載項的方法

    Spring Boot啟動及退出加載項的方法

    這篇文章主要介紹了Spring Boot啟動及退出加載項的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-04-04
  • Java由淺入深細數(shù)數(shù)組的操作下

    Java由淺入深細數(shù)數(shù)組的操作下

    數(shù)組對于每一門編程語言來說都是重要的數(shù)據(jù)結構之一,當然不同語言對數(shù)組的實現(xiàn)及處理也不盡相同。Java?語言中提供的數(shù)組是用來存儲固定大小的同類型元素
    2022-04-04
  • Java中反射動態(tài)代理接口的詳解及實例

    Java中反射動態(tài)代理接口的詳解及實例

    這篇文章主要介紹了Java中反射動態(tài)代理接口的詳解及實例的相關資料,需要的朋友可以參考下
    2017-04-04
  • Java基礎教程之List集合的常用方法

    Java基礎教程之List集合的常用方法

    這篇文章主要給大家介紹了關于Java基礎教程之List集合的常用方法,在Java編程中List集合是一種常用的數(shù)據(jù)結構,用于存儲一組元素,有時候我們需要對List集合中的元素進行分組操作,即將相同屬性或特征的元素歸類到一組,需要的朋友可以參考下
    2023-10-10
  • Maven項目打包成可執(zhí)行Jar文件步驟解析

    Maven項目打包成可執(zhí)行Jar文件步驟解析

    這篇文章主要介紹了Maven項目如何打包成可執(zhí)行Jar文件,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-05-05
  • springboot?sleuth?日志跟蹤問題記錄

    springboot?sleuth?日志跟蹤問題記錄

    Spring?Cloud?Sleuth是一個在應用中實現(xiàn)日志跟蹤的強有力的工具,使用Sleuth庫可以應用于計劃任務?、多線程服務或復雜的Web請求,尤其是在一個由多個服務組成的系統(tǒng)中,這篇文章主要介紹了springboot?sleuth?日志跟蹤,需要的朋友可以參考下
    2023-07-07

最新評論