John Miiler 是ebay團隊的高級後端工程師,負責各類項目,包括結帳和支付系統。做爲公司擺脫單一業務的努力的一部分,他的團隊正試圖將業務邏輯一塊一塊地提取到單獨的微服務中。他分享了他的團隊如何解決在提取圖像處理微服務時遇到的內存使用問題。html
最近提取的microservice是一種圖像處理服務,它對圖像進行大小調整、裁剪、從新編碼和執行其餘處理操做。這個服務是一個在Docker容器中使用springboot構建的Java應用程序,並部署到AWS託管的Kubernetes集羣中。在實現該服務時,咱們偶然發現了一個巨大的問題:該服務存在內存使用問題。本文將討論咱們識別和解決這些問題的方法。我將從對通常記憶問題的簡要介紹開始,而後深刻研究解決這個問題的過程。java
我覺得是內存泄露。可是在使用內存分析器(MAT)時,當我比較一個快照和另外一個快照的內存使用狀況時,個人「驚喜」時刻到來了,我意識到問題在於springboot產生的線程數linux
有許多類型的錯誤直接或間接地影響應用程序。本文主要討論其中的兩個問題: OOM (內存不足)錯誤和內存泄漏。調查這類錯誤多是一項艱鉅的任務,咱們將詳細介紹在咱們開發的服務中修復此類錯誤所採起的步驟。web
OOM錯誤表明第一類內存問題。它能夠歸結爲一個試圖在堆上分配內存的應用程序。可是,因爲各類緣由,操做系統或虛擬機(對於JVM應用程序)沒法知足該請求,所以,應用程序的進程會當即中止。spring
使識別和修復變得很是困難的是,它能夠在任什麼時候候從代碼中的任何位置發生。所以,僅僅查看一些日誌來肯定觸發它的代碼行一般是不夠的。一些最多見的緣由是:數據庫
有各類各樣的開源和開源工具,用於檢查進程的內存使用狀況以及它是如何演變的。咱們將在後面的部分討論這些工具。後端
首先讓咱們瞭解什麼是內存泄漏。內存泄漏是一種資源泄漏類型,當程序釋放丟棄的內存時發生故障,致使性能受損或失敗。當一個對象存儲在內存中,但運行的代碼沒法訪問時,也可能發生這種狀況。springboot
這聽起來很抽象,但在現實生活中,內存泄漏到底是什麼樣子呢?讓咱們看一個用垃圾回收(GC)語言編寫的應用程序內存泄漏的典型示例。微服務
該圖顯示了舊的Gen內存(老年代對象)的內存模式。綠線顯示分配的內存,紫色線顯示GC對Old Gen memory執行掃描階段後的實際內存使用量,垂直紅線顯示GC步驟先後內存使用量的差別。工具
正如您在本例中所看到的,每一個垃圾收集步驟都會略微減小內存使用量,但整體而言,分配的空間會隨着時間的推移而增加。此模式表示並不是全部分配的內存均可以釋放。
內存泄漏有多種緣由。咱們將在這裏討論最多見的。第一個也是最容易被忽視的緣由是 靜態變量的濫用 。在Java應用程序中,只要全部者類加載到Java虛擬機(JVM)中,靜態字段就存在於內存中。若是類自己是靜態的,那麼將在整個程序執行過程當中加載該類,所以類和靜態字段都不會被垃圾回收。
這個問題的實際解決辦法出人意料地簡單。咱們選擇將默認線程池從200個線程覆蓋到16個線程。
未關閉的流和鏈接是內存泄漏的另外一個緣由。通常來講,操做系統只容許有限數量的打開的文件流,所以,若是應用程序忘記關閉這些文件流,在一段時間後,最終將沒法打開新文件。
一樣,容許的開放鏈接的數量也受到限制。若是一我的鏈接到一個數據庫但沒有關閉它,在打開必定數量的這樣的鏈接以後,它將達到全侷限制。在此以後,應用程序將沒法再與數據庫通訊,由於它沒法打開新的鏈接。
最後,內存泄漏的最後一個主要緣由是未釋放的本機對象。若是本機庫自己有漏洞,那麼使用本機庫 JNI 的Java應用程序很容易遇到內存泄漏。這些類型的泄漏一般是最難調試的,由於大多數時候,您不必定擁有本機庫的代碼,而且一般將其用做黑盒。
關於本機庫內存泄漏的另外一個方面是,JVM垃圾收集器甚至不知道本機庫分配的堆內存。所以,人們只能使用其餘工具來解決此類泄漏問題。
好吧,理論夠了。讓咱們看一個真實的場景:
現狀
如簡介部分所述,咱們一直致力於圖像處理服務。如下是開發初始階段內存使用模式的外觀:
在這張圖中,Y軸上的數字表示內存的GiBs。紅線表示JVM可用於堆內存(1GiB)的絕對最大值。深灰色線表示一段時間內的平均實際堆使用量,灰色虛線表示隨時間推移的最小和最大實際堆使用量。開頭和結尾處的峯值表示應用程序被從新部署的時間,所以在本例中能夠忽略它們。
該圖顯示了一個明顯的趨勢,由於它從大約須要的300MB堆開始,而後在短短几天內增加到超過800MiB,而在Docker容器中運行的應用程序將因爲OOM而被殺死。
爲了更好地說明這種狀況,讓咱們也看看在同一時間段內應用程序的其餘指標。
看看這個圖,內存泄漏的惟一跡象是堆使用率和GC舊gen大小隨着時間的推移而增加。當堆空間使用量達到1GiB時,運行Docker容器的Kubernetes pod就要被殺死了。每個其餘指標看起來都很穩定:線程數一直保持在略低於40的水平,加載的類的數量也很穩定,非堆的使用也很穩定。
這些圖表中惟一缺乏的變量是垃圾收集時間。它與堆上分配的內存成比例增長。這意味着響應時間愈來愈慢,應用程序運行的時間越長。
咱們試圖解決這個問題的第一步是確保全部的流和鏈接都關閉了。有些角落的案子咱們一開始沒有涉及。然而,一切都沒有改變。咱們觀察到的行爲和之前徹底同樣。這意味着咱們必須更深刻地挖掘。
下一步是查看本機內存的使用狀況,並確保最終釋放全部分配的內存。咱們用來爲服務作重載的 OpenCV 庫不是java庫,而是本地C++庫。它提供了一個能夠在應用程序中使用的Java本機接口。
由於咱們知道OpenCV有可能泄漏Java本機內存,因此咱們確保全部OpenCV Mat對象都被釋放,並在返回響應以前顯式地調用GC。仍然沒有明確的泄漏指示器,內存使用模式也沒有任何變化。
到目前爲止尚未明確的指示,是時候用專用工具進一步分析內存使用狀況了。首先,咱們研究了內存分析器工具中的內存轉儲。
第一個轉儲是在應用程序啓動後生成的,只有幾個請求。第二個轉儲是在應用程序達到1GiB堆使用率以前生成的。咱們分析了在這兩種狀況下分配的內容和可能引發問題的內容。乍一看沒有什麼不尋常的事。
而後咱們決定比較堆上最須要的內存。令咱們驚訝的是,堆上存儲了至關多的請求和響應對象。這是「bingo」時刻。
深刻研究這個內存轉儲,咱們發現堆上存儲了44個響應對象,比初始轉儲中的響應對象要高得多。這44個響應對象實際上都存儲了本身的 launchDurlClassLoader ,由於它位於一個單獨的線程中。每一個對象的保留內存大小都超過 3MiB 。
咱們容許應用程序爲咱們的用例使用不少的線程。默認狀況下,springboot應用程序使用大小爲200的線程池來處理web請求。這對於咱們的服務來講太大了,由於每一個請求/響應都須要幾MB的內存來保存原始/調整大小的圖像。由於線程只是按需建立的,因此應用程序開始時的堆使用量很小,但隨着每一個新請求的增長,使用量愈來愈高。
這個問題的實際解決辦法出人意料地簡單。咱們選擇將默認線程池從200個線程減小到16個線程。這就完全解決了咱們的內存問題。如今堆終於穩定了,所以GC也更快了。
在調查和排除此問題的過程當中,咱們使用了幾個被證實是必不可少的工具:
咱們手頭上的第一個工具是針對JVM度量的DataDog APM儀表板,它很是容易使用,容許咱們得到上面的圖形和儀表板。
咱們用來分析堆使用率和本機內存使用狀況的另外一個工具是jemalloc庫的使用狀況來分析對malloc的調用。爲了可以使用jemalloc,須要使用apt get install libjemalloc dev進行安裝,而後在運行時將其注入Java應用程序:
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so MALLOC_CONF=prof:true,lg_prof_interval:30,lg_prof_sample:17 java [arguments omitted for simplicity ...] -jar imageprocessing.jar
總而言之,我覺得是內存泄漏。可是最後發現問題出在springboot生成的線程數上。
原文連接:http://javakk.com/982.html
若是以爲本文對你有幫助,能夠點贊關注支持一下