再談Spark下寫S3文件的File Output Committer問題

在《聊一聊Spark寫文件的機制——如何保證數據一致性》一文中,我們分析了Spark寫文件的機制,探討了多個File Output Committer在性能與數據一致性上的權衡,以及針對AWS S3這樣的對象存儲的優化思路。文章結尾處,曾提到我們將會採用EMRFS S3-optimized Committer來解決Rename機制帶來的性能與一致性問題。然而,最近在嘗試使用這個Committer時,發現其並不如預期那麼美好,存在諸多問題,這裏將其總結下來,與諸位同道中人分享。


1. 前言

我們先來回顧下之前介紹過的三種Committer:FileOutputCommitter V1、FileOutputCommitter V2、S3A Committer,其基本代表了整體的演進趨勢。

FileOutputCommitter V1,採用兩次Commit的方式來保證較強的一致性,每次Commit都對應一次文件的Rename。每個Task先將數據寫入到Task的臨時目錄下,寫完後將其Rename到Job的臨時目錄下;所有Task都完成後,由Job負責將其臨時目錄下的所有文件Rename到正式目錄下,此時文件對外可見。對於HDFS而已,Rename是一個十分高效的操作,然而對於S3這樣的對象存儲來說,則有着很大的代價。原因在於,S3本身並不是文件系統,不存在Rename操作,一個Rename操作需要分解爲List + Copy + Delete操作。因此,對於S3而言,兩次Rename有着非常大的性能開銷。我們經常發現,Spark UI上看到各個Task已經結束了,但是Job就是遲遲不結束,有點像hang住了,其實就是在做第二次Rename。

FileOutputCommitter V2,在V1的基礎上減去了第二次Rename,即每個Task先將數據寫入到Task的臨時目錄下,寫完後直接將其Rename到正式目錄中;所有Task都完成後,Job只是寫入一個_SUCCESS文件來標識已完成。顯然,V2是犧牲一定的一致性來換取性能。因爲,如果Spark Job在執行過程中失敗了,就會出現部分成功的Task寫入的文件對外可見,成爲髒數據。

S3A Committer,最初由Netflix貢獻給社區,採用S3 Multipart Upload機制替換了Rename機制。原因在於,對於S3而言,Rename不僅僅會帶來性能問題,還可能因爲S3的“最終一致性”特性而失敗。社區版的這個Committer,會在每個Task中將數據先寫入到本地磁盤,然後採用Multipart Upload方式上傳到S3;所有Task都完成後,由Job統一向S3發送Complete信號,此時文件對外可見。

綜合來看,使用Spark往S3寫入文件時,應該儘量選擇基於S3 Multipart Upload機制的Committer。在我們的系統中,主要採用AWS EMR來構建Spark集羣,數據寫入S3存儲。EMR在5.19.0之後引入了EMRFS S3-optimized Committer,同樣採用S3 Multipart Upload機制,因此我們會優先使用這個Committer。

本文所述均基於EMR5.29.0、Spark2.4.4。


2. AWS EMRFS S3-optimized Committer

EMRFS S3-optimized Committer,如AWS官方博客所言,其思想來自於S3A Committer,但是就目前的實現來看,坦白說,個人感覺比較雞肋。一方面,這是官方出品,雖然不開源,但是相信其內部做了很多跟EMR集成的東西,因此我們通常會默認選擇使用;另一方面,它有兩個比較大的缺陷:

  • 目前只能對部分符合條件的語法代碼生效,比如只能是寫Parquet文件,具體可以參考官方文檔
  • 它只是在FileOutputCommitter V2的基礎上進行了改進,即將V2中的Rename機制替換爲了S3 Multipart Upload機制,因此V2存在的數據一致性問題它也存在

對於第一個缺陷,我們在使用中經常戰戰兢兢,需要確認是否有效觸發了這個Committer。以下面代碼爲例,我們在Spark Streaming中,每10分鐘一個Batch,對數據進行加工處理後寫入到S3中。該代碼是否觸發到了這個Committer呢?通過INFO Log分析來看,是有觸發到的。下圖上半部分是一個Executor的Log,下半部分是Driver的Log,讀者可以參考這個來判斷是否有效觸發。

df.write.mode("append")\
		.partitionBy("ts_interval", "schema_version") \
        .option("path", s3_path)\
        .saveAsTable(table)

3. Job失敗帶來的數據不一致問題

這裏重點探討一下第二個缺陷,即Job失敗帶來的數據不一致問題,也是FileOutputCommitter V2存在的問題。如下圖,假設一個Job有12個Task,執行過程中Task 0~2成功了,而其他失敗了,進而導致Job失敗了。此時,在S3上就可以看到前面三個Task寫入的文件,當這個Job重做一次時,已經寫入的文件的數據就會成爲重複的數據。

針對這個問題,有哪些解法呢?目前,就我們接觸到的而言,主要有三種方法,可以分別應用在不同的業務場景。

第一種,每次寫入的目錄都帶有一個UUID,在整體文件寫入成功後,將這個目錄分發出去,給下游的Reader使用。比如下面的代碼中,seq就扮演着這樣的角色。但是,這樣做還是會有數據殘留在S3中的,只是暫時不會被下游Reader讀到而已。

path = "s3://{bucket}/type={type}/ts_interval={ts_interval}/seq={uuid}" \
                    .format(bucket=args["bucket"],
                            type=log_type,
                            ts_interval=ts_interval,
                            uuid=uuid.uuid1())
df.write.parquet(path)

第二種,採用overwrite寫入的方式,該方式需要有一個UUID來標識某一批數據,保證該批數據在多次寫入時UUID是不變的,而不同批次的數據的UUID是不同的。比如,在Spark Streaming中,每個Batch的數據,就可以使用Batch Timestamp來作爲這個UUID。在下面的代碼中,就可以達到這個效果,然而遺憾的是,AWS官方文檔明確提出了目前這種寫法無法觸發到EMRFS S3-optimized Committer。

df.write.mode("overwrite")\
		.partitionBy("ts_interval", "schema_version", "ts_batch") \
        .option("path", s3_path)\
        .option("partitionOverwriteMode", "dynamic")
        .saveAsTable(table)

第三種,保持寫文件的方式不變,但是在Job失敗後,捕獲其異常,然後進行一次補償,即刪除掉多餘的文件。我們知道,每次commitJob成功後,都會寫入一個_SUCCESS文件來標識整體寫入成功。如果在這個文件的Last Modified Time之後又有一些新的文件殘留在S3上,我們就認爲其實髒數據,將其刪除。當然,隨着數據量的積累,我們不可能檢測所有的數據,不過對於數據實時上傳的業務而言,只要檢測最近一段時間內的數據文件就好了。


4. 殘留數據問題

除了上述的問題之外,採用S3 Multipart Upload機制實現的Committer還會存在一些共性的數據殘留問題,需要在實踐中有所注意。殘留的數據主要來自兩方面:

  • 每個Task的數據會先寫入到本地磁盤,比如上面的“/mnt/s3/emrfs-4425809305170904769/0000000000”,如果Task中斷,有可能會有數據殘留在本地
  • S3 Multipart Upload會先將上傳的多個Part的文件放在S3的cache隱藏目錄,如果Task中斷,有可能會有數據殘留在S3

對於第一方面,通過腳本監控相應的本地目錄的磁盤大小並定期清理掉歷史悠久的數據即可,避免磁盤被用爆了。

對於第二方面,殘留在S3上的數據雖然對外不可見,但是會被收取存儲費用的,因此需要進行相關清理,目前有兩種方式:

  • 設置Spark參數fs.s3.multipart.clean.enabled,該方式會啓動一個異步進程來定期清理,會有一定的負載壓力
  • 在S3中配置相關Policy Lifecyle的屬性即可,我們更傾向於這種方式,由S3來負責解決,沒有額外開銷
<LifecycleConfiguration>
    <Rule>
        <ID>sample-rule</ID>
        <Prefix></Prefix>
        <Status>Enabled</Status>
        <AbortIncompleteMultipartUpload>
          <DaysAfterInitiation>7</DaysAfterInitiation>
        </AbortIncompleteMultipartUpload>
    </Rule>
</LifecycleConfiguration>

以上便是當前我們在AWS EMRFS S3-optimized Committer,雖然存在諸多問題,我們還是儘量優先選擇使用,畢竟是官方出品的,也期望其能做的越來越好。



(全文完,本文地址:https://bruce.blog.csdn.net/article/details/105800045

版權聲明:本人拒絕不規範轉載,所有轉載需徵得本人同意,並且不得更改文字與圖片內容。大家相互尊重,謝謝!

Bruce
2020/05/03 下午

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