【華爲雲技術分享】Spark中的文件源(上)

摘要:

在大數據/數據庫領域,數據的存儲格式直接影響着系統的讀寫性能。Spark針對不同的用戶/開發者,支持了多種數據文件存儲方式。本文的內容主要來自於Spark AI Summit 2019中的一個talk【1】,我們將整個talk分爲上下兩個部分,上文會以概念爲主介紹spark的文件/數據組織方式,下文中則通過例子講解spark中的讀寫流程。本文是上半部分,首先會對spark中幾種流行的文件源(File Sources)進行特性介紹,這裏會涉及行列存儲的比較。然後會介紹兩種不同的數據佈置(Data layout),分別是partitioning以及bucketing,它們是spark中兩種重要的查詢優化手段。

1. 文件格式

在介紹文件格式之前,不得不提一下在存儲過程中的行(Row-oriented)、列(Column-oriented)存儲這兩個重要的數據組織方式,它們分別適用於數據庫中OLTP和OLAP不同的場景。spark對這兩類文件格式都有支持,列存的有parquet, ORC;行存的則有Avro,JSON, CSV, Text, Binary。

下面用一個簡單的例子說明行列兩種存儲格式的適用場景:

在上圖的music表中,如果用列存和行存存儲會得到下面兩種不同的組織方式。在左邊的列存中,同一列的數據被組織在一起,當一列數據存儲完畢時,接着下一列的數據存放,直到數據全部存好;而在行存中,數據按照行的順序依次放置,同一行中包括了不同列的一個數據,在圖中通過不同的顏色標識了數據的排列方法。

如果使用列存去處理下面的查詢,可以發現它只涉及到了兩列數據(album和artist),而列存中同一列的數據放在一起,那麼我們就可以快速定位到所需要的列的位置,然後只讀取查詢中所需要的列,有效減少了無用的數據IO(year 以及 genre)。同樣的,如果使用行存處理該查詢就無法起到 “列裁剪“” 的作用,因爲一列中的數據被分散在文件中的各個位置,每次IO不可避免地需要讀取到其他的數據,所以需要讀取表中幾乎所有的數據才能滿足查詢的條件。

通過這個例子可以發現,列存適合處理在幾個列上作分析的查詢,因爲可以避免讀取到不需要的列數據,同時,同一列中的數據放置在一起也十分適合壓縮。但是,如果需要對列存進行INSET INTO操作呢?它需要挪動幾乎所有數據,效率十分低下。行存則只需要在文件末尾append一行數據即可。在學術界,有人爲了中和這兩種“極端”的存儲方式,提出了行列混存來設計HTAP(Hybrid transactional/analytical processing)數據庫,感興趣的讀者可以參考【2】。

所以簡單總結就是:列存適合讀密集的workload,特別是那些僅僅需要部分列的分析型查詢;行存適合寫密集的workload,或者是要求所有列的查詢。

1.2 文件結構介紹

  • Parquet

在Parquet中,首尾都是parquet的magic number,用於檢驗該文件是否是一個parquet文件。Footer放在文件的末尾,存放了元數據信息,這裏包括schema信息,以及每個row group的meta data。每個row group是一系列行數據的組成,row group中的每個column是一個列。

parquet格式能有效應用查詢優化中的優化規則,比如說謂詞下推(Predicate Push),將filter的條件推到掃描(Scan)數據時進行,減少了上層操作節點不必要的計算。又比如通過設置元數據中的min/max,在查詢時可以拿着條件和元數據進行對比,如果查詢條件完全不符合min/max,則可以直接跳過元數據所指的數據塊,減少了無用的數據IO。

  • ORC

ORC全稱是Optimized Row Columnar,它的組織方式如下圖,其中

Postsctipt保存了該表的行數,壓縮參數,壓縮大小,列等信息;

File Footer中是各個stripe的位置信息,以及該表的統計結果;

數據分成一個個stripe,對應於parquet中的row group;

Stripe Footer主要是記錄每個stripe的統計信息,包括min,max,count等;

Row data是數據的具體存儲;

Index Data保存該stripe數據的具體位置,總行數等。

它們之間的關係在上圖中用虛實線做了很好的補充。

行存文件格式

行存相較於列存會比較簡單,在實際開發中可能也接觸會相對較多,所以這裏簡單介紹其優缺點。

  • Avro:Avro的特點就是快速以及可壓縮,並且支持schema的操作,比如增加/刪除/重新命名一個字段,更改默認值等。

  • JSON:在Spark中,通常被當做是一個結構體,在使用過程中需要注意key的數目(容易觸發OOM錯誤),它對schema的支持並不是很好。優點是輕量,容易部署以及便於debug。

  • CSV:通常用於數據的收集,比如說日誌信息等,寫性能比讀性能好,它的缺點是文件規範的不夠標準(分隔符,轉義符,引號),對嵌套數據類型的支持不足等。它和JSON都屬於半結構化的文本結構。

  • Raw text file:基於行的文本文件,在spark中可通過 spark.read.text()直接讀入並按行切分,但是需要保持行的size在一個合理的值,支持有限的schema。

  • Binary:二進制文件,是Spark 3.0 的新特性。Spark會讀取每個binary文件並轉化成一條記錄(record),該記錄(record)會存儲原始的二進制數據以及文件的matedata。這裏記錄(record)是一個schema,包括文件的路徑(StringType),文件被修改的時間(TimestampType),文件的長度(LongType)以及內容(BinaryType)。

例如,如果我們需要遞歸讀取某目錄下所有的JPG文件則可以通過下面的API來完成:

spark.read.format("binaryFile")
.option("pathGlobFilter", "*.jpg")
.option("recursiveFileLookup", "true")
.load("/path/to/dir")

2. 數據佈置(Data layout)

2.1 partitioning

分區(Partition)是指當數據量很大時,可以按照某種方式對數據進行粗粒度切分的方式,比如在上圖中按year字段進行了切分,在year字段內部,又將genre字段進行了切分。這樣帶來的好處也是顯而易見的,當處理“year = 2019 and genre = ‘folk’”的查詢時,就可以過濾掉不需要掃描的數據,直接定位到相應的切片中去做查詢,提高了查詢效率。

在Spark SQL和DataFrame API分別提供了相應的創建partition的方式。

同時,越多的分區並不意味着越好的性能。當分區越多時,分區的文件數也隨着增多,這給metastore獲取分區的數據以及文件系統list files帶來了很大的壓力,這也降低了查詢的性能。所以建議就是,選取合適的字段做分區,該字段不應出現過多的distinct values,使分區數處於一個合適的數目。如果distinct values很多怎麼辦?可以嘗試將字段hash到合適的桶中,或是可以使用字段中的一小部分作爲分區字段,比如name中的第一個字母。

2.2 bucketing

在Spark的join操作中,如果兩邊的表都比較大,會需要數據的shuffle,shuffle數據會佔據查詢過程中大量的時間,當某個耗時的Join的字段被頻繁使用時,我們可以通過使用分桶(bucketing)的手段來優化該類查詢。通過分桶,我們將數據按照joinkey預先shuffle及排序,每次處理sort merge join時,只需要各自將自己本地的數據處理完畢即可,減少了shuffle的耗時。這裏要注意,分桶表的性能和分桶的個數密切相關,過多的分桶會導致小文件問題,而過少的分桶會導致併發度太小從而影響性能。

分桶前的Sort merge join:

   

分桶後: 

 

在Spark SQL和DataFrame API分別提供了相應的創建分桶的方式。通過排序,我們也可以記錄好min/max,從而避免讀取無用的數據。

參考

【1】Databricks. 2020. Apache Spark's Built-In File Sources In Depth - Databricks. [online] Available at: <https://databricks.com/session_eu19/apache-sparks-built-in-file-sources-in-depth>.

【2】 Bridging the Archipelago betweenRow-Stores and Column-Stores for Hybrid Workloads (SIGMOD'16)

 

點擊這裏,瞭解更多精彩內容

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