Node怎麼排查記憶體洩漏?以下這篇文章就來給大家整理總結一下Node記憶體洩漏排查經驗,希望對大家有幫助!
在 Nodejs
服務端開發的場景中,記憶體洩漏
絕對是最令人頭痛的問題;
但只要專案一直在開發迭代,那麼出現 記憶體洩漏
的問題絕對不可避免,只是出現的時間早晚而已。所以系統性掌握有效的 記憶體洩漏
排查方法是一名Nodejs
工程師最基礎、最核心的能力。
記憶體洩漏處理的困難是如何能在無數的功能、函數中找到具體是哪一個功能中的哪一個函數的第多少行到多少行引起了記憶體洩漏。
很遺憾目前市面上沒有能夠輕鬆定位記憶體洩漏的工具,所以許多初次遇到這種問題的工程師會感到茫然,一下子不知道該如何處理。
這裡我以22年的一次排查 記憶體洩漏
的案例分享一下我的處理想法。
2022 Q4
某天,研發用戶群中回饋我們的研發平台不能訪問,後台中出現了大量的異常任務未完成。
第一反應就是可能出現了記憶體洩漏還好服務接入了監控(prometheus
grafana
),在grafana
監控面板中發現在10.00 後記憶體一直在漲沒有下來過出現了明顯的資料外洩。 【相關教學推薦:nodejs影片教學】
說明:
#process memory
:rss
(Resident Set Size),進程的常駐記憶體大小。heapTotal
: V8 堆的總大小。heapUsed
: V8 堆已使用的大小。external
: V8 堆外的記憶體使用量。在
Nodejs
中可以呼叫全域方法process.memoryUsage()
取得這些資料其中heapTotal
和heapUsed
是V8 堆的使用情況,V8 堆是Node.js
中JavaScript 物件儲存的地方。而external
則表示非 V8 堆中分配的內存,例如 C 物件。rss
則是進程所有記憶體的使用量。一般看監控資料的時候重點關注heapUsed
的指標就行了
#其實不管是全域性記憶體洩漏還是局部性的記憶體洩漏,要做的都是盡可能縮小排除範圍。
中間件與
元件中,這種類型的記憶體洩漏排查起來也是最簡單的。
2022 Q4 中遇到的記憶體洩漏不屬於這個類型,所以還得按照局部性洩漏的思路進行分析。
流程流程:
中間件、
元件、或其它公用邏輯的使用)
2020 年的時候我在做基於##局部性記憶體洩漏排查Nuxt
SSR 應用時,上線前壓測發現應用記憶體洩漏,判斷定為全局性的洩漏之後,採用二分法排查大約花了30min 就成功定位了問題。
當時洩漏的原因是我們在服務端使用
axios導致的洩漏,後來統一
axios相關的全換成
node-fetch後就解決了,從此換上了
axios PDST後來絕對不會在
Node服務中使用
axios了
、某個介面
、某個非同步任務
中,由於這樣的特性它的排查難度也較大。這種情況都會做 heapdump
來分析。 <p>這裡主要講我這個案例中的思路關於<code>heapdump
的詳細說明我放在下個段落,
Heap Dump
:堆轉儲, 後面部分都使用heapdump
表示,做heapdump
的工具和教程也非常多比如:chrome、vscode、heapdump 這個開源函式庫。我用的 heapdump 庫做的網路教學非常多這裡不展開了。
局部性記憶體洩漏排查需要一定的記憶體洩漏排查經驗,每次遇到都把它當成對自己的一次磨礪,這樣的經驗累積多了以後排查記憶體洩漏問題會越來越快。
這一點非常重要,明確了這一點可以大幅縮小排查範圍。
經常會出現這種情況,這個迭代做了A、B、C 三個功能,壓測時或上線後出現了記憶體洩漏。那就可以直接鎖定,記憶體洩漏發生小這三個新的功能之中。這種情況下就不需要非常麻煩的去生產做 heapdump
我們在本地通過一些工具就可以很輕鬆的分析定位出內存洩漏點。
由於我們20年Q4
的一些特殊情況,當我們發現存在記憶體洩漏的時候已經很難確定記憶體洩漏初次出現在什麼時間點了,只能大概鎖定在1月的時間內。這一個月我們又經歷了一個大版本迭代,如果一一排查這些功能與介面成本必然非常高。
所以還需要結合更多的數據進行進一步分析
node
新增--expose-gc
,這個參數會向全域注入gc()
方法,方便手動觸發GC 取得更準確的堆快照
資料heapdump
擷取堆快照資料時需要特別注意的一些點!
- 在
heapdump
時 Node 服務會中斷,根據當時伺服器記憶體大小這個時間會在 2 ~ 30min 左右。在生產環境做heapdump
需要和維運一起制定合理的策略。我在這裡是使用了主、備兩個pod
, 當主pod
停掉之後,業務請求會透過負載平衡到備用pod
由此保障生產業務的正常進行。 (這個過程必定是一個與維運密切配合的過程,畢竟heapdump
玩抽還需要透過他們拿到伺服器中堆快照
檔)- 上述接近臨界點列印快照只是一個模糊的描述,如果你試過就知道等非常接近臨界點再打印內存快照就打印不出來了。所以接近這個度需要自己把握。
- 做至少3 次
heapdump
(實際上為了拿到最詳細的資料我做了5 次)
需要你的應用服務接入監控,我這裡應用是使用prometheus
grafana
做的監控, 主要監控服務的以下指標
QPS
(每秒請求存取量) ,請求狀態,及其存取路徑 (平均介面回應時間) 及其存取資料
版本
(句柄)
服務程序重啟次數heapTotal
、heapUsed
、external
、heapAvailableDetail
資料是不夠的,heapdump
我的分析處理結果資料非常晦澀,就算在視覺化工具的加持下也難以準確定位問題。這時候我是結合了
grafana的一些數據一起看。
grafana
監控面看看到記憶體一直在漲一直下不來,但同時我也注意到,服務中的句柄數也在瘋漲一直不掉。
2、這是我回顧了一下出現洩漏的那一個月新增的功能懷疑可能是在使用 bull
訊息佇列元件造成的記憶體洩漏。先去分析了相關應用程式碼,但並看不出那裡寫的有問題導致了記憶體洩漏,
結合 1 中句柄洩漏的問題感覺是在使用 bull
後需要手動的去釋放某些資源,在這個時候還不太確定具體原因。
3、然後對5 次的heapdunmp
資料進行了分析,資料匯入chrome
對5 次堆快照進行比較後,發現每次建立佇列後TCP 、Socket、EventEmitter 的事件都沒有被釋放到。到這裡基本可以確定是由於對 bull
的使用不規範導致的。在 bull
通常不會經常建立佇列,佇列佔用的系統資源並不會自動釋放,若有需要,需手動釋放。
4、在調整完程式碼後重新進行了壓測,問題解決。
Tips: Nodejs 中的
句柄
是一種指針,指向底層系統資源(如檔案、網路連接等)。句柄允許 Node.js 程式存取和操作這些資源,而無需直接與底層系統互動。句柄可以是整數或對象,這取決於 Node.js 函式庫或模組使用的句柄類型。常見句柄
:
fs.open()
傳回的檔案句柄net.createServer()
傳回的網頁伺服器句柄dgram.createSocket()
傳回的UDP socket 帳號child_process.spawn()
傳回的子程序句柄crypto.createHash()
傳回的雜湊句柄zlib.createGzip()
傳回的壓縮句柄
通常很多人第一次拿到堆快照
資料是懵的,我也是。在看了網路上無數的分析技巧結合自身實戰後總結了一些比較好用的技巧,一些基礎的使用教程這裡就不講了。這裡主要講資料導入chrome
後如何看圖;
看這個視圖的時候一般會先對Retained Size 進行排查,然後觀察其中物件的大小與數量,有經驗的工程師,可以快速判斷出某些物件數量異常。在這個視圖中除了關心自己定義的一些物件之外, 一些容易發生記憶體洩漏的物件也需要注意如:
TCP
Socket
Summary 視圖, 不能定位到問題這時我們一般會使用
Comparison 視圖。透過這個視圖我們能對比兩個堆快照中物件個數、與物件佔有記憶體的變化;
透過這些資訊我們可以判斷在一段時間(某些操作)之後,堆中的物件與記憶體變化的數值,透過這些數值我們可以找出一些異常的物件。透過這些物件的名稱屬性或作用可以縮小我們記憶體洩漏的排查範圍。
Comparison 視圖中選擇兩個堆疊快照,並在它們之間進行比較。您可以查看哪些物件在兩個堆快照之間新增,哪些物件在兩個堆快照之間減少,以及哪些物件的大小發生了變化。
Comparison 視圖還允許查看物件之間的關係,以及物件的詳細信息,例如類型、大小和引用計數。透過這些訊息,可以了解哪些物件是導致記憶體洩漏的原因。
lru-cache
存的太多就會導致記憶體不夠用,在Nodejs 服務中建議使用redis
替代lru-cache
##服務需要存取監控,方便第一時間確定問題類型
#判斷記憶體洩漏是全域性的還是局部性的
#全域性記憶體洩漏使用二分法快速排查定位
局部記憶體洩漏
遇到記憶體洩漏的問題不要畏懼,多累積記憶體洩漏問題的排查經驗處理經驗多了找起來就非常快了。每次解決之後做複盤總結回頭再多看看
堆快照
數據利於更快的積累相關經驗
更多node相關知識,請造訪:nodejs 教學!
以上是【經驗總結 】Node怎麼排查記憶體洩漏?思路分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!