跳轉到主要內容
工程的博客

一個循序漸進的指南調試火花應用程序中的內存泄漏

2020年12月16日 工程的博客

分享這篇文章

這是一個客戶撰寫文章Shivansh斯利瓦斯塔瓦,軟件工程師,迪斯尼的流媒體服務。這是出版在Medium.com上

的上下文

我們在迪斯尼流媒體服務使用Apache火花在商業和火花結構化流發展我們的管道。這些應用程序上運行磚運行時(DBR)環境這是非常友好的。

我們的一個結構化流工作使用flatMapGroupsWithState在那裏積累狀態,根據我們的業務邏輯執行分組操作。這個工作繼續崩潰大約每3天。有時甚至更少,然後之後,整個應用程序重新啟動,因為提供的重試功能DBR環境。如果這是一個正常的批處理作業這是可以接受的但在我們的例子中,我們有一個結構化的流媒體工作和低延遲SLA來滿足。這是我們戰鬥的故事OutOfMemory異常(伯父)我們如何解決。

下麵是我們作為部門解決了所述方法的問題:

步驟1:檢查司機日誌。是什麼導致了這個問題呢?

如果出現問題導致工作的失敗,然後司機日誌(可直接上發現火花UI)將描述任務的最後一次重試失敗的原因。

如果任務失敗(如果spark.task四(4)倍。maxFailures = 4),然後最後失敗的原因將在司機日誌,報告詳細說明為什麼整個工作失敗了。

在我們的例子中,它表明,遺囑執行人死和電離了。因此,下一步是找到原因。

步驟2:檢查執行者日誌。他們為什麼失敗?

遺囑執行人日誌,一般可以通過ssh訪問,我們看到,這是失敗的伯父。

我們遇到兩種類型的伯父錯誤:

  1. . lang。OutOfMemoryError: GC開銷限製超過
  2. . lang。OutOfMemoryError: Java堆空間。

注意:JavaHeapSpace伯父會發生如果係統沒有足夠的內存的數據需要處理。在某些情況下,選擇一個更大的實例i3.4x大(16個vCPU 122直布羅陀海峽)來解決這個問題。

另一個可能的解決方案可能是優化參數,以確保消費可以處理的。這基本上意味著必須提供足夠的內存來處理在一個micro-batch要處理的數據量。

步驟3:檢查垃圾收集器的活動

我們看到從我們的日誌,垃圾收集器(GC)是帶著太多的時間,有時失敗錯誤GC開銷限製超過當時試圖執行完整的垃圾收集。

根據火花文檔,G1GC可以解決問題在某些情況下,垃圾收集是一個瓶頸。我們啟用G1GC使用以下配置:


              spark.executor。extraJavaOptions: - xx: + UseG1GC

值得慶幸的是,這個調整改善很多東西:

  1. GC周期速度提高。
  2. 完整GC還是太慢了我們喜歡的類型,但完整的GC周期變得不那麼頻繁。
  3. GC開銷限製超過異常消失了。

然而,我們仍然有Java堆空間伯父來解決錯誤。我們的下一步是看看我們的集群健康看看我們是否能得到任何線索。

步驟4:檢查集群的健康

磚集群提供支持神經節,一種可伸縮的分布式監控係統,用於高性能計算係統,如集群和網格。

我們的神經節圖看起來是這樣的:
從神經節集群內存截圖

從神經節集群內存截圖

從神經節Worker_Memory截圖

從神經節Worker_Memory截圖

圖表告訴我們,集群內存穩定一段時間,開始增長,繼續增長,然後邊上掉了下去。這是什麼意思?

  1. 這是一個有狀態的工作也許我們沒有清理狀態。
  2. 可能是發生了內存泄漏。

第五步:檢查你的流指標

看著我們的流指標帶我們的路徑消除罪犯創建集群內存問題。流指標,發出火花,提供每批加工信息。

它看起來像這樣:

注意:這些都不是我們真正的度量。這隻是一個例子。

注意:這些我們的真正的指標。它隻是一個例子。{“id”:“abe526d3 - 1127 - 4805 - 83 - e6 - 9 - c477240e36b”,:“runId d4fec928 - 4703 - 4 - d74 bb9d - 233 fb9d45208”,“名稱”:“display_query_114”,“時間戳”:“2020 - 04 - 23 t09:28:18.886z”,“batchId”: 36歲,“numInputRows”: 561682年,“inputRowsPerSecond”: 25167.219284882158,“processedRowsPerSecond”: 19806.12856588737," durationMs ": {“addBatch”: 26638年,“getBatch”: 173年,“getOffset”: 196年,“queryPlanning”: 400年,“triggerExecution”: 28359年,“walCommit”: 247}," eventTime ": {“avg”:“2020 - 04 - 23 t08:33:03.664z”,“馬克斯”:“2020 - 04 - 23 t08:34:58.911z”,“最小值”:“2020 - 04 - 23 t08:00:34.814z”,“水印”:“2020 - 04 - 23 - t08:33:42.664z”},“stateOperators”: [{“numRowsTotal”: 1079年,“numRowsUpdated”: 894年,“memoryUsedBytes”: 485575年," customMetrics ": {“loadedMapCacheHitCount”: 14400年,“loadedMapCacheMissCount”: 0,“stateOnCurrentVersionSizeBytes”: 284151}}

策劃stateOperators.numRowsTotal對事件的時間,隨著時間的推移我們注意到穩定。因此,消除了伯父發生的可能性,因為國家被保留。

結論:發生了內存泄漏,我們需要找到它。為此,我們使堆轉儲,看看是占用了太多的內存。

第六步:啟用HeapDumpOnOutOfMemory

伯父得到一個堆轉儲,以下選項中可以啟用火花執行人一邊集群配置:

spark.executor。extraJavaOptions: - xx: + HeapDumpOnOutOfMemoryError - xx: HeapDumpPath =/ dbfs /heapdump

此外,一條可以供堆轉儲得救。我們使用這個配置,因為我們可以從磚平台訪問它。Beplay体育安卓版本您還可以訪問這些文件通過向工人和ssh-ing使用等工具下載它們rsync

第七步:周期性的堆轉儲

采取定期堆轉儲允許多個堆轉儲分析與伯父堆轉儲。我們把堆轉儲每12小時從相同的遺囑執行人。一旦我們的遺囑執行人進入伯父,我們將至少有兩個可用的轉儲。在我們的例子中,執行者正在進入伯父至少24小時。

步驟采取定期堆轉儲:

  1. ssh到工人
  2. 獲得使用java進程的Pid
  3. 得到的堆轉儲jmap您:= pbs_worker.hprof = b,文件格式
  4. 提供正確的權限來堆轉儲文件。
    sudo chmod 444 pbs_worker.hprof
  5. 下載文件在您的本地
    / rsync -chavzP——統計數據
    (電子郵件保護) :/ home / ubuntu / pbs_worker。hprof。

第八步:分析堆轉儲

堆轉儲分析等工具可以執行YourKitEclipse墊。

在我們的例子中,堆轉儲的範圍很大,在40 gb或更多。堆轉儲的大小使它很難分析。有一個解決方案可用於索引大文件,然後對它們進行分析。

步驟9:發現內存泄露的地方通過觀察對象資源管理器

YourKit提供檢查hprof文件。如果問題是顯而易見的,它將檢驗部分所示。在我們的案例中,問題是不明顯的。

看著堆直方圖時,我們看到許多HashMapNode實例,但是根據我們的業務邏輯,並沒有認為過有關的信息。

從火花UI HeapHistogram截圖

從火花UI HeapHistogram截圖

當我們看著YourKit中的類和包部分,我們發現相同的結果;正如我們的預期。

火花從YourKit堆轉儲分析截圖

堆轉儲分析從YourKit截圖

什麼讓我們措手不及HashMap節點[16384]美元增長周期堆轉儲文件。看在HashMap節點[16384]美元顯示,這些HashMap與業務邏輯無關但AWS SDK。

從YourKit截圖

從YourKit截圖

穀歌搜索和代碼分析給我們回答:我們沒有正確關閉連接。同樣的問題也被解決的aws sdk Github問題

第十步:解決內存泄漏

通過分析堆轉儲,我們能夠確定的位置問題。連接時動作,我們創建了一個新的運動客戶在連接打開時每個分區(抄襲磚”運動的文檔):

KinesisSink擴展ForeachWriter(SinkInput]{私人varkinesisClient: kinesisClient =_覆蓋def開放(partitionId:長,版本:長):布爾= {val httpClient = ApacheHttpClient.builder ().build ()kinesisClient = kinesisClient.builder ().region (Region.of(地區)).httpClient (httpClient).build ()真正的}覆蓋def過程(價值:KinesisSinkInput):單元= {/ /流程的東西}覆蓋def關閉(errorOrNull: Throwable):單元= {kinesisClient.close ()}}

但在關閉連接的情況下,我們都是關閉的KinesisClient:

覆蓋def關閉(errorOrNull: Throwable):單位= {kinesisClient.close ()}

Apache Http客戶端沒有被關閉。這導致越來越多的Http客戶端係統上被創建和TCP連接被打開,導致這一問題進行了討論在這裏。在aws sdk文檔州:

*這個提供者創建一個線程定期更新證書的背景。如果提供者不再需要,

我們能夠證明使用以下腳本:

進口艾薇美元。“software.amazon.awssdk: apache-client: 2.13.37”/ /原因伯父(11 e6.toInt)。foreach {_= >software.amazon.awssdk.http.apache.ApacheHttpClient.builder.build ()}/ /不會引起伯父(11 e6.toInt)。foreach {_= >.close software.amazon.awssdk.http.apache.ApacheHttpClient.builder.build () ()}
覆蓋def關閉(errorOrNull: Throwable):單位= {client.close ()httpClient.close ()}

結論

我們看到在這篇文章中是如何診斷內存泄漏的一個例子發生在一個火花應用程序。如果我再次麵臨這個問題,我會附上一個JVM分析器執行程序並嚐試調試它。

從這次調查中,我們有一個更好的理解內部引發結構化流是如何工作的,以及我們如何可以調整我們的優勢。一些值得記住的教訓:

  1. 內存泄漏可能發生,但是有很多事情你可以做調查。
  2. 我們需要更好的工具來讀取大型hprof文件。
  3. 如果你打開一個連接,當你完成,總是關閉它。
免費試著磚
看到所有工程的博客的帖子
Baidu
map