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