引言
隨著大資料技術架構的演進,儲存與計算分離的架構能更好的滿足使用者對降低資料儲存成本,按需排程計算資源的訴求,正在成為越來越多人的選擇。相較 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) 產品研發團隊近期的重要目標,歡迎大家一起交流探討相關問題。