基於spark之上的即席分析-spark內存泄漏及源碼調優

spark 內存泄露

1.    高併發狀況下的內存泄露的具體表現

很遺憾, spark 的設計架構並非爲了高併發請求而設計的,咱們嘗試在網絡條件很差的集羣下,進行 100 併發的查詢,在壓測 3 天后發現了內存泄露。node

a) 在進行大量小 SQL 的壓測過程當中發現,有大量的 activejob 在 spark ui 上一直處於 pending 狀態,且永遠不結束,以下圖所示sql

b) 而且發現 driver 內存爆滿apache

c) 用內存分析分析工具分析了下網絡

2. 高併發下 AsynchronousListenerBus 引發的 WEB UI 的內存泄露

短期內 SPARK 提交大量的 SQL ,並且 SQL裏面存在大量的 union與 join的情形,會建立大量的 event對象,使得這裏的 event 數量超過 10000 個 event ,一旦超過 10000 個 event 就開始丟棄 event,而這個 event 是用來回收 資源的,丟棄了 資源就沒法回session

收了。 針對 UI 頁面的這個問題,咱們將這個隊列長度的限制給取消了。多線程

3.    AsynchronousListenerBus 自己引發的內存泄露

抓包發現架構

這些 event 是經過 post 方法傳遞的,並寫入到隊列裏併發

可是也是由一個單線程進行 postToAll 的jvm

可是在高併發狀況下,單線程的 postToAll 的速度沒有 post 的速度快,會致使隊列堆積的高併發

event 愈來愈多,若是是持續性的高併發的 SQL 查詢,這裏就會致使內存泄露

 

接下來咱們在分析下 postToAll 的方法裏面,那個路徑是最慢的,致使事件處理最慢的邏輯是那個?

可能您都不敢相信,經過 jstack 抓取分析,程序大部分時間都阻塞在記錄日誌上

能夠經過禁用這個地方的 log 來提高 event 的速度

log4j.logger.org.apache.spark.scheduler=ERROR

4. 高併發下的 Cleaner 的內存泄露

說道這裏, Cleaner 的設計應該算是 spark 最糟糕的設計。 spark 的 ContextCleaner 是用於回收與清理已經完成了的 廣播 boradcast,shuffle 數據的。可是高併發下,咱們發現這個地方積累的數據會愈來愈多,最終致使 driver 內存跑滿而掛掉。

l  咱們先看下,是如何觸發內存回收的

沒錯,就是經過 System.gc() 回收的內存,若是咱們在 jvm 裏配置了禁止執行 System.gc,這個邏輯就等於廢掉(並且有不少 jvm 的優化參數通常都推薦配置禁止 system.gc 參數)

l  clean 過程

這是一個單線程的邏輯,並且每次清理都要協同不少機器一同清理,清理速度相對來講比較慢,可是SQL 併發很大的時候,產生速度超過了清理速度,整個 driver 就會發生內存泄露。並且 brocadcast 若是佔用內存太多,也會使用很是多的本地磁盤小文件,咱們在測試中發現,高持續性併發的狀況下本地磁盤用於存儲 blockmanager 的目錄佔據了咱們 60%的存儲空間。

咱們再來分析下 clean 裏面,那個邏輯最慢

真正的瓶頸在於 blockManagerMaster 裏面的 removeBroadcast,由於這部分邏輯是須要跨越多臺機器的。

 

針對這種問題,

咱們在 SQL 層加了一個 SQLWAITING 邏輯,判斷了堆積長度,若是堆積長度超過了咱們的設定值,咱們這裏將阻塞新的 SQL 的執行。堆積長度能夠經過更改 conf 目錄下的 ya100_env_default.sh 中的ydb.sql.waiting.queue.size 的值來設置。

l  建議集羣的帶寬要大一些,萬兆網絡確定會比千兆網絡的清理速度快不少。

l  給集羣休息的機會,不要一直持續性的高併發,讓集羣有間斷的機會。

l  增大 spark 的線程池,能夠調節 conf 下的 spark-defaults.conf 的以下值來改善。

5. 線程池與 threadlocal 引發的內存泄露

發現 spark, hive, lucene 都很是鍾愛使用 threadlocal 來管理臨時的 session 對象,期待 SQL 執行完畢後這些對象可以自動釋放,可是與此同時 spark 又使用了線程池,線程池裏的線程一直不結束,這些資源一直就不釋放,時間久了內存就堆積起來了。

針對這個問題,延雲修改了 spark 關鍵線程池的實現,更改成每 1 個小時,強制更換線程池爲新的線程池,舊的線程數可以自動釋放。

6. 文件泄露

您會發現,隨着請求的 session 變多, spark 會在 hdfs 和本地磁盤建立海量的磁盤目錄,最終會由於本地磁盤與 hdfs 上的目錄過多,而致使文件系統和整個文件系統癱瘓。在 YDB 裏面咱們針對這種狀況也作了處理。

7. deleteONExit 內存泄露

爲何會有這些對象在裏面,咱們看下源碼

8. JDO 內存泄露

多達 10 萬多個 JDOPersistenceManager

9. listerner 內存泄露

經過 debug 工具監控發現,spark 的 listerner 隨着時間的積累,通知(post)速度運來越慢

發現全部代碼都卡在了 onpostevent 上

jstack 的結果以下

研究下了調用邏輯以下,發現是循環調用 listerners,並且 listerner 都是空執行纔會產生上面的 jstack 截圖

經過內存發現有 30 多萬個 linterner 在裏面

發現都是大多數都是同一個listener,咱們覈對下該處源碼

最終定位問題

確係是這個地方的 BUG ,每次建立 JDBC 鏈接的時候 , spark 就會增長一個 listener, 時間久了, listener就會積累愈來愈多 針對這個問題 我簡單的修改了一行代碼,開始進入下一輪的壓測

spark 源碼調優

測試發現,即便只有 1 條記錄,使用 spark 進行一次 SQL 查詢也會耗時 1 秒,對不少即席查詢來講 1秒的等待,對用戶體驗很是不友好。針對這個問題,咱們在 spark 與 hive 的細節代碼上進行了局部調優,調優後,響應時間由原先的 1 秒縮減到如今的 200~300 毫秒。

如下是咱們改動過的地方

1. SessionState 的建立目錄 佔用較多的時間

另外使用 hadoop namenode HA 的同窗會注意到,若是第一個 namenode 是 standby 狀態,這個地方會更慢,就不止一秒,因此除了改動源碼外,若是使用 namenode ha 的同窗必定要注意,將 active 狀態的 node 必定要放在前面。

2. HiveConf 的初始化過程佔用太多時間

頻繁的 hiveConf 初始化,須要讀取 core-default.xml, hdfs-default.xml, yarn-default.xml, mapreduce-default.xml, hive-default.xml 等多個xml 文件,而這些xml 文件都是內嵌在 jar 包內的。

第一,解壓這些 jar 包須要耗費較多的時間,第二每次都對這些 xml 文件解析也耗費時間。

3. 廣播 broadcast 傳遞的 hadoop configuration 序列化很耗時

configuration 的序列化,採用了壓縮的方式進行序列化,有全局鎖的問題configuration 每次序列化,傳遞了太多了沒用的配置項了, 1000 多個配置項,佔用 60 多 Kb。咱們剔除了不是必須傳輸的配置項後,縮減到 44 個配置項, 2kb 的大小。

4. 對 spark 廣播數據 broadcast 的 Cleaner 的改進

因爲 SPARK-3015 的 BUG, spark 的 cleaner 目前爲單線程回收模式。

你們留意 spark 源碼註釋

其中的單線程瓶頸點在於廣播數據的 cleaner,因爲要跨越不少臺機器,須要經過 akka 進行網絡交互。

若是回收併發特別大, SPARK-3015 的 bug 報告會出現網絡擁堵,致使大量的 timeout 出現。

爲何回收量特變大呢? 實際上是由於 cleaner 本質是經過 system.gc() ,按期執行的,默認積累 30 分鐘或者進行了 gc 後才觸發 cleaner,這樣就會致使瞬間,大量的 akka 併發執行,集中釋放,網絡不瞬間癱瘓纔不怪呢。

可是單線程回收意味着回收速度恆定,若是查詢併發很大,回收速度跟不上 cleaner 的速度,會致使 cleaner 積累不少,會致使進程 OOM( YDB 作了修改,會限制前臺查詢的併發)。

不管是 OOM 仍是限制併發都不是咱們但願看到的,因此針對高併發狀況下,這種單線程的回收速度是知足不了高併發的需求的。

對於官方的這樣的作法,咱們表示並非一個完美的 cleaner 方案。併發回收必定要支持,

只要解決 akka 的 timeout 問題便可。

因此這個問題要仔細分析一下, akka 爲何會 timeout,是由於 cleaner 佔據了太多的資源,那麼咱們是否能夠控制下 cleaner 的併發呢?好比說使用 4 個併發,而不是默認將所有的併發線程都給佔滿呢?這樣及解決了 cleaner 的回收速度,也解決了 akka 的問題不是更好麼?

針對這個問題,咱們最終仍是選擇了修改 spark 的 ContextCleaner 對象,將廣播數據的回收改爲多線程的方式,但如今了線程的併發數量,從而解決了該問題。

相關文章
相關標籤/搜索