Spark 資源自動清理

Spark運行一次SQL,根據SQL的具體執行情況,可能會產生很多垃圾。譬如你可以很容易觀察到的就是在Spark UI上跑完SQL後會有Storage Memory的佔用:

這個應該是SQL中有join,並且使用的hash join時產生broadcast引起的。除了Broadcast,Spark還會產生如RDD,Shuffle臨時數據,累加器數據,Checkpoint數據。

因爲Spark是一個分佈式框架,這些數據清理可能會涉及到多個節點的複雜的IO,鐵定是不能同步執行的,否則清理的時間可能比執行任務的時間還長。爲此Spark提供了一個異步清理的機制。也就是`org.apache.spark.ContextCleaner`.  異步清理會有個問題,就是你怎麼知道一個對象是不是可以清理了?比如一個任務產生了一個Shuffle,這個Shuffle在很多節點產生了很多數據,這個Shuffle在Driver端會有一個對應的對象,如果這個對象生命週期到頭了,那麼就說明我們可以去清理這個對象所持有的數據了。

所以,當產生這個對象的時候,我們就需要把這個對象註冊起來,其次,當這個對象的生命週期結束了,我們就需要發送一個通知給異步的清理線程,告訴他你可以清理了。所以通常你要維護一個隊列,同時你還要手動的在每個資源需要釋放的地方將對象設置爲Null,並且發送消息到隊列裏。

每次都這樣做實在是太麻煩,而且很容易疏漏,導致資源泄露。這裏不說內存泄露是因爲對象所關聯的資源可能是文件也可能是內存,並且是分佈在多個節點上的。Spark團隊就做了個取巧的設計,註冊肯定是要註冊的,我new對象後就順帶註冊下,清理的話,我通過ReferenceQueue 來獲取被GC掉的對象的通知,然後觸發真實的清理動作。假設我們要清理的是RDD A, 那麼我會提供A的一個WeakRef, 比如叫ARef, A被回收,但是ARef不會被回收,通過ARef我們還能拿到一些信息,比如rddId,然後通過這個rddId去做真正的清理。

但是我們知道,一個對象什麼時候被GC我們是不可控的,比如對於內存充足的Driver,可能半天都不會發生GC,這個時候會導致spark沒有辦法及時做垃圾清理了,如果你的任務還比較多,很可能不是磁盤被跑慢(大量的shuffle數據)就是Storage memory炸了。所以作爲對這個方案副作用的補救,Spark團隊設置了一個30分鐘的定時任務,通過調用 `System.gc()`手動觸發GC。然後你就會發現Spark Driver會定時有一個Full GC發生。顯然,這個Full GC對於流程序或者AdHoc查詢服務都是難以忍受的。

那有解決辦法麼?如果你使用G1,那麼還是有的。G1 GC的System.gc()默認觸發的是full GC,也就是serial old GC。我們只要修改下這個默認行爲就可以,通過加上GC參數 ` -XX:+ExplicitGCInvokesConcurrent`,G1就會用自身的併發GC來執行System.gc()了。此時他只是強行進行一次global concurrent marking(裏面有個initial marking會暫停應用程序,但是很快)。另外就是正常的global concurrent marking 中的initial marking都是在都是搭的young GC的便車(也就是young GC做了,然後global concurrent marking 直接拿到現成的結果,所以System.gc()+ -XX:+ExplicitGCInvokesConcurrent 是特例)。然後按照正常的GC節奏走。這種情況下就和G1的young GC無異了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章