大家好,我是 V 哥。在實際的業務場景中,Spark任務出現OOM(Out of Memory) 問題通常是由於任務處理的資料量過大、資源分配不合理或者程式碼存在效能瓶頸等原因造成的。針對不同的業務場景和原因,可以從以下幾個方面進行最佳化和解決。
一、業務場景及可能的OOM原因分析
-
資料量過大:
- 業務場景:處理海量資料集(例如,數億行日誌資料或數十TB的資料集),任務執行過程中需要對資料進行大規模的聚合、排序、連線等操作。
- OOM 原因:資料無法完全放入記憶體,導致溢位,尤其是在
shuffle
或join
操作時,資料量暴增。
-
資料傾斜:
- 業務場景:處理的資料分佈不均勻(如某個使用者或產品的資料量過多),導致部分節點上出現計算或記憶體瓶頸。
- OOM 原因:由於部分節點需要處理大量的資料,某些節點的任務會使用超出可用記憶體的資源,而其他節點的負載較輕。
-
不合理的資源分配:
- 業務場景:資源分配過低,導致單個任務分配到的記憶體、CPU等資源不足。
- OOM 原因:Executor的記憶體設定太小,或者資料過度快取,導致記憶體不足。
-
程式碼中存在快取過多或記憶體使用不合理:
- 業務場景:頻繁使用
cache()
、persist()
,或對資料結構進行不必要的操作,導致記憶體過度消耗。 - OOM 原因:資料快取沒有及時釋放,導致記憶體佔用過多。
- 業務場景:頻繁使用
二、針對OOM問題的解決方案
1. 調整Executor的記憶體和CPU資源
透過合理的資源分配,確保每個Executor
有足夠的記憶體處理資料。
- 增加Executor的記憶體:
Spark 中的Executor
負責在叢集節點上執行任務,預設每個Executor
的記憶體可能不足以處理大資料集。可以增加Executor
的記憶體以緩解OOM問題。
--executor-memory 8G
可以透過--executor-memory
選項來設定每個Executor
的記憶體。例如,將記憶體設定為8GB。如果資料量很大,可以根據情況設定更大的記憶體。
- 調整堆外記憶體:
Spark還使用了一部分堆外記憶體(off-heap memory)。如果涉及大量的堆外記憶體操作,可以透過以下配置增加堆外記憶體:
--conf spark.memory.offHeap.enabled=true
--conf spark.memory.offHeap.size=4G
- 調整Executor的CPU核心數:
為每個Executor
分配更多的CPU核心,以加快任務的處理速度,防止長時間佔用記憶體。
--executor-cores 4
透過--executor-cores
設定每個Executor
使用的核心數。例如,可以將核心數設定為4,以提升併發計算能力。
2. 調整記憶體管理策略
Spark的記憶體管理策略主要涉及以下幾個關鍵引數,它們的最佳化配置可以幫助減少OOM問題。
- 調整記憶體管理比例:
Spark 2.x 及以上版本採用統一的記憶體管理模型,可以透過調節以下引數最佳化記憶體使用:
--conf spark.memory.fraction=0.8
--conf spark.memory.storageFraction=0.5
spark.memory.fraction
:該引數控制了儲存與執行記憶體的總佔比,預設是0.6,可以適當調高。spark.memory.storageFraction
:該引數決定了在memory.fraction
的基礎上,儲存記憶體的佔比。如果需要更多執行記憶體,可以適當減小該值。
- 減少快取資料的儲存佔用:
- 及時清理快取:對於不再需要的資料,及時呼叫
unpersist()
來清理快取,釋放記憶體。
- 及時清理快取:對於不再需要的資料,及時呼叫
rdd.unpersist()
- 調整快取級別:在快取時,使用
StorageLevel.DISK_ONLY
或StorageLevel.MEMORY_AND_DISK
,以減少記憶體佔用。
rdd.persist(StorageLevel.MEMORY_AND_DISK)
3. 資料切分與最佳化操作
Spark任務中的shuffle
、join
、groupBy
等操作通常會引起大量記憶體消耗,以下最佳化可以減輕這些操作帶來的OOM風險。
- 調整分割槽數:
- 對於大規模資料操作如
join
、shuffle
等,分割槽數的設定至關重要。如果分割槽數過少,可能會導致某些分割槽資料量過大,進而導致記憶體溢位。
- 對於大規模資料操作如
rdd.repartition(200)
或者在執行某些操作時,顯式指定分割槽數:
rdd.reduceByKey(_ + _, numPartitions = 200)
- 通常的經驗是將分割槽數量設定為比Executor數量高出數倍(例如,每個核心處理2-4個分割槽)。
- 避免過多的寬依賴:
寬依賴(如groupByKey
)會在shuffle時造成記憶體的壓力,特別是資料量較大時,應該儘量避免。可以透過替換為reduceByKey
等具有預聚合功能的操作來減少記憶體消耗:
rdd.reduceByKey(_ + _)
-
避免資料傾斜:
如果存在資料傾斜,部分節點處理大量資料,容易導致OOM。以下是常見的解決方法:- 隨機鍵拆分:可以為資料加上隨機字首,以打散資料,避免部分節點資料量過大。
rdd.map(x => ((x._1 + new Random().nextInt(10)), x._2))
- 廣播小表:在
join
操作中,如果一張表很小,可以使用廣播變數,將小表廣播到每個節點,減少資料傳輸和記憶體佔用:
val broadcastVar = sc.broadcast(smallTable)
largeTable.mapPartitions { partition =>
val small = broadcastVar.value
partition.map(largeRow => ...)
}
4. 調整Spark的並行度和Shuffle機制
Spark的shuffle操作(如groupByKey
、join
)會導致大量資料需要在不同的節點之間傳輸。如果並行度設定過低,容易導致某個節點處理的資料量過大,從而引發OOM。
- 增加並行度:
--conf spark.sql.shuffle.partitions=200
或者在程式碼中顯式設定:
spark.conf.set("spark.sql.shuffle.partitions", "200")
- 預設情況下,
spark.sql.shuffle.partitions
的值可能偏小(例如200),根據資料規模適當調整該值可以減輕單個節點的負載。
- 調整Shuffle合併機制:
Spark 3.0引入了 Adaptive Query Execution (AQE),可以在執行時動態調整shuffle的分割槽數,避免某些分割槽資料量過大:
--conf spark.sql.adaptive.enabled=true
--conf spark.sql.adaptive.shuffle.targetPostShuffleInputSize=64M
AQE 可以根據任務的執行情況自動調整shuffle的分割槽數,從而避免OOM。
五、小結一下
Spark任務中的OOM問題常常由於資料量過大、資料傾斜、資源分配不合理等問題引起,針對不同的業務場景,可以採取以下措施進行最佳化:
- 合理分配記憶體和CPU:增加Executor的記憶體和CPU核心數,合理配置記憶體管理引數。
- 調整分割槽數和最佳化操作:透過調整分割槽數、減少寬依賴等方式減少記憶體佔用。
- 處理資料傾斜:透過隨機鍵拆分、廣播小表等方法避免資料傾斜。
- 使用快取最佳化記憶體:減少不必要的
cache()
和persist()
操作,並及時釋放快取資料。
好了,今天的內容就寫到這裡,這些最佳化方法結合使用,可以有效解決Spark任務中的OOM問題。關注威哥愛程式設計,碼碼通暢不掉髮。