ODPS JOB 長尾問題調優
引言
上篇JOB logview 查看問題
提到長尾問題,本文深入探討下 長尾調優的方法
概述
因為數據分布不均,導致各個節點的工作量不同,整個任務就需要等最慢的節點完成才能完成。這種問題就是長尾問題,是分布式計算裏最常見的問題之一,也是典型的疑難雜症。
處理這類問題的思路就是把工作分給多個Worker去執行,而不是一個Worker單獨抗下最重的那份工作。本文分享平時工作中遇到的一些典型的長尾問題的場景及其解決方案。
分類
Join長尾
Join時出現某個Key裏的數據特別多的情況會出現Join長尾,是因為
解法:
-
排除兩張表都是小表的情況,若兩張表裏有一張大一張小,可以考慮使用Mapjoin,對小表進行緩存。具體語法和說明見下段文字詳細解釋。如果是MapReduce作業,可以使用資源表的功能,對小表進行緩存。
-
但是如果兩張表都比較大,就需要先盡量去重。
-
若還是不能解決,就需要從業務上考慮,為什麼會有這樣的兩個大數據量的Key要做笛卡爾積,直接考慮從業務上進行優化。
MAPJOIN HINT
當一個大表和一個或多個小表做join時,可以使用mapjoin,性能比普通的join要快很多。mapjoin的基本原理是:在小數據量情況下,SQL會將用戶指定的小表全部加載到執行join操作的程序的內存中,從而加快join的執行速度。需要注意,使用mapjoin時:
- left outer join的左表必須是大表;
- right outer join的右表必須是大表;
- inner join左表或右表均可以作為大表;
- full outer join不能使用mapjoin;
- mapjoin支持小表為子查詢;
- 使用mapjoin時需要引用小表或是子查詢時,需要引用別名;
- 在mapjoin中,可以使用不等值連接或者使用or連接多個條件;
- 目前MaxCompute 在mapjoin中最多支持指定6張小表,否則報語法錯誤;
- 如果使用mapjoin,則所有小表占用的內存總和不得超過512MB。請注意由於MaxCompute 是壓縮存儲,因此小表在被加載到內存後,數據大小會急劇膨脹。此處的512MB限製是加載到內存後的空間大小;
- 多個表join時,最左邊的兩個表不能同時是mapjoin的表。
下麵是一個簡單的示例:
select /* + mapjoin(a) */
a.shop_name,
b.customer_id,
b.total_price
from shop a join sale_detail b
on a.shop_name = b.shop_name;
MaxCompute SQL不支持支持在普通join的on條件中使用不等值表達式、or 邏輯等複雜的join條件,但是在mapjoin中可以進行如上操作,例如:
select /*+ mapjoin(a) */
a.total_price,
b.total_price
from shop a join sale_detail b
on a.total_price < b.total_price or a.total_price + b.total_price < 500;
Group By長尾
Group By Key 出現長尾的原因是因為某個Key內的計算量特別大。
解法 一:可對SQL進行改寫,添加隨機數,把長Key進行拆分。如SQL:
Select Key,Count(*) As Cnt From TableName Group By Key;
不考慮Combiner,M節點會Shuffle到R上,然後R再做Count操作。對應的執行計劃是M->R
但是如果對長尾的Key再做一次工作再分配,就變成:
-- 假設長尾的Key已經找到是KEY001
SELECT a.Key
, SUM(a.Cnt) AS Cnt
FROM (
SELECT Key
, COUNT(*) AS Cnt
FROM TableName
GROUP BY Key,
CASE
WHEN Key = 'KEY001' THEN Hash(Random()) % 50
ELSE 0
END
) a
GROUP BY a.Key;
可以看到,這次的執行計劃變成了M->R->R。雖然執行的步驟變長了,但是長尾的Key經過了2個步驟的處理,整體的時間消耗可能反而有所減少。注意,若數據的長尾並不嚴重,用這種方法人為地增加一次R的過程,最終的時間消耗可能反而更大。
解法二:使用通用的優化策略——係統參數,設置
set odps.sql.groupby.skewindata=true。
但是通用性的優化策略無法針對具體的業務進行分析,得出的結果不總是最優的。開發人員可以根據實際的數據情況,用更加高效的方法來改寫SQL。
Distinct長尾
可以看到,對於Distinct,上述Group By長尾“把長Key進行拆分”的策略已經不生效了。對這種場景,我們可以考慮其他方式解決。
解法:
--原始SQL,不考慮Uid為空
SELECT COUNT(uid) AS Pv
, COUNT(DISTINCT uid) AS Uv
FROM UserLog;
可以改寫成
SELECT SUM(PV) AS Pv
, COUNT(*) AS UV
FROM (
SELECT COUNT(*) AS Pv
, uid
FROM UserLog
GROUP BY uid
) a;
該解法是把Distinct改成了普通的Count,這樣的計算壓力不會落到同一個Reducer上。而且這樣改寫後,既能支持前麵提到的Group By優化,係統又能做Combiner,性能會有較大的提升。
動態分區長尾
動態分區功能為了整理小文件,會在最後起一個Reduce,對數據進行整理,所以如果使用動態分區寫入數據時若有傾斜,就會發生長尾。另外一般情況下濫用動態分區的功能也是產生這類長尾的一個常見原因。
解法:若寫入的數據已經確定需要把數據寫入某個具體分區,那可以在Insert的時候指定需要寫入的分區,而不是使用動態分區。
解決長尾問題的方法
通過Combiner解決長尾
對於MapRedcuce作業,使用Combiner是一種常見的長尾優化策略。在WordCount的例子裏,就已經有提到這種做法。通過Combiner,減少Maper Shuffle往Reducer的數據,可以大大減少網絡傳輸的開銷。對於MaxCompute SQL,這種優化會由係統自動完成。
需要注意的是,Combiner隻是Map端的優化,需要保證是否執行Combiner的結果是一樣的。以WordCount為例,傳2個(KEY,1)和傳1個(KEY,2)的結果是一樣的。但是比如在做平均值的時候,就不能在Combiner裏就把(KEY,1)和(KEY,2)合並成(KEY,1.5)。
通過係統優化解決長尾
針對長尾這種場景,除了前麵提到的Local Combiner,MaxCompute係統本身還做了一些優化。比如在跑任務的時候,日誌裏突然打出這樣的內容(+N backups部分):
M1_Stg1_job0:0/521/521[100%] M2_Stg1_job0:0/1/1[100%] J9_1_2_Stg5_job0:0/523/523[100%] J3_1_2_Stg1_job0:0/523/523[100%]R6_3_9_Stg2_job0:1/1046/1047[100%]
M1_Stg1_job0:0/521/521[100%] M2_Stg1_job0:0/1/1[100%] J9_1_2_Stg5_job0:0/523/523[100%] J3_1_2_Stg1_job0:0/523/523[100%]R6_3_9_Stg2_job0:1/1046/1047[100%]
M1_Stg1_job0:0/521/521[100%] M2_Stg1_job0:0/1/1[100%] J9_1_2_Stg5_job0:0/523/523[100%] J3_1_2_Stg1_job0:0/523/523[100%] R6_3_9_Stg2_job0:1/1046/1047(+1backups)[100%]
M1_Stg1_job0:0/521/521[100%] M2_Stg1_job0:0/1/1[100%] J9_1_2_Stg5_job0:0/523/523[100%] J3_1_2_Stg1_job0:0/523/523[100%] R6_3_9_Stg2_job0:1/1046/1047(+1backups)[100%]
可以看到1047個Reducer,有1046個已經完成了,但是最後一個一直沒完成。係統識別出這種情況後,自動啟動了一個新的Reducer,跑一樣的數據,然後看兩個哪個快,取快的數據歸並到最後的結果集裏。
通過業務優化解決長尾
雖然前麵的優化策略有很多,但是實際上還是有限。有時候碰到長尾問題,還需要從業務角度上想想是否有更好的解決方法,比如:
- 實際數據可能包含非常多的噪音。如,需要根據訪問者的ID進行計算,看每個用戶的訪問記錄的行為。需要先去掉爬蟲的數據(現在的爬蟲已越來越難識別),否則爬蟲數據很容易長尾計算的長尾。類似的情況還有根據xxid進行關聯的時候,需要考慮這個關聯字段是否存在為空的情況。
- 一些業務特殊情況,如,ISV的操作記錄,在數據量、行為方式上都會和普通的個人會有很大的區別。那麼可以考慮針對大客戶,使用特殊的分析方式進行單獨處理。
- 數據分布不均勻的情況下,不要使用常量字段做Distribute by字段來實現全排序。
最後更新:2017-04-01 17:00:39