存算分離下寫性能提升10倍以上,EMR Spark引擎是如何做到的?

  • 2020 年 11 月 2 日
  • 筆記

​引言

 

隨著大數據技術架構的演進,存儲與計算分離的架構能更好的滿足用戶對降低數據存儲成本,按需調度計算資源的訴求,正在成為越來越多人的選擇。相較 HDFS,數據存儲在對象存儲上可以節約存儲成本,但與此同時,對象存儲對海量文件的寫性能也會差很多。

 

騰訊雲彈性 MapReduce(EMR) 是騰訊雲的一個雲端託管的彈性開源泛 Hadoop 服務,支援 Spark、Hbase、Presto、Flink、Druid 等大數據框架。

 

近期,在支援一位 EMR 客戶時,遇到典型的存儲計算分離應用場景。客戶使用了 EMR 中的 Spark 組件作為計算引擎,數據存儲在對象存儲上。在幫助客戶技術調優過程中,發現了 Spark 在海量文件場景下寫入性能比較低,影響了架構的整體性能表現。

 

在深入分析和優化後,我們最終將寫入性能大幅提升,特別是將寫入對象存儲的性能提升了 10 倍以上,加速了業務處理,獲得了客戶好評。

 

本篇文章將介紹在存儲計算分離架構中,騰訊雲 EMR Spark 計算引擎如何提升在海量文件場景下的寫性能,希望與大家一同交流。文章作者:鍾德艮,騰訊後台開發工程師。

 

一、問題背景

 

Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎,可用來構建大型的、低延遲的數據分析應用程式。Spark 是 UC Berkeley AMP lab (加州大學伯克利分校的 AMP 實驗室)所開源的類 Hadoop MapReduce 的通用並行框架,Spark 擁有 Hadoop MapReduce 所具有的優點。

 

與 Hadoop 不同,Spark 和 Scala 能夠緊密集成,其中的 Scala 可以像操作本地集合對象一樣輕鬆地操作分散式數據集。儘管創建 Spark 是為了支援分散式數據集上的迭代作業,但是實際上它是對 Hadoop 的補充,可以在 Hadoop 文件系統中並行運行,也可以運行在雲存儲之上。

 

在這次技術調優過程中,我們研究的計算引擎是 EMR 產品中的 Spark 組件,由於其優異的性能等優點,也成為越來越多的客戶在大數據計算引擎的選擇。

 

存儲上,客戶選擇的是對象存儲。在數據存儲方面,對象存儲擁有可靠,可擴展和更低成本等特性,相比 Hadoop 文件系統 HDFS,是更優的低成本存儲方式。海量的溫冷數據更適合放到對象存儲上,以降低成本。

 

在 Hadoop 的生態中,原生的 HDFS 存儲也是很多場景下必不可少的存儲選擇,所以我們也在下文加入了與 HDFS 的存儲性能對比。

 

回到我們想解決的問題中來,先來看一組測試數據,基於 Spark-2.x 引擎,使用 SparkSQL 分別對 HDFS、對象存儲寫入 5000 文件,分別統計執行時長:

 

從測試結果可以看出,寫入對象存儲耗時是寫入 HDFS 的 29 倍,寫入對象存儲的性能要比寫入 HDFS 要差很多。而我們觀察數據寫入過程,發現網路 IO 並不是瓶頸,所以需要深入剖析一下計算引擎數據輸出的具體過程。

 

二、Spark數據輸出過程剖析

 

1. Spark數據流

 

先通過下圖理解一下 Spark 作業執行過程中數據流轉的主要過程:

 

 

首先,每個 task 會將結果數據寫入底層文件系統的臨時目錄 _temporary/task_[id],目錄結果示意圖如下所示:

 

 

到此為止,executor 上的 task 工作其實已經結束,接下來將交由 driver,將這些結果數據文件 move 到 hive 表最終所在的 location 目錄下,共分三步操作:

 

第一步,調用 OutputCommiter 的 commitJob 方法做臨時文件的轉存和合併:

 

 

通過上面示意圖可以看到,commitJob 會將 task_[id] 子目錄下的所有數據文件 merge 到了上層目錄 ext-10000。

 

接下來如果是 overwrite 覆蓋寫數據模式,會先將表或分區中已有的數據移動到 trash 回收站。

 

在完成上述操作後,會將第一步中合併好的數據文件,move 到 hive 表的 location,到此為止,所有數據操作完成。

 

2. 定位分析根因

 

有了上面對 Spark 數據流的分析,現在需要定位性能瓶頸在 driver 端還是 executor 端?觀察作業在 executor 上的耗時:

 

 

發現作業在 executor 端執行時長差異不大,而總耗時卻差異卻非常大, 這說明作業主要耗時在 driver 端。

 

在 driver 端有 commitJob、trashFiles、moveFiles 三個操作階段,具體是在 driver 的哪些階段耗時比較長呢?

 

我們通過 spark-ui 觀察 Thread dump (這裡通過手動刷新 spark-ui 或者登錄 driver 節點使用 jstack 命令查看執行緒堆棧資訊),發現這三個階段都比較慢, 下面我們來分析這三部分的源碼。

 

 

 

3. 源碼分析

 

(1)JobCommit階段

 

Spark 使用的是 Hadoop 的 FileOutputCommitter 來處理文件合併操作, Hadoop 2.x 默認使用 mapreduce.fileoutputcommitter.algorithm.version=1,使用單執行緒 for 循環去遍歷所有 task 子目錄,然後做 merge path 操作,顯然在輸出文件很多情況下,這部分操作會非常耗時。

 

特別是對對象存儲來說,rename 操作並不僅僅是修改元數據,還需要去 copy 數據到新文件。

 

(2)TrashFiles階段

 

trashFiles 操作是單執行緒 for 循環來將文件 move 到文件回收站,如果需要被覆蓋寫的數據比較多,這步操作會非常慢。

 

(3)MoveFiles階段

 

與前面問題類似,在 moveFiles 階段也是採用了單執行緒 for 循環方式來 move 文件。

 

4. 問題小結

 

  • Spark 引擎寫海量文件性能瓶頸在Driver端;

  • 在 Driver 的 CommitJob、TrashFiles、MoveFiles 三個階段執行耗時都比較長;

  • 三個階段耗時長的原因都是因為單執行緒循環挨個處理文件;

  • 對象存儲的 rename 性能需要拷貝數據,性能較差,導致寫海量文件時耗時特別長。 

 

三、優化結果

 

可以看到社區版本大數據計算引擎在處理對象存儲的訪問上還在一定的性能問題,主要原因是大多數數據平台都是基於 HDFS 存儲,而 HDFS 對文件的 rename 只需要在 namenode 上修改元數據,這個操作是非常快的,不容易碰到性能瓶頸。

 

而目前數據上雲、存算分離是企業降低成本的重要考量,所以我們分別嘗試將 commitJob、trashFiles、moveFile 程式碼修改成多執行緒並行處理文件,提升對文件寫操作性能。

 

基於同樣的基準測試,使用 SparkSQL 分別對 HDFS、對象存儲寫入 5000 個文件,我們得到了優化後的結果如下圖所示:

 

最終寫 HDFS 性能提升 41%,寫對象存儲性能提升 1100% !

 

四、結語

 

從上述的分析過程看到,問題的關鍵是 Spark 引擎某些處理部分的單執行緒限制造成的。單執行緒限制其實是比較常見的技術瓶頸。雖然我們在一開始也有猜測這種可能性,但具體限制在哪一部分還需要理清思路,踏實的查看源程式碼和多次調試。

 

另外分析中也發現了對象存儲自身的限制,其 rename 性能需要拷貝數據,導致寫海量文件耗時長,我們也還在持續進行改進。

 

對存儲計算分離應用場景深入優化,提升性能,更好的滿足客戶對存儲計算分離場景下降本增效的需求,是我們騰訊雲彈性 MapReduce(EMR) 產品研發團隊近期的重要目標,歡迎大家一起交流探討相關問題。