前言
資源是影響 Spark 應用執行效率的一個重要因素。Spark 應用中真正執行 task 的元件是 Executor,可以通過spark.executor.instances 指定 Spark 應用的 Executor 的數量。在執行過程中,無論 Executor上是否有 task 在執行,都會被一直佔有直到此 Spark 應用結束。
上篇我們從動態優化的角度講述了 Spark 3.0 版本中的自適應查詢特性,它主要是在一條 SQL 執行過程中不斷優化執行邏輯,選擇更好的執行策略,從而達到提升效能的目的。本篇我們將從整個 Spark 叢集資源的角度討論一個常見痛點:資源不足。
在 Spark 叢集中的一個常見場景是,隨著業務的不斷髮展,需要執行的 Spark 應用數和資料量越來越大,靠資源堆砌的優化方式也越來越顯得捉襟見肘。當一個長期執行的 Spark 應用,若分配給它多個 Executor,可是卻沒有任何 task 分配到這些 Executor 上,而此時有其他的 Spark 應用卻資源緊張,這就造成了資源浪費和排程不合理。
要是每個 Spark 應用的 Executor 數也能動態調整那就太好了。
動態資源分配(Dynamic Resource Allocation)就是為了解決這種場景而產生。Spark 2.4 版本中 on Kubernetes 的動態資源並不完善,在 Spark 3.0 版本完善了 Spark on Kubernetes 的功能,其中就包括更靈敏的動態分配。我們 Erda 的 FDP 平臺(Fast Data Platform)從 Spark 2.4 升級到 Spark 3.0,也嘗試了動態資源分配的相關優化。本文將針對介紹 Spark 3.0 中 Spark on Kubernetes 的動態資源使用。
原理
一個 Spark 應用中如果有些 Stage 稍微資料傾斜,那就有大量的 Executor 是空閒狀態,造成叢集資源的極大浪費。通過動態資源分配策略,已經空閒的 Executor 如果超過了一定時間,就會被叢集回收,並在之後的 Stage 需要時可再次請求 Executor。
如下圖所示,固定 Executor 個數情況,Job1 End 和 Job2 Start 之間,Executor 處於空閒狀態,此時就造成叢集資源的浪費。
開啟動態資源分配後,在 Job1 結束後,Executor1 空閒一段時間便被回收;在 Job2 需要資源時再申Executor2,實現叢集資源的動態管理。
動態分配的原理很容易理解:“按需使用”。當然,一些細節還是需要考慮到:
- 何時新增/移除 Executor
- Executor 數量的動態調整範圍
- Executor 的增減頻率
- Spark on Kubernetes 場景下,Executor 的 Pod 銷燬後,它儲存的中間計算資料如何訪問
這些注意點在下面的引數列表中都有相應的說明。
引數一覽
spark.dynamicAllocation.enabled=true #總開關,是否開啟動態資源配置,根據工作負載來衡量是否應該增加或減少executor,預設false
spark.dynamicAllocation.shuffleTracking.enabled=true #spark3新增,之前沒有官方支援的on k8s的Dynamic Resouce Allocation。啟用shuffle檔案跟蹤,此配置不會回收儲存了shuffle資料的executor
spark.dynamicAllocation.shuffleTracking.timeout #啟用shuffleTracking時控制儲存shuffle資料的executor超時時間,預設使用GC垃圾回收控制釋放。如果有時候GC不及時,配置此引數後,即使executor上存在shuffle資料,也會被回收。暫未配置
spark.dynamicAllocation.minExecutors=1 #動態分配最小executor個數,在啟動時就申請好的,預設0
spark.dynamicAllocation.maxExecutors=10 #動態分配最大executor個數,預設infinity
spark.dynamicAllocation.initialExecutors=2 #動態分配初始executor個數預設值=spark.dynamicAllocation.minExecutors
spark.dynamicAllocation.executorIdleTimeout=60s #當某個executor空閒超過這個設定值,就會被kill,預設60s
spark.dynamicAllocation.cachedExecutorIdleTimeout=240s #當某個快取資料的executor空閒時間超過這個設定值,就會被kill,預設infinity
spark.dynamicAllocation.schedulerBacklogTimeout=3s #任務佇列非空,資源不夠,申請executor的時間間隔,預設1s(第一次申請)
spark.dynamicAllocation.sustainedSchedulerBacklogTimeout #同schedulerBacklogTimeout,是申請了新executor之後繼續申請的間隔,預設=schedulerBacklogTimeout(第二次及之後)
spark.specution=true #開啟推測執行,對長尾task,會在其他executor上啟動相同task,先執行結束的作為結果
實戰演示
無圖無真相,下面我們將動態資源分配進行簡單演示。
1.配置引數
動態資源分配相關引數配置如下圖所示:
如下圖所示,Spark 應用啟動時的 Executor 個數為 2。因為配置了
spark.dynamicAllocation.initialExecutors=2
執行一段時間後效果如下,executorNum 會遞增,因為空閒的 Executor 被不斷回收,新的 Executor 不斷申請。
2. 驗證快慢 SQL 執行
使用 SparkThrfitServer 會遇到的問題是一個資料量很大的 SQL 把所有的資源全佔了,導致後面的 SQL 都等待,即使後面的 SQL 只需要幾秒就能完成。我們開啟動態分配策略,再來看 SQL 執行順序。
先提交慢 SQL:
再提交快 SQL:
如下圖所示,開啟動態資源分配後,因為 SparkThrfitServer 可以申請新的 Executor,後面的 SQL 無需等待便可執行。Job7(慢 SQL)還在執行中,後提交的 Job8(快 SQL)已完成。這在一定程度上緩解了資源分配不合理的情況。
3. 詳情檢視
我們在 SparkWebUI 上可以看到動態分配的整個流程。
登陸 SparkWebUI 頁面,Jobs -> Event Timeline,可以看到 Driver 對整個應用的 Executor 排程。如下圖所示,顯示了每個 Executor 的建立和回收。
同時也能看到此 Executor 的具體建立和回收時間。
在 Executors 標籤頁,我們可以看到所有歷史 Executor 的當前狀態。如下圖所示,之前的 Executor 都已被回收,只有 Executor-31 狀態為 Active。
總結
動態資源分配策略在空閒時釋放 Executor,繁忙時申請 Executor,雖然邏輯比較簡單,但是和任務排程密切相關。它可以防止小資料申請大資源,Executor 空轉的情況。在叢集資源緊張,有多個 Spark 應用的場景下,可以開啟動態分配達到資源按需使用的效果。
以上是我們在 Spark 相關優化的一點經驗,希望能夠對大家有所幫助?。
注:文中部分圖片源自於網路,侵刪。
更多技術乾貨請關注【爾達 Erda】公眾號,與眾多開源愛好者共同成長~