首頁 >web前端 >js教程 >【經驗總結 】Node怎麼排查記憶體洩漏?思路分享

【經驗總結 】Node怎麼排查記憶體洩漏?思路分享

青灯夜游
青灯夜游轉載
2023-01-28 19:25:373090瀏覽

Node怎麼排查記憶體洩漏?以下這篇文章就來給大家整理總結一下Node記憶體洩漏排查經驗,希望對大家有幫助!

【經驗總結 】Node怎麼排查記憶體洩漏?思路分享

Nodejs 服務端開發的場景中,記憶體洩漏 絕對是最令人頭痛的問題; 但只要專案一直在開發迭代,那麼出現 記憶體洩漏 的問題絕對不可避免,只是出現的時間早晚而已。所以系統性掌握有效的 記憶體洩漏 排查方法是一名Nodejs 工程師最基礎、最核心的能力。

記憶體洩漏處理的困難是如何能在無數的功能、函數中找到具體是哪一個功能中的哪一個函數的第多少行到多少行引起了記憶體洩漏。 很遺憾目前市面上沒有能夠輕鬆定位記憶體洩漏的工具,所以許多初次遇到這種問題的工程師會感到茫然,一下子不知道該如何處理。 這裡我以22年的一次排查 記憶體洩漏 的案例分享一下我的處理想法。

問題描述

2022 Q4 某天,研發用戶群中回饋我們的研發平台不能訪問,後台中出現了大量的異常任務未完成。 第一反應就是可能出現了記憶體洩漏還好服務接入了監控(prometheus grafana),在grafana 監控面板中發現在10.00 後記憶體一直在漲沒有下來過出現了明顯的資料外洩。 【相關教學推薦:nodejs影片教學

DX 内存泄漏监控图.png

說明

  • #process memory: rss (Resident Set Size),進程的常駐記憶體大小。
  • heapTotal: V8 堆的總大小。
  • heapUsed: V8 堆已使用的大小。
  • external: V8 堆外的記憶體使用量。

Nodejs 中可以呼叫全域方法process.memoryUsage() 取得這些資料其中heapTotalheapUsed 是V8 堆的使用情況,V8 堆是Node.js 中JavaScript 物件儲存的地方。而 external 則表示非 V8 堆中分配的內存,例如 C 物件。 rss 則是進程所有記憶體的使用量。一般看監控資料的時候重點關注heapUsed 的指標就行了

#記憶體洩漏類型

##記憶體洩漏主要分為:

    全域性洩漏
  • 局部性洩漏
#其實不管是全域性記憶體洩漏還是局部性的記憶體洩漏,要做的都是盡可能縮小排除範圍。

全域性記憶體洩漏

全域性內容洩漏出現一般高發於:

中間件元件中,這種類型的記憶體洩漏排查起來也是最簡單的。

很遺憾我在

2022 Q4 中遇到的記憶體洩漏不屬於這個類型,所以還得按照局部性洩漏的思路進行分析。

二分法排查

這種類型我就不講其它科學的分析方法了,這種情況下我認為使用二分法排查是最快的。

流程流程

  • 先註解一半的程式碼(減少一半

    中間件元件、或其它公用邏輯的使用)

  • 隨便選擇一個介面或新寫一個測試介面進行壓測

  • 如果出現記憶體洩漏,那麼洩漏點就在目前使用的程式碼之中,若沒有洩漏則洩漏點出現在

  • 然後一直循環往復上述流程大約20 ~ 60 min 一定可以定位到記憶體洩漏的詳細位置

2020 年的時候我在做基於

Nuxt SSR 應用時,上線前壓測發現應用記憶體洩漏,判斷定為全局性的洩漏之後,採用二分法排查大約花了30min 就成功定位了問題。 當時洩漏的原因是我們在服務端使用
axios 導致的洩漏,後來統一axios 相關的全換成node-fetch 後就解決了,從此換上了axios PDST 後來絕對不會在Node 服務中使用axios

##局部性記憶體洩漏排查

大多數記憶體洩漏的情況都是局部性的洩漏,洩漏點可能存在與某個

中間件

、某個介面、某個非同步任務中,由於這樣的特性它的排查難度也較大。這種情況都會做 heapdump 來分析。 <p>這裡主要講我這個案例中的思路關於<code>heapdump的詳細說明我放在下個段落,

Heap Dump :堆轉儲, 後面部分都使用heapdump 表示,做heapdump 的工具和教程也非常多比如:chrome、vscode、heapdump 這個開源函式庫。我用的 heapdump 庫做的網路教學非常多這裡不展開了。

局部性記憶體洩漏排查需要一定的記憶體洩漏排查經驗,每次遇到都把它當成對自己的一次磨礪,這樣的經驗累積多了以後排查記憶體洩漏問題會越來越快。

1. 確定記憶體洩漏出現的時間範圍

這一點非常重要,明確了這一點可以大幅縮小排查範圍。
經常會出現這種情況,這個迭代做了A、B、C 三個功能,壓測時或上線後出現了記憶體洩漏。那就可以直接鎖定,記憶體洩漏發生小這三個新的功能之中。這種情況下就不需要非常麻煩的去生產做 heapdump 我們在本地通過一些工具就可以很輕鬆的分析定位出內存洩漏點。

由於我們20年Q4 的一些特殊情況,當我們發現存在記憶體洩漏的時候已經很難確定記憶體洩漏初次出現在什麼時間點了,只能大概鎖定在1月的時間內。這一個月我們又經歷了一個大版本迭代,如果一一排查這些功能與介面成本必然非常高。 所以還需要結合更多的數據進行進一步分析

2. 採集heapdump 數據

  • 生產啟動時node 新增--expose-gc,這個參數會向全域注入gc() 方法,方便手動觸發GC 取得更準確的堆快照資料
  • 這裡我加入了兩個介面並帶上了自己的專屬權限,
    • 手動觸發GC
    • 列印堆快照
  • heapdump
    • 專案啟動後第一次列印快照資料
    • 記憶體上漲100M 後:先觸發GC,再第二次列印堆快照資料
    • 記憶體接近臨界時再次觸發GC 然後列印堆疊快照

擷取堆快照資料時需要特別注意的一些點!

  • heapdump時 Node 服務會中斷,根據當時伺服器記憶體大小這個時間會在 2 ~ 30min 左右。在生產環境做 heapdump 需要和維運一起制定合理的策略。我在這裡是使用了主、備兩個pod, 當主pod 停掉之後,業務請求會透過負載平衡到備用pod 由此保障生產業務的正常進行。 (這個過程必定是一個與維運密切配合的過程,畢竟heapdump 玩抽還需要透過他們拿到伺服器中堆快照檔)
  • 上述接近臨界點列印快照只是一個模糊的描述,如果你試過就知道等非常接近臨界點再打印內存快照就打印不出來了。所以接近這個度需要自己把握。
  • 做至少3 次heapdump(實際上為了拿到最詳細的資料我做了5 次)

#3 . 結合監控面板的數據進行分析

需要你的應用服務接入監控,我這裡應用是使用prometheus grafana 做的監控, 主要監控服務的以下指標

  • QPS (每秒請求存取量) ,請求狀態,及其存取路徑
  • ##ART (平均介面回應時間) 及其存取資料
  • NodeJs 版本
  • #Actice Handlers(句柄)
  • ##Event Loop Lag
  • (事件延遲)服務程序重啟次數
  • CPU 使用率
  • 記憶體使用:
  • rss
  • heapTotalheapUsedexternalheapAvailableDetail
  • ##只有
heapdump
資料是不夠的,

heapdump 資料非常晦澀,就算在視覺化工具的加持下也難以準確定位問題。這時候我是結合了 grafana 的一些數據一起看。

我的分析處理結果

由於當時的對快照資料遺失了,我在這裡模擬當時的場景。

1、透過

grafana

監控面看看到記憶體一直在漲一直下不來,但同時我也注意到,服務中的

句柄數也在瘋漲一直不掉。

2、這是我回顧了一下出現洩漏的那一個月新增的功能懷疑可能是在使用 bull 訊息佇列元件造成的記憶體洩漏。先去分析了相關應用程式碼,但並看不出那裡寫的有問題導致了記憶體洩漏, 結合 1 中句柄洩漏的問題感覺是在使用 bull 後需要手動的去釋放某些資源,在這個時候還不太確定具體原因。

3、然後對5 次的heapdunmp 資料進行了分析,資料匯入chrome 對5 次堆快照進行比較後,發現每次建立佇列後TCP 、Socket、EventEmitter 的事件都沒有被釋放到。到這裡基本可以確定是由於對 bull 的使用不規範導致的。在 bull 通常不會經常建立佇列,佇列佔用的系統資源並不會自動釋放,若有需要,需手動釋放。

【經驗總結 】Node怎麼排查記憶體洩漏?思路分享

4、在調整完程式碼後重新進行了壓測,問題解決。

Tips: Nodejs 中的句柄是一種指針,指向底層系統資源(如檔案、網路連接等)。句柄允許 Node.js 程式存取和操作這些資源,而無需直接與底層系統互動。句柄可以是整數或對象,這取決於 Node.js 函式庫或模組使用的句柄類型。常見句柄:

  • fs.open() 傳回的檔案句柄
  • net.createServer()傳回的網頁伺服器句柄
  • dgram.createSocket() 傳回的UDP socket 帳號
  • child_process.spawn() 傳回的子程序句柄
  • crypto.createHash() 傳回的雜湊句柄
  • zlib.createGzip() 傳回的壓縮句柄

#heapdump 分析總結

通常很多人第一次拿到堆快照資料是懵的,我也是。在看了網路上無數的分析技巧結合自身實戰後總結了一些比較好用的技巧,一些基礎的使用教程這裡就不講了。這裡主要講資料導入chrome 後如何看圖;

Summary 視圖

【經驗總結 】Node怎麼排查記憶體洩漏?思路分享看這個視圖的時候一般會先對Retained Size 進行排查,然後觀察其中物件的大小與數量,有經驗的工程師,可以快速判斷出某些物件數量異常。在這個視圖中除了關心自己定義的一些物件之外, 一些容易發生記憶體洩漏的物件也需要注意如:

  • TCP
  • Socket
  • ## EventEmitter
  • global

Comparison 視圖

如果透過

Summary 視圖, 不能定位到問題這時我們一般會使用Comparison 視圖。透過這個視圖我們能對比兩個堆快照中物件個數、與物件佔有記憶體的變化; 透過這些資訊我們可以判斷在一段時間(某些操作)之後,堆中的物件與記憶體變化的數值,透過這些數值我們可以找出一些異常的物件。透過這些物件的名稱屬性或作用可以縮小我們記憶體洩漏的排查範圍。

Comparison 視圖中選擇兩個堆疊快照,並在它們之間進行比較。您可以查看哪些物件在兩個堆快照之間新增,哪些物件在兩個堆快照之間減少,以及哪些物件的大小發生了變化。 Comparison 視圖還允許查看物件之間的關係,以及物件的詳細信息,例如類型、大小和引用計數。透過這些訊息,可以了解哪些物件是導致記憶體洩漏的原因。

【經驗總結 】Node怎麼排查記憶體洩漏?思路分享

Containment 視圖

#顯示了物件之間的所有可達的參考關係。每個物件都被表示為一個圓點,並由一條線條連接到它的父物件。透過這種方式可以查看物件之間的層次關係,並了解哪些物件是導致記憶體洩漏的原因。

【經驗總結 】Node怎麼排查記憶體洩漏?思路分享

Statistics 視圖

這個圖很簡單地不展開講了

【經驗總結 】Node怎麼排查記憶體洩漏?思路分享

記憶體洩漏場景

  • 全域變數:全域變數不會被回收
  • #快取:使用了記憶體密集的第三方函式庫如lru-cache 存的太多就會導致記憶體不夠用,在Nodejs 服務中建議使用redis 替代lru-cache
  • ##「句柄洩漏:調用完系統資源沒有釋放
  • 事件監聽
  • 閉包
  • 循環引用
##總結

  • ##服務需要存取監控,方便第一時間確定問題類型

  • #判斷記憶體洩漏是全域性的還是局部性的

  • #全域性記憶體洩漏使用二分法快速排查定位

  • 局部記憶體洩漏

    • 確定記憶體洩漏出現時間點,快速定位問題功能
    • 採堆快照數據,至少3 次
    • 結合監控數據、堆快照數據、與出現洩漏事時間點內的新功能對記憶體洩漏點進行定位

遇到記憶體洩漏的問題不要畏懼,多累積記憶體洩漏問題的排查經驗處理經驗多了找起來就非常快了。每次解決之後做複盤總結回頭再多看看堆快照 數據利於更快的積累相關經驗

其它

  • #壓測工具:wrk

更多node相關知識,請造訪:nodejs 教學

以上是【經驗總結 】Node怎麼排查記憶體洩漏?思路分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除