這半年發生真多事,疫情爆發實施WFH,小孩滿周歲,買了人生第一輛二手車,域名莫名逾期,家中出現白蟻…等等(BTW, Shawn前走網址從七月改了…)。這些林林總總的事使得在家上班並沒有比去公司輕鬆。當然許多事有弊必有利,這段期間可以看到小孩一點點成長也是一種喜悅(用汗水與淚水換來)。然而,在這段期間還是有許多技術的東西想要分享給大家,但是礙於時間關係,遲遲無法下筆,剛好最近疫情趨緩,生活步調也漸漸恢復,趁還記憶猶新的時候分享給大家。

說了那麼多,趕快進入正題;最近使用Dotnet Core上到k8s時,使用Prometheus監控發現有memory一直無法釋放的問題,並且Dotnet Core非常貪婪,會一直吃記憶體直到記憶體上限重啟服務。為了解決此問題,使用dotnet tool來dump memory的快照來分析,最後順利地解決記憶體異常被吃光的問題。在此紀錄一下如何找出並分析是哪一塊程式出了問題。


如何使用

STEP 1 安裝SDK

首先,你要能進入正在執行的線上服務的Pod中(此塊為k8s議題),通常官方的image不會有安裝dotnet tool給你使用,所以這邊要自行安裝。
以下是以Debian 10為例,先安裝Dotnet Core SDK 3.1,大致安裝過程大同小異。(可以根據自己的需求變更)

curl https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb -o packages-microsoft-prod.deb
dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
apt-get update; \
apt-get install -y apt-transport-https && \
apt-get update && \
apt-get install -y dotnet-sdk-3.1

STEP 2 安裝並執行dotnet tool

接下來就是安裝dotnet tool,這麼多工具安裝哪些呢,我這邊只需要安裝一個工具就可以了,就是gcdump,其他工具用途大家有空可以玩玩看喔。如果你是3.1以上的版本建議直接裝gcdump即可,如果不是可以安裝dotnet-dump。

dotnet tool install --global dotnet-gcdump

基本上安裝完會在使用者目錄下的.dotnet資料夾裡有一個執行檔,只要執行一下就可以採集當下記憶體快照。

~/.dotnet/tools/dotnet-gcdump ps // 此方法可以獲得主機裡dotnet core的process id
~/.dotnet/tools/dotnet-gcdump collect --process-id 1234 // 假設process id為1234,執行採集

這時候在你執行的目錄地下會有一個類似20210905_xxxx_1.gcdump的檔案,此檔案就是快照。可以使用Visual Studio or PerfView的軟體打開分析。
為了之後下載方便,我們還是先將檔案壓縮一下。

tar -zcvf dotnet_snapshot.tar.gz 20210905_xxxx_1.gcdump

STEP 3 下載快照檔

接下來就是要把那個快照檔案下載回來本地。此案例是跑在k8s的環境,這邊假設你已經有kubectl的權限,且已經將kubeconfig匯入能以對此cluster做操作。 只需要執行類似以下指令:

kubectl cp <namespace>/<pod>:/<file-path>/dotnet_snapshot.tar.gz ~/dotnet_snapshot.tar.gz
tar -zxvf ~/dotnet_snapshot.tar.gz

你就可以在本地端獲取到快照檔案。

STEP 4 安裝並使用PerfView

在此介紹如何用PerfView做分析;首先,去找一台Window電腦(…這就是微軟)。第二,就是去PerfView的Github下載最新的安裝檔
安裝完成以後只要執行匯入(將檔案拖入左邊視窗即可)。即可看到以下畫面。這邊只簡單介紹不再多做說明。 1 我們可以把焦點放在第二個與倒數第二個頁籤(RefFrom-RefTo and Flame Graph)
首先我們先點擊Flame Graph就可以清楚看到memory heap的堆疊圖,如下所示:
2 可以清楚看到此記憶體問題有兩大塊組成,左邊我們可以看到是微軟的IdentityModel.Tokens出了問題。右邊是我們自己的服務,且引用到StackExchange.Redis所照成。哈哈,問題呼之欲出,之後就可以開始找問題並分析問題。
若是想要更深入了解層層呼叫關係,可以使用第二個頁籤去做更細部的分析,在此不多做說明了。

STEP 5 分析並處理異常

在此提醒幾個分析要點,至於詳細解決方案還是要看code如何寫。

  • 是不是使用Package本身的bug
  • 是不是使用第三方庫的方式錯誤
  • 自己程式寫不好…

關於第一點,只要你發現Flame Graph上,沒有你自己寫的namespace,就是此問題。但說老實話要解這問題很兩極,要馬超簡單要馬超級難;因為是別人提供的程式,如果該Package已經停止維護那就很可能GG了。我這次發生的問題是Dotnet core官方所維護的JWT的bug,上去Github就能找到這個issue,而且剛好在最近被解掉(So lucky!),趕快更新喔~
第二點就是Flame Graph下層是我們開發的程式,但是上層是引用到其他的第三方庫,這時很有可能是我們不當的使用到別人庫。可以去他們的官網,或是github多看看文件來修正寫法。此次,我是使用StackExchange.Redis的第三方庫,我一開始沒注意官網有說明不需要對IDataBase這個物件作儲存,因為開銷不大每次new就好了。所以使用時要多看文件啊。
第三點就是Flame Graph較上層幾乎顯示為你的code,這時可探討的議題就多了,在此不多做敘述了,以後有機會再跟他家說。以下羅列一些可能發生的原因:

  • Сlosure(閉包)使用不當
  • Static變數濫用
  • Events沒有被釋放
  • Infinitely threads沒有停止
  • Cache使用氾濫(Dotnet core本身的cache)
  • IDisposable不當的實作

好了~我想寫到這裡至少幫大家開個頭,如果真有分析上的問題歡迎寄信討論。