算框架優(yōu)化實(shí)戰(zhàn):從瓶頸定位到數(shù)據(jù)傾斜調(diào)優(yōu))
1. 別急著調(diào)參先把瓶頸層次分清1.1 任務(wù)變慢首先要回答“慢在哪一層”做分布式計(jì)算框架優(yōu)化這幾年我見過一個(gè)非常普遍的現(xiàn)象任務(wù)一慢大家第一反應(yīng)就是調(diào)并行度、加內(nèi)存、改超時(shí)時(shí)間。結(jié)果參數(shù)改了一圈任務(wù)還是那個(gè)鳥樣甚至更慢了。分布式計(jì)算框架優(yōu)化這件事難點(diǎn)從來不在“調(diào)參數(shù)”本身而在于你怎么定位問題。一個(gè)跑得慢的分布式任務(wù)可能慢在四個(gè)完全不同的地方調(diào)度層、計(jì)算層、數(shù)據(jù)讀寫層、網(wǎng)絡(luò)傳輸層。不同層次的瓶頸對(duì)應(yīng)的優(yōu)化手段是完全不同甚至互相矛盾的。比如你盲目提高并行度如果是數(shù)據(jù)傾斜導(dǎo)致的慢任務(wù)數(shù)量越多長尾反而越明顯如果是小文件過多導(dǎo)致的慢并行度提上去之后文件碎片化只會(huì)更嚴(yán)重。所以我現(xiàn)在的習(xí)慣是先回答三個(gè)問題再動(dòng)手。第一任務(wù)慢是整個(gè)作業(yè)都慢還是只有某個(gè)階段慢如果整個(gè)作業(yè)都慢多半是資源總量不夠或者調(diào)度策略有問題如果只有特定階段慢那個(gè)階段內(nèi)部一定有異常。第二慢的階段是CPU密集、內(nèi)存密集還是網(wǎng)絡(luò)和磁盤密集這三個(gè)方向的優(yōu)化動(dòng)作完全不一樣。CPU密集要考慮簡(jiǎn)化計(jì)算邏輯、壓縮數(shù)據(jù)量內(nèi)存密集要查對(duì)象大小和GC情況網(wǎng)絡(luò)磁盤密集要重點(diǎn)看Shuffle讀寫量和數(shù)據(jù)本地性。第三任務(wù)運(yùn)行過程中有多少執(zhí)行單元是真正在干活的很多分布式任務(wù)出現(xiàn)性能問題的時(shí)候你會(huì)發(fā)現(xiàn)大量執(zhí)行單元處于空閑等待狀態(tài)只有少數(shù)幾個(gè)在拼命跑。這時(shí)候你再怎么加資源都沒有意義因?yàn)閱栴}不在資源總量而在數(shù)據(jù)分布。1.2 用一條流水線理解分布式作業(yè)的性能結(jié)構(gòu)我習(xí)慣把分布式計(jì)算框架比作一條多工位流水線數(shù)據(jù)是流水線上的物料每個(gè)執(zhí)行任務(wù)是一個(gè)工位框架的調(diào)度器就是工廠的排產(chǎn)系統(tǒng)。物料從上游流向下游經(jīng)過每個(gè)工位的處理最終產(chǎn)出結(jié)果。流水線有一個(gè)很直白的規(guī)律整條線的產(chǎn)出速度不由最快的工位決定而由最慢的工位決定。分布式計(jì)算框架里的“最慢工位”通常表現(xiàn)為兩類。一類是的確有個(gè)別工位處理的數(shù)據(jù)量特別大這就是數(shù)據(jù)傾斜另一類是工位本身不缺活干但物料傳輸太慢工位一直在空等數(shù)據(jù)這類問題常見于網(wǎng)絡(luò)擁塞、本地性差、序列化開銷高的場(chǎng)景。在動(dòng)手之前建議先打開框架自帶的執(zhí)行歷史頁面把每個(gè)階段的耗時(shí)分布圖看一遍。不管是什么框架只要是類MapReduce模型都會(huì)有類似的階段耗時(shí)視圖。你要找的是那些“尾巴”特別長的階段大部分任務(wù)幾秒鐘就跑完了但某個(gè)階段里有幾個(gè)任務(wù)跑了十幾分鐘甚至更長。這種長尾形態(tài)是最重要的線索??吹介L尾先別急著調(diào)參繼續(xù)往下定位大概率能發(fā)現(xiàn)問題本質(zhì)。2. 調(diào)度層優(yōu)化并行度、動(dòng)態(tài)分配與數(shù)據(jù)本地性2.1 并行度不是越大越好它有一個(gè)合理區(qū)間并行度是分布式計(jì)算框架里最常見的調(diào)整項(xiàng)但也是被誤解最多的一個(gè)參數(shù)。很多人認(rèn)為并行度開得越大任務(wù)跑得越快實(shí)際上并行度只是決定了任務(wù)被切成多少份來并行處理真正決定處理速度的是你有多少可用的CPU核心和內(nèi)存。最合理的并行度區(qū)間大致是集群可用CPU核心數(shù)的2到3倍。為什么不是1倍因?yàn)榉植际饺蝿?wù)的執(zhí)行階段之間有小幅波動(dòng)有的任務(wù)快有的任務(wù)慢留出一些余量可以讓執(zhí)行單元忙起來之后仍然有新任務(wù)可領(lǐng)。超過3倍之后收益就開始不明顯了反而會(huì)引入額外的調(diào)度開銷、序列化開銷和上下文切換開銷。反過來如果并行度小于核心數(shù)那么有大量CPU資源會(huì)被浪費(fèi)。舉個(gè)例子一個(gè)集群有32個(gè)執(zhí)行單元每個(gè)執(zhí)行單元2個(gè)核心那么可用核心數(shù)大約是64個(gè)并行度建議設(shè)定在128到192之間。我之前排查過一個(gè)任務(wù)并行度被人從200改成了2000結(jié)果單階段創(chuàng)建了兩千個(gè)小分區(qū)每個(gè)分區(qū)只裝了幾百條數(shù)據(jù)。執(zhí)行完一看絕大多數(shù)時(shí)間都花在任務(wù)的調(diào)度和元數(shù)據(jù)管理上真正計(jì)算的時(shí)間反而沒多少。把并行度調(diào)回256之后時(shí)間立刻降了一半還不止。并行度還有一個(gè)隱性作用它會(huì)影響Shuffle時(shí)生成的文件數(shù)量。并行度越高每個(gè)上游任務(wù)產(chǎn)生的輸出分區(qū)越多下游要拉取的數(shù)據(jù)塊就越碎。文件一多NameNode層面壓力大下游任務(wù)拉數(shù)據(jù)的次數(shù)也多。所以調(diào)整并行度的時(shí)候一定要同時(shí)關(guān)注Shuffle讀寫量和文件數(shù)量別只看任務(wù)跑完的時(shí)間。2.2 動(dòng)態(tài)資源分配什么時(shí)候開什么時(shí)候關(guān)動(dòng)態(tài)資源分配是一個(gè)很實(shí)用的調(diào)度層優(yōu)化手段。簡(jiǎn)單說就是框架根據(jù)當(dāng)前任務(wù)積壓情況動(dòng)態(tài)地向資源管理器申請(qǐng)或釋放執(zhí)行單元。任務(wù)多的時(shí)候就多申請(qǐng)幾個(gè)執(zhí)行單元任務(wù)少了就自動(dòng)回收避免資源閑著浪費(fèi)、忙時(shí)又不能用的尷尬局面。什么場(chǎng)景適合開動(dòng)態(tài)分配最典型的是混合負(fù)載的場(chǎng)景不同團(tuán)隊(duì)提交不同類型的作業(yè)作業(yè)到達(dá)時(shí)間不均勻有的凌晨跑批有的白天跑交互式查詢。這種場(chǎng)景下固定分配資源要么浪費(fèi)要么不夠用動(dòng)態(tài)分配能讓集群整體利用率上去不少。什么場(chǎng)景不適合開如果你的任務(wù)是分鐘級(jí)甚至秒級(jí)的短任務(wù)那動(dòng)態(tài)分配的收益就很小。因?yàn)槊總€(gè)任務(wù)的執(zhí)行時(shí)間本身就不長動(dòng)態(tài)申請(qǐng)資源也要花時(shí)間申請(qǐng)過程中任務(wù)可能已經(jīng)跑完了大半這反而拖慢了整體進(jìn)度。還有一種情況是任務(wù)本身對(duì)穩(wěn)定性要求極高比如核心鏈路的準(zhǔn)實(shí)時(shí)計(jì)算這種任務(wù)更推薦固定資源池保證資源隨時(shí)可用而不是和別的作業(yè)搶資源。如果決定開啟動(dòng)態(tài)分配有幾個(gè)參數(shù)值得注意。一個(gè)是執(zhí)行單元空閑回收的超時(shí)時(shí)間默認(rèn)值通常偏保守適合長任務(wù)如果你跑的是半小時(shí)以內(nèi)的短任務(wù)建議把回收時(shí)間調(diào)短一些讓資源能更快流轉(zhuǎn)給其他作業(yè)。另一個(gè)是同時(shí)運(yùn)行任務(wù)的并行度上限建議保留一個(gè)合理上限防止極端情況下資源被某個(gè)任務(wù)無限制地吞掉影響其他核心作業(yè)。2.3 數(shù)據(jù)本地性調(diào)度器最容易被忽視的一條規(guī)則調(diào)度器在給任務(wù)分配執(zhí)行單元的時(shí)候會(huì)盡量讓計(jì)算發(fā)生在數(shù)據(jù)所在的節(jié)點(diǎn)上。這樣任務(wù)可以直接讀取本機(jī)磁盤上的數(shù)據(jù)塊不用走網(wǎng)絡(luò)。數(shù)據(jù)本地性從好到差依次有四個(gè)級(jí)別進(jìn)程內(nèi)本地、節(jié)點(diǎn)內(nèi)本地、機(jī)架內(nèi)本地和跨機(jī)架讀取。級(jí)別每降一級(jí)網(wǎng)絡(luò)傳輸?shù)拈_銷就上一個(gè)臺(tái)階。在實(shí)踐里最容易出問題的不是本地性本身而是本地性“退化”。什么是退化本來任務(wù)應(yīng)該在本節(jié)點(diǎn)執(zhí)行但由于本節(jié)點(diǎn)的執(zhí)行單元不夠、節(jié)點(diǎn)負(fù)載過高、或者節(jié)點(diǎn)出了故障調(diào)度器等了一段時(shí)間之后發(fā)現(xiàn)本地執(zhí)行不了就會(huì)退而求其次把任務(wù)調(diào)度到別的節(jié)點(diǎn)上去讀遠(yuǎn)程數(shù)據(jù)。任務(wù)雖然跑起來了但代價(jià)是大量網(wǎng)絡(luò)IO。如何判斷本地性是否退化看執(zhí)行歷史里的本地性級(jí)別統(tǒng)計(jì)如果任務(wù)大量跑在“任意節(jié)點(diǎn)”級(jí)別而很少跑在“節(jié)點(diǎn)內(nèi)”級(jí)別那就要注意了。常見的修復(fù)方向有幾個(gè)檢查節(jié)點(diǎn)是否頻繁宕機(jī)或重啟、避免執(zhí)行單元數(shù)量被其他任務(wù)擠占、盡量讓任務(wù)隊(duì)列長度短一些。還有一個(gè)非常容易被忽略的原因數(shù)據(jù)文件放在外部存儲(chǔ)上每次讀取都要走網(wǎng)絡(luò)這時(shí)候本地性級(jí)別天然就不高優(yōu)化方向應(yīng)該是把熱數(shù)據(jù)預(yù)熱到本地緩存或者調(diào)整存儲(chǔ)掛載方式。3. 數(shù)據(jù)傾斜與Shuffle重分布默認(rèn)分區(qū)方案往往就是瓶頸3.1 從長尾任務(wù)到傾斜判定三步完成數(shù)據(jù)傾斜是分布式計(jì)算框架里最經(jīng)典也最讓人頭疼的問題。它的本質(zhì)是數(shù)據(jù)在分區(qū)之間分布不均勻某個(gè)分區(qū)處理的數(shù)據(jù)量遠(yuǎn)遠(yuǎn)大于其他分區(qū)。判定數(shù)據(jù)傾斜我一般分三步。第一步看耗時(shí)分布。打開執(zhí)行歷史如果某個(gè)階段的大部分任務(wù)都在一分鐘內(nèi)跑完但有少量任務(wù)跑了十分鐘以上而且這類任務(wù)的耗時(shí)是正常任務(wù)的三五倍起步基本可以鎖定傾斜嫌疑。第二步看每個(gè)任務(wù)讀取的記錄數(shù)。不用精確到條數(shù)看大體的量級(jí)就行。正常情況下各個(gè)任務(wù)讀取的數(shù)據(jù)量應(yīng)該在差不多的量級(jí)如果個(gè)別任務(wù)讀取的數(shù)據(jù)量是其他任務(wù)的幾十倍那傾斜幾乎是可以確定的。第三步看GC和內(nèi)存表現(xiàn)。傾斜任務(wù)通常會(huì)因?yàn)樘幚頂?shù)據(jù)量過大出現(xiàn)頻繁的垃圾回收甚至直接內(nèi)存溢出。GC日志里面如果看到長暫停頻繁出現(xiàn)而其他任務(wù)沒有這個(gè)現(xiàn)象那基本可以實(shí)錘了。數(shù)據(jù)傾斜最常見的高發(fā)場(chǎng)景有兩個(gè)。一個(gè)是寬表關(guān)聯(lián)關(guān)聯(lián)鍵集中在少數(shù)幾個(gè)熱門值上比如一個(gè)訂單系統(tǒng)里某個(gè)大客戶貢獻(xiàn)了全站一半以上的訂單量按客戶ID關(guān)聯(lián)時(shí)這個(gè)客戶對(duì)應(yīng)的分區(qū)就會(huì)被打爆。另一個(gè)是分組聚合比如按城市維度做統(tǒng)計(jì)一線城市的數(shù)據(jù)量可能是四五線城市的幾十倍導(dǎo)致聚合任務(wù)嚴(yán)重不均。3.2 加鹽和廣播兩種最常用的處理手段處理傾斜沒有萬能方案要根據(jù)操作類型選擇手段。我做得最多的兩組組合是聚合場(chǎng)景用“加鹽兩階段聚合”關(guān)聯(lián)場(chǎng)景用“小表廣播”。加鹽的思路其實(shí)很樸素既然某個(gè)鍵的數(shù)據(jù)太多那就把這個(gè)鍵拆散。第一階段在鍵上附加一個(gè)隨機(jī)后綴把原鍵拆成多個(gè)子鍵這樣原本一個(gè)分區(qū)承受的壓力就被分散到多個(gè)分區(qū)上第一階段在子鍵層面上做局部聚合然后第二階段去掉后綴再對(duì)原始鍵做一次最終聚合。這里有個(gè)細(xì)節(jié)加鹽的核心是隨機(jī)后綴的基數(shù)基數(shù)太小起不到拆分效果基數(shù)太大又會(huì)在第二階段聚合時(shí)引入額外開銷。比較穩(wěn)妥的做法是結(jié)合集群核心數(shù)來設(shè)一般取核心數(shù)的1到2倍不要讓第一階段產(chǎn)生太多臨時(shí)分區(qū)。還有一個(gè)限制條件需要特別留意如果業(yè)務(wù)要求精確結(jié)果而且計(jì)算里有去重、排序這類不能被兩階段拆解的操作那加鹽方案就不合適了只能退而求其次把熱點(diǎn)鍵單獨(dú)抽出來處理其余鍵走正常流程。廣播適用于關(guān)聯(lián)場(chǎng)景邏輯也很直白如果一張表很小比如幾MB到幾十MB那就把它廣播到每個(gè)執(zhí)行單元上去每個(gè)任務(wù)都在本地內(nèi)存里維護(hù)這張表的查找結(jié)構(gòu)關(guān)聯(lián)的時(shí)候直接查內(nèi)存完全不需要走Shuffle。這個(gè)方案對(duì)分布式框架優(yōu)化的收益非常大因?yàn)樗苯訌臋C(jī)制上消滅了關(guān)聯(lián)操作里的網(wǎng)絡(luò)傳輸。但廣播也有邊界條件。首先小表大小必須可控一般控制在幾十MB以內(nèi)太大的表廣播一次就要占滿執(zhí)行單元內(nèi)存。其次要確認(rèn)小表的內(nèi)容相對(duì)穩(wěn)定如果關(guān)聯(lián)鍵經(jīng)常變化、小表頻繁更新廣播的成本就會(huì)升高。我在實(shí)際項(xiàng)目中見過有人把一張幾百M(fèi)B的維表強(qiáng)行廣播結(jié)果每個(gè)執(zhí)行單元多吃了兩百多MB內(nèi)存整個(gè)集群的GC時(shí)間直線上升反而比Shuffle Join還慢。3.3 Shuffle相關(guān)配置排序、壓縮和緩沖區(qū)的取舍Shuffle是分布式計(jì)算框架中成本最高的環(huán)節(jié)之一因?yàn)樗殡S大量磁盤讀寫和網(wǎng)絡(luò)傳輸。圍繞Shuffle的優(yōu)化本質(zhì)上就是在四個(gè)開銷之間做平衡CPU壓縮開銷、磁盤IO開銷、網(wǎng)絡(luò)傳輸開銷和內(nèi)存緩沖開銷。先說分區(qū)數(shù)。Shuffle的分區(qū)數(shù)決定了下游讀取數(shù)據(jù)時(shí)的并行度也決定了生成文件的數(shù)量。一個(gè)常見的錯(cuò)誤是Shuffle分區(qū)數(shù)設(shè)置得過大產(chǎn)生大量幾KB甚至幾百字節(jié)的小文件。這些小文件的元數(shù)據(jù)開銷比數(shù)據(jù)本身還大。比較穩(wěn)妥的做法是讓Shuffle后每個(gè)分區(qū)的數(shù)據(jù)量控制在50MB到200MB之間。如果一群小文件鋪面而來優(yōu)先考慮重分區(qū)合并或者調(diào)小分區(qū)數(shù)。再說壓縮。Shuffle數(shù)據(jù)寫入磁盤和網(wǎng)絡(luò)傳輸時(shí)默認(rèn)會(huì)做壓縮。壓縮能減少磁盤占用和網(wǎng)絡(luò)傳輸量但代價(jià)是CPU開銷。如果你的集群CPU很緊張而磁盤和網(wǎng)絡(luò)相對(duì)寬裕可以考慮關(guān)閉壓縮或者換更輕量的壓縮算法反過來如果磁盤帶寬或網(wǎng)絡(luò)帶寬是瓶頸那就開更高壓縮比的算法。這個(gè)選擇沒有絕對(duì)標(biāo)準(zhǔn)完全取決于你的集群到底缺什么資源。我見過一個(gè)任務(wù)開啟高壓縮比后網(wǎng)絡(luò)傳輸量降了70%但CPU使用率漲了快一倍最終任務(wù)時(shí)間幾乎沒變。所以在壓縮這件事上一定要實(shí)測(cè)之后再?zèng)Q定。還有緩沖區(qū)。Shuffle讀寫過程中數(shù)據(jù)會(huì)經(jīng)過內(nèi)存緩沖區(qū)。緩沖區(qū)太小數(shù)據(jù)就要頻繁刷盤磁盤IO次數(shù)激增緩沖區(qū)太大會(huì)擠占執(zhí)行單元堆內(nèi)存誘發(fā)GC壓力。比較合理的做法是給Shuffle緩沖區(qū)一個(gè)適中的默認(rèn)值然后重點(diǎn)觀察GC耗時(shí)和磁盤IO等待時(shí)間??吹紾C耗時(shí)占比過高就調(diào)小緩沖區(qū)或增加執(zhí)行單元內(nèi)存看到磁盤等待時(shí)間居高不下就適當(dāng)調(diào)大緩沖區(qū)。4. 序列化、緩存與算子鏈從傳輸和存儲(chǔ)里摳時(shí)間4.1 序列化器的選擇直接影響CPU和網(wǎng)絡(luò)占用分布式計(jì)算里數(shù)據(jù)經(jīng)常要在任務(wù)之間傳輸、在磁盤和內(nèi)存之間讀寫。每一次傳輸和落盤都要經(jīng)過序列化和反序列化。序列化器的好壞直接決定了兩件事一是傳輸數(shù)據(jù)體積的大小二是CPU消耗的多少。Java內(nèi)置的序列化機(jī)制雖然用起來方便但效果很差。它序列化出來的對(duì)象體積往往是緊湊二進(jìn)制的3到10倍而且序列化過程本身CPU開銷也很大。對(duì)分布式計(jì)算框架來說默認(rèn)的Java序列化幾乎算得上性能毒藥。換成高性能序列化框架之后效果通常立竿見影。具體做法一般就是注冊(cè)類、啟用相關(guān)配置、把序列化器指定為緊湊二進(jìn)制類型。有幾點(diǎn)要注意第一類注冊(cè)后如果類的字段結(jié)構(gòu)發(fā)生變化舊數(shù)據(jù)可能反序列化失敗所以升級(jí)類的時(shí)候要謹(jǐn)慎第二不要對(duì)無意義的類型啟用序列化保持對(duì)接類的規(guī)模穩(wěn)定第三泛型類型需要注冊(cè)為對(duì)應(yīng)的具體類型否則框架會(huì)回退到性能很差的Java默認(rèn)實(shí)現(xiàn)。有人會(huì)問序列化器優(yōu)化的收益能有多大我做一個(gè)實(shí)際的項(xiàng)目時(shí)曾把一個(gè)每天處理上億條數(shù)據(jù)的統(tǒng)計(jì)任務(wù)遷移到高性能序列化結(jié)果Shuffle數(shù)據(jù)量從800GB降到了190GB任務(wù)整體時(shí)間縮短了將近30%。這個(gè)提升完全不需要?jiǎng)訕I(yè)務(wù)邏輯只是換了一下序列化方式。4.2 緩存級(jí)別不是越強(qiáng)越好關(guān)鍵看數(shù)據(jù)復(fù)用度很多框架都支持對(duì)中間計(jì)算結(jié)果做緩存把數(shù)據(jù)駐留在內(nèi)存中后續(xù)階段直接復(fù)用。緩存是雙刃劍用得好少計(jì)算一次甚至少讀寫一輪磁盤用不好白占內(nèi)存不說還會(huì)頻繁觸發(fā)GC把其他正常計(jì)算拖下水。我先說什么時(shí)候該緩存。最典型的是迭代式計(jì)算任務(wù)比如多次迭代直到收斂的算法中間結(jié)果在每一輪迭代里都會(huì)被反復(fù)讀取這種情況下緩存收益極大。另一種場(chǎng)景是同一份數(shù)據(jù)集被多個(gè)下游任務(wù)復(fù)用比如一份基礎(chǔ)統(tǒng)計(jì)數(shù)據(jù)被三個(gè)報(bào)表任務(wù)共同使用那這份數(shù)據(jù)緩存一次三個(gè)任務(wù)都能受益。什么時(shí)候不該緩存數(shù)據(jù)只被計(jì)算一次、后續(xù)再也不會(huì)被使用的場(chǎng)景緩存純屬浪費(fèi)。很多新手犯的毛病是不分青紅皂白把所有中間結(jié)果都標(biāo)記成緩存結(jié)果緩存堆積執(zhí)行單元內(nèi)存被塞滿框架頻繁把緩存溢寫到磁盤任務(wù)到最后不比原來快反而多了大量磁盤IO和GC停頓。緩存級(jí)別的選擇也很有講究。純內(nèi)存緩存訪問最快但有一定丟失風(fēng)險(xiǎn)執(zhí)行單元崩潰后緩存數(shù)據(jù)需要重新計(jì)算內(nèi)存加磁盤備用的方式穩(wěn)妥一些但讀取時(shí)可能涉及到磁盤IO。我對(duì)緩存級(jí)別的建議是默認(rèn)優(yōu)先純內(nèi)存追求穩(wěn)定性再考慮磁盤備份版本如果有多個(gè)執(zhí)行單元要共享大體積緩存直接不緩存或者讓它按需重復(fù)計(jì)算因?yàn)榇篌w積緩存會(huì)占滿執(zhí)行單元大部分內(nèi)存空間拖垮整體并發(fā)能力。4.3 算子鏈與執(zhí)行計(jì)劃裁剪盡量少制造寬依賴分布式計(jì)算框架在執(zhí)行任務(wù)前會(huì)把用戶寫的一系列操作解析成一個(gè)計(jì)算圖。這個(gè)計(jì)算圖里有一條非常關(guān)鍵的判斷標(biāo)準(zhǔn)相鄰算子之間是窄依賴還是寬依賴。窄依賴指的是每個(gè)父分區(qū)只對(duì)應(yīng)一個(gè)子分區(qū)這種依賴可以在同一個(gè)執(zhí)行單元內(nèi)部直接串聯(lián)不需要移動(dòng)數(shù)據(jù)。寬依賴指的是一個(gè)父分區(qū)被多個(gè)子分區(qū)引用或者子分區(qū)需要從多個(gè)父分區(qū)拉數(shù)據(jù)這種依賴會(huì)引入Shuffle也就是重新分區(qū)和網(wǎng)絡(luò)傳輸。寬依賴的代價(jià)比窄依賴高出幾個(gè)數(shù)量級(jí)。所以優(yōu)化計(jì)算圖的核心思路就是盡可能減少寬依賴同時(shí)讓窄依賴算子盡量合并執(zhí)行。舉一個(gè)非常典型的例子。統(tǒng)計(jì)某張用戶表里最近七天內(nèi)活躍用戶的總量有人會(huì)把所有需要的數(shù)據(jù)全部讀出來然后做一次全量過濾再進(jìn)行嚴(yán)格關(guān)聯(lián)操作最后聚合。但更合理的順序是先做過濾再做關(guān)聯(lián)最后聚合。過濾這一步能極大減少后面節(jié)點(diǎn)的數(shù)據(jù)量。雖然這個(gè)順序看起來就是代碼里幾個(gè)操作的前后挪動(dòng)但實(shí)際執(zhí)行計(jì)劃的差別非常大。過濾提前Shuffle的數(shù)據(jù)量可能減少一個(gè)數(shù)量級(jí)任務(wù)耗時(shí)也會(huì)成倍下降。另一個(gè)常見優(yōu)化是把小數(shù)據(jù)集的多種處理合并到一起減少中間結(jié)果的落盤次數(shù)。比如對(duì)同一個(gè)維度表做多次轉(zhuǎn)換這些操作如果在同一個(gè)執(zhí)行階段內(nèi)完成就能避免多輪讀寫。暗含的一個(gè)原則是對(duì)數(shù)據(jù)流的每一步操作都要問一句“這步操作是否讓數(shù)據(jù)量變大了還是變小了”。盡量讓數(shù)據(jù)量大的計(jì)算往后排讓過濾、裁剪、字段精簡(jiǎn)這些縮小數(shù)據(jù)量的操作盡可能前置。5. 用監(jiān)控和復(fù)盤代替直覺一次長尾任務(wù)的完整定位記錄5.1 必看的監(jiān)控指標(biāo)清單調(diào)優(yōu)如果沒有數(shù)據(jù)支撐就和閉著眼睛開車差不多。做分布式計(jì)算框架優(yōu)化至少要看這些指標(biāo)我把它們整理成了我日常工作里必備的檢查清單指標(biāo)維度具體指標(biāo)正常表現(xiàn)異常信號(hào)作業(yè)總體總運(yùn)行時(shí)長、等待調(diào)度時(shí)長等待時(shí)長占比不超過總時(shí)長10%長時(shí)間處于等待資源狀態(tài)階段耗時(shí)每個(gè)階段的耗時(shí)分布各任務(wù)耗時(shí)差距在3倍以內(nèi)長尾明顯個(gè)別任務(wù)遠(yuǎn)超中位數(shù)計(jì)算資源執(zhí)行單元數(shù)量、CPU使用率CPU使用率整體平穩(wěn)大量執(zhí)行單元空閑或長時(shí)間滿載內(nèi)存GCGC次數(shù)、GC時(shí)長、堆內(nèi)存占用GC時(shí)間占比低于5%GC時(shí)間占比高老年代頻繁回收ShuffleShuffle寫數(shù)據(jù)量、讀數(shù)據(jù)量、文件數(shù)數(shù)據(jù)量符合預(yù)期且波動(dòng)不大某個(gè)階段讀寫量異常高小文件數(shù)量激增數(shù)據(jù)分布輸入數(shù)據(jù)量、分區(qū)大小分布各分區(qū)數(shù)據(jù)量大致均勻少量分區(qū)數(shù)據(jù)量遠(yuǎn)超均值這套指標(biāo)配合框架自帶的執(zhí)行歷史頁面來看效率非常高。執(zhí)行歷史頁面能看到每個(gè)階段的耗時(shí)、每條任務(wù)讀取的數(shù)據(jù)量、GC時(shí)間、Shuffle大小等信息基本覆蓋了上表的大部分指標(biāo)。我拿到一個(gè)慢任務(wù)第一步就是打開頁面截圖然后按這張表的順序逐項(xiàng)排查。5.2 一次長尾調(diào)優(yōu)的完整排查鏈路有一次我負(fù)責(zé)的一套離線統(tǒng)計(jì)作業(yè)里有一條報(bào)表任務(wù)最近跑得越來越慢從之前的大概18分鐘漲到了43分鐘。我打開執(zhí)行歷史頁面第一眼看到的是階段列表里某個(gè)關(guān)聯(lián)階段占了總耗時(shí)的近八成。再看這個(gè)階段的詳細(xì)列表一共101個(gè)并行任務(wù)其中99個(gè)任務(wù)都在兩分鐘內(nèi)跑完了剩下兩個(gè)任務(wù)一個(gè)跑了29分鐘一個(gè)跑了32分鐘。這個(gè)長尾形態(tài)非常典型不是整個(gè)階段都慢而是極少數(shù)任務(wù)拖住了整個(gè)階段。我隨后查看了這兩個(gè)任務(wù)各自處理的數(shù)據(jù)量其中一個(gè)任務(wù)讀取的記錄數(shù)接近全階段均值的35倍。到這里數(shù)據(jù)傾斜的判定基本成立。至于傾斜的鍵是哪一種我看了一眼關(guān)聯(lián)條件這個(gè)階段是按訂單歸屬機(jī)構(gòu)的編號(hào)做的關(guān)聯(lián)報(bào)表里某個(gè)大機(jī)構(gòu)的數(shù)據(jù)量明顯遠(yuǎn)高于其他小機(jī)構(gòu)。修復(fù)方案很簡(jiǎn)單兩步走。第一步針對(duì)這個(gè)大機(jī)構(gòu)對(duì)應(yīng)的訂單數(shù)據(jù)和訂單明細(xì)之間不加鹽直接關(guān)聯(lián)第二步把第二步之后的維表數(shù)據(jù)量很小適合做廣播。同時(shí)把關(guān)聯(lián)階段原本的1000個(gè)分區(qū)降到200個(gè)減少小文件數(shù)量。改動(dòng)之后重新跑任務(wù)總耗時(shí)從43分鐘降到了15分鐘關(guān)聯(lián)階段從30多分鐘縮短到不足6分鐘。在這個(gè)例子里我沒有改任何業(yè)務(wù)邏輯沒有增加一臺(tái)機(jī)器全靠定位和選區(qū)得當(dāng)。這件事給我一個(gè)很深的印象分布式框架優(yōu)化里很多問題本質(zhì)是數(shù)據(jù)分布問題不是資源問題。5.3 三個(gè)常見的負(fù)優(yōu)化案例負(fù)優(yōu)化就是我說的“越調(diào)越慢”相信不少人經(jīng)歷過。舉三個(gè)我實(shí)際遇到過的典型案例。第一個(gè)是把并行度盲目調(diào)大。有人覺得任務(wù)慢一定是并行度不夠于是一步到位調(diào)成原來的三倍。結(jié)果任務(wù)被切成了幾萬個(gè)微小的分區(qū)每個(gè)分區(qū)的數(shù)據(jù)量只有幾百條調(diào)度開銷和序列化開銷反而成了大頭最終任務(wù)時(shí)間翻倍。負(fù)優(yōu)化的原因很簡(jiǎn)單并行度不是資源只是一個(gè)切分維度維度太細(xì)了反而增加管理成本。第二個(gè)是緩存所有中間結(jié)果。有人給計(jì)算鏈路里每一個(gè)Dataset都加了緩存標(biāo)記結(jié)果是執(zhí)行單元內(nèi)存被巨量緩存占滿GC連續(xù)不斷任務(wù)還沒跑完一半就頻繁觸發(fā)老年代回收整個(gè)過程比不緩存還慢。負(fù)優(yōu)化的原因是把緩存當(dāng)成免費(fèi)午餐沒有考慮緩存本身也是有成本和生命周期的。第三個(gè)是統(tǒng)一開啟壓縮和高壓縮比。壓縮確實(shí)能減少數(shù)據(jù)傳輸量但如果集群CPU已經(jīng)吃緊開高壓縮比只會(huì)讓計(jì)算速度更慢。壓縮、序列化、緩存這些配置項(xiàng)都存在明顯的資源置換關(guān)系最忌諱的就是“一把梭”地全局開啟。6. 工程化收尾把優(yōu)化變成可回歸、可驗(yàn)證的過程6.1 建立基準(zhǔn)任務(wù)集一次只改一個(gè)變量我做分布式計(jì)算框架優(yōu)化到了一定階段后最大的體會(huì)是優(yōu)化本身并不難難的是讓優(yōu)化結(jié)果可復(fù)現(xiàn)、可對(duì)比、可回歸。建議你在自己的環(huán)境里固化一組“基準(zhǔn)任務(wù)集”。這組任務(wù)要能代表線上大部分作業(yè)的特征有計(jì)算密集型的有Shuffle密集型的有數(shù)據(jù)傾斜傾向的。每次調(diào)整配置之前先用這組基準(zhǔn)任務(wù)跑一遍記錄每個(gè)任務(wù)的耗時(shí)、資源消耗、Shuffle數(shù)據(jù)量。改完配置之后再跑一遍直接對(duì)比變化。這里有一個(gè)非常重要的實(shí)驗(yàn)紀(jì)律一次只改一個(gè)變量。并行度、序列化器、緩存級(jí)別、分區(qū)數(shù)這些參數(shù)之間常常有相互影響如果同時(shí)改兩個(gè)以上出問題了都不知道是誰的問題調(diào)優(yōu)變成了瞎猜。先改一個(gè)記錄結(jié)果再改下一個(gè)調(diào)優(yōu)才是可控的。另外要強(qiáng)調(diào)一點(diǎn)參數(shù)調(diào)優(yōu)的結(jié)果并不具備普適性。同一個(gè)并行度設(shè)置在一個(gè)集群上很好用在另一個(gè)集群上可能就很差。因?yàn)閳?zhí)行單元的核心數(shù)、內(nèi)存大小、任務(wù)特征都不一樣。所以不要照抄別人的配置一定要基于自己的基準(zhǔn)任務(wù)集得出自己的參數(shù)組合。6.2 把“數(shù)據(jù)健康”納入日常監(jiān)控最后再說一個(gè)容易被忽視的工程化習(xí)慣不僅監(jiān)控任務(wù)的運(yùn)行狀態(tài)還要監(jiān)控輸入數(shù)據(jù)本身的健康度。數(shù)據(jù)健康度主要看三個(gè)維度分區(qū)大小是否均勻、文件數(shù)量是否合理、字段取值分布是否發(fā)生了劇烈變化。很多傾斜問題的根因都是上游數(shù)據(jù)分布發(fā)生了變化。比如某個(gè)關(guān)聯(lián)鍵的取值從過去占比5%突然漲到30%你昨天還跑得好好的任務(wù)今天就慢如蝸牛。這種變化靠參數(shù)調(diào)整是沒法根治的必須回溯到數(shù)據(jù)鏈路里找到數(shù)據(jù)分布突變的原因。所以我現(xiàn)在會(huì)為關(guān)鍵任務(wù)配套一個(gè)數(shù)據(jù)分布檢查在任務(wù)啟動(dòng)前先對(duì)最耗時(shí)的關(guān)聯(lián)鍵做一次取樣統(tǒng)計(jì)。如果發(fā)現(xiàn)取值分布有明顯長尾就提前觸發(fā)傾斜優(yōu)化邏輯。這個(gè)做法看起來多花了一點(diǎn)時(shí)間但能避免大量任務(wù)跑到一半才發(fā)現(xiàn)傾斜、然后再白跑一遍的情況。做分布式計(jì)算框架優(yōu)化這么多年我最大的體會(huì)是通用配置永遠(yuǎn)只存在于文檔里不存在于你的集群里。每次手癢想調(diào)參之前先回答三個(gè)問題慢在哪一層數(shù)據(jù)分布怎么樣這次改動(dòng)能否被下一次任務(wù)驗(yàn)證這三句話幫我少走了很多彎路也分享給你。