對(duì) spark 任務(wù)數(shù)據(jù)落地(HDFS) 碎片文件過(guò)多的問(wèn)題的優(yōu)化實(shí)踐及思考。
背景
此文是關(guān)于公司在 Delta Lake 上線之前對(duì)Spark任務(wù)寫(xiě)入數(shù)據(jù)產(chǎn)生碎片文件優(yōu)化的一些實(shí)踐。
-
形成原因
數(shù)據(jù)在流轉(zhuǎn)過(guò)程中經(jīng)歷 filter/shuffle 等過(guò)程后,開(kāi)發(fā)人員難以評(píng)估作業(yè)寫(xiě)出的數(shù)據(jù)量。即使使用了 Spark 提供的AE功能,目前也只能控制 shuffle read 階段的數(shù)據(jù)量,寫(xiě)出數(shù)據(jù)的大小實(shí)際還會(huì)受壓縮算法及格式的影響,因此在任務(wù)運(yùn)行時(shí),對(duì)分區(qū)的數(shù)據(jù)評(píng)估非常困難。- shuffle 分區(qū)過(guò)多過(guò)碎,寫(xiě)入性能會(huì)較差且生成的小文件會(huì)非常多。
- shuffle 分區(qū)過(guò)少過(guò)大,則寫(xiě)入并發(fā)度可能會(huì)不夠,影響任務(wù)運(yùn)行時(shí)間。
不利影響
在產(chǎn)生大量碎片文件后,任務(wù)數(shù)據(jù)讀取的速度會(huì)變慢(需要尋找讀入大量的文件,如果是機(jī)械盤(pán)更是需要大量的尋址操作),同時(shí)會(huì)對(duì) hdfs namenode 內(nèi)存造成很大的壓力。
在這種情況下,只能讓業(yè)務(wù)/開(kāi)發(fā)人員主動(dòng)的合并下數(shù)據(jù)或者控制分區(qū)數(shù)量,提高了用戶的學(xué)習(xí)及使用成本,往往效果還非常不理想。
既然在運(yùn)行過(guò)程中對(duì)最終落地?cái)?shù)據(jù)的評(píng)估如此困難,是否能將該操作放在數(shù)據(jù)落地后進(jìn)行?對(duì)此我們進(jìn)行了一些嘗試,希望能自動(dòng)化的解決/緩解此類問(wèn)題。
一些嘗試
大致做了這么一些工作:
- 修改 Spark FileFormatWriter 源碼,數(shù)據(jù)落盤(pán)時(shí),記錄相關(guān)的 metrics,主要是一些分區(qū)/表的記錄數(shù)量和文件數(shù)量信息。
- 在發(fā)生落盤(pán)操作后,會(huì)自動(dòng)觸發(fā)碎片文件檢測(cè),判斷是否需要追加合并數(shù)據(jù)任務(wù)。
- ?實(shí)現(xiàn)一個(gè) MergeTable 語(yǔ)法用于合并表/分區(qū)碎片文件,通過(guò)系統(tǒng)或者用戶直接調(diào)用。
第1和第2點(diǎn)主要是平臺(tái)化的一些工作,包括監(jiān)測(cè)數(shù)據(jù)落盤(pán),根據(jù)采集的 metrics 信息再判斷是否需要進(jìn)行 MergeTable 操作,下文是關(guān)于 MergeTable 的一些細(xì)節(jié)實(shí)現(xiàn)。
MergeTable
功能:
- 能夠指定表或者分區(qū)進(jìn)行合并
- 合并分區(qū)表但不指定分區(qū),則會(huì)遞歸對(duì)所有分區(qū)進(jìn)行檢測(cè)合并
- ?指定了生成的文件數(shù)量,就會(huì)跳過(guò)規(guī)則校驗(yàn),直接按該數(shù)量進(jìn)行合并
語(yǔ)法:
merge table [表名] [options (fileCount=合并后文件數(shù)量)] --非分區(qū)表
merge table [表名] PARTITION (分區(qū)信息) [options (fileCount=合并后文件數(shù)量)] --分區(qū)表
碎片文件校驗(yàn)及合并流程圖?:
性能優(yōu)化
對(duì)合并操作的性能優(yōu)化
只合并碎片文件
如果設(shè)置的碎片閾值是128M,那么只會(huì)將該表/分區(qū)內(nèi)小于該閾值的文件進(jìn)行合并,同時(shí)如果碎片文件數(shù)量小于一定閾值,將不會(huì)觸發(fā)合并,這里主要考慮的是合并任務(wù)存在一定性能開(kāi)銷(xiāo),因此允許系統(tǒng)中存在一定量的小文件?。-
分區(qū)數(shù)量及合并方式
定義了一些規(guī)則用于計(jì)算輸出文件數(shù)量及合并方式的選擇,獲取任務(wù)的最大并發(fā)度 maxConcurrency 用于計(jì)算數(shù)據(jù)的分塊大小,再根據(jù)數(shù)據(jù)碎片文件的總大小選擇合并(coalesce/repartition)方式。- 開(kāi)啟 dynamicAllocation
maxConcurrency = spark.dynamicAllocation.maxExecutors * spark.executor.cores
- 未開(kāi)啟 dynamicAllocation
maxConcurrency = spark.executor.instances * spark.executor.cores
以幾個(gè)場(chǎng)景為例對(duì)比優(yōu)化前后?的性能:
? 場(chǎng)景1:最大并發(fā)度100,碎片文件數(shù)據(jù)100,碎片文件總大小100M,如果使用 coalesce(1),將會(huì)只會(huì)有1個(gè)線程去讀/寫(xiě)數(shù)據(jù),改為 repartition(1),則會(huì)有100個(gè)并發(fā)讀,一個(gè)線程順序?qū)憽P阅芟嗖?00X。? 場(chǎng)景2:最大并發(fā)度100,碎片文件數(shù)量10000,碎片文件總大小100G,如果使用 repartition(200),將會(huì)導(dǎo)致100G的數(shù)據(jù)發(fā)生 shuffle,改為 coalesce(200),則能在保持相同并發(fā)的情況下避免 200G數(shù)據(jù)的IO。
? 場(chǎng)景3:最大并發(fā)度200,碎片文件數(shù)量10000,碎片文件總大小50G,如果使用 coalesce(100),會(huì)保存出100個(gè)500M文件,但是會(huì)浪費(fèi)一半的計(jì)算性能,改為 coalesce(200),合并耗時(shí)會(huì)下降為原來(lái)的50%。
上述例子的核心都是在充分計(jì)算資源的同時(shí)避免不必要的IO。
- 開(kāi)啟 dynamicAllocation
修復(fù)元數(shù)據(jù)
因?yàn)?merge 操作會(huì)修改數(shù)據(jù)的創(chuàng)建及訪問(wèn)時(shí)間,所以在目錄替換時(shí)需要將元數(shù)據(jù)信息修改到 merge 前的一個(gè)狀態(tài),該操作還能避免冷數(shù)據(jù)掃描的誤判。最后還要調(diào)用 refresh table 更新表在 spark 中的狀態(tài)緩存。?commit 前進(jìn)行校驗(yàn)
在最終提交前對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),判斷合并前后數(shù)據(jù)量是否發(fā)生變化(從數(shù)據(jù)塊元數(shù)據(jù)中直接獲取數(shù)量,避免發(fā)生IO),存在異常則會(huì)進(jìn)行回滾,放棄合并操作。?
數(shù)據(jù)寫(xiě)入后,自動(dòng)合并效果圖:
后記
收益
該同步合并的方式已經(jīng)在我們的線上穩(wěn)定運(yùn)行了1年多,成功的將平均文件大小從150M提升到了270M左右,提高了數(shù)據(jù)讀取速度,與此同時(shí) Namenode 的內(nèi)存壓力也得到了極大緩解。
?對(duì) MergeTable 操作做了上述的相關(guān)優(yōu)化后,根據(jù)不同的數(shù)據(jù)場(chǎng)景下,能帶來(lái)數(shù)倍至數(shù)十倍的性能提升。
缺陷
因?yàn)椴捎玫氖峭胶喜⒌姆绞?,由于沒(méi)有事務(wù)控制,所以在合并過(guò)程中數(shù)據(jù)不可用,這也是我們后來(lái)開(kāi)始引入 D?elta Lake 的一個(gè)原因。