方格精選

從你的 Node.js 專案裡找出 Memory leak,及早發現、及早治療!

更新於 發佈於 閱讀時間約 10 分鐘
前陣子,公司已經上線的產品持續出現效能問題,服務運作一段時間後會慢得讓人難以忍受,甚至超過 node 記憶體上限而直接中止,當時專案還沒實裝監測系統,只能邊調整邊測試,搞得團隊焦頭爛額。這時同事講出明燈般的一句話:「該不會是傳說中的 Memory leak 吧?」
經過工程師們同心協力,總算是解決了這個問題,看到記憶體使用圖不再步步高升的那一刻,真的很感動 ⋯⋯ 這篇文章希望能留下記錄,並說明:
  • memory leak 的起因
  • 如何找出造成 memory leak 的程式碼
  • 我們遇到的實例,與解決方式

小心!Memory leak 離我們並不遙遠!

Memory leak(記憶體流失),是指程式運行的過程中,不再使用的記憶體空間沒有正常被釋放,持續佔用空間而造成的記憶體浪費。如果這種狀況不斷發生,就會使可用的記憶體越來越少,而降低電腦的效能,最後可能導致程式崩潰
在 JavaScript 裡,記憶體回收的工作是交由自動化的 Garbage Collection 來完成,它就像記憶體裡的清道夫,會判斷不再使用的記憶體並將其回收。
那很好啊!代表我不需要擔心記憶體流失的問題了,對吧?
並不是這樣的。Garbage collection 的簡單流程是:定期從根物件 (root,在瀏覽器中是 window,node 則是 global) 開始往下探詢每一個子節點,並清除沒有被探詢到、或是沒有被探詢物件參考的物件,也就是所謂「無法到達的物件 (unreachable objects)」,而在程式中,要製造出一個「可被探詢到的垃圾資料」簡直輕而易舉。
如果 root -> F 的參考消失,導致 F 變成「無法到達的物件」
那麼 F 與其子節點們就會被自動回收。
舉例來說,我們寫一個簡單的 Server:
Code

每次收到請求,都會使 requests 的資料量成長,而 request 又存在於 global scope,屬於可從根探詢到的程式碼,因此它不會被清除,如果 requests 是沒有用的資料,就會造成記憶體空間的浪費,甚至隨著請求數增多而用盡記憶體空間。
其它像是 timerclosure 的誤用也會造成 memory leak,我們這邊先不詳述。畢竟,要直接從整個專案的程式碼中翻找出 memory leak 簡直是大海撈針 ⋯⋯ 我覺得更有效率的方式應該是使用工具來找出不斷增長的資料,縮小範圍後再修正造成問題的程式碼
若你的記憶體使用圖表是這個形狀,你可能就是 memory leak 的受害者!

使用 DevTool 找出病灶!

Chrome 提供的 DevTool 除了能監測運行在瀏覽器中的程式外,也能監測運行在本地端的 Node.js 程式。如果你使用 Node.js 運行 API Server,或是你的前端使用像 Next.js 的 SSR 框架(它會常駐一個 Node Server),就能使用 Devtool 監測 Heap 的使用狀況。
我們先以上面的簡單 Server 為例,使用 --inspect flag 來啟動它。
node --inspect app.js
在 console 裡,你會看到 Node.js 已經幫我們打開一個監聽 9229 port 的 debugger。
接著,打開你的 Chrome 並輸入 chrome://inspect ,你應該能夠在 Remote Target 裡找到 app.js,按下 inspect 來打開 DevTool。
切換到 Memory 頁籤,我們會在下方看到目前運行中的 VM instance,按下 Take snapshot,它就會幫我們分析此刻的 heap 使用狀況,並將結果儲存在左側邊欄。

從這張圖中,我們可以看到:

  • Constructor:此物件的建構子 (或 DevTool 幫我們進行的分類)。
  • Distance:從根 (root) 開始探訪的深度。
  • Shallow Size:物件本身佔用的記憶體量(bytes),通常 shallow size 很大的都是 String 或是裝著 Primitive Type Data 的陣列,如果是物件裡存著 reference 則不會被算到 shallow size 裡面。
  • Retained Size:物件本身佔用的記憶體空間,加上依賴此物件的所有資料所佔用的記憶體量(bytes)。文件裡有一句淺顯易懂的解釋:你刪除這個物件後,他總共會釋放的記憶體量,因此在查找 memory leak 問題時,我們會以這個欄位為判斷點。
點開左邊的箭頭,就能繼續往下探詢,相當方便。我們先將物件以 Retained Size 排序,沒意外的話佔用最多的應該是 (compiled code),因為我們引用了一些套件,這些物件也會被存在 Heap 中。
接下來,我們就要手動重現 memory leak

使用 K6 進行壓力測試

如果你的專案是 API 或 SSR Server,就可以用發送大量請求的方式來重現 memory leak,這邊我推薦使用 K6,也可以直接使用你熟悉的壓力測試服務。
我們先寫一個簡單的測試。一般來說,測試單一 Endpoint 可能無法重現問題,你可能需要將 API Server 所有提供的(或是你懷疑的) Endpoints 測試一遍,就能知道是哪一段程式碼造成問題。

接著從終端機啟動壓力測試,使用下面的參數:
  • duration:執行測試的時間
  • vus:同時測試的虛擬使用者數量 (virtual users)
k6 run --duration 2m --vus 100 request.js
K6 壓測執行中的畫面
等它跑完。
等。

回來觀察 DevTool 的變化

每次跑完壓力測試,就回到 DevTool 快照一下,如果你發現 heap 不斷增長且沒有回到正常數字的話,那八成是抓到兇手了
觀察兩個 snapshot 之間的變化,在這個例子中不難發現是 global scope 底下的資料不斷成長。實際狀況下,leak 的記憶體量可能很少,你得發送更多請求數才能看出端倪。點開之後發現 DevTool 連變數名稱都告訴你了,接下來,只需要把問題修正,就能順利解決 memory leak 問題了。
壓測前,compiled code 佔用最多空間
壓測後,global 佔用了大部分的空間
點開 global 物件,可以看到 requests 這個 Array 正式佔用記憶體的元兇

我們實際遇到的狀況


看到這裡,你可能心想:
「我是一個有紀律的程式設計師,才不會隨便污染 global scope!」
「我用的套件都是精挑細選,至少都 10,000 stars 欸,這些套件應該不會隨便就出現 memory leak 問題吧?」
沒想到呀沒想到,就真的被我們遇見了來自套件的 memory leak。下圖是經過 V6 壓測兩分鐘前後的 snapshot,可以看到 instance 這項資料成長了近一倍,且隨著請求數而增長,不會自動被回收。
第一次壓測,instance 佔用 16%
第二次壓測,instance 成長到 23%,佔用 19 MB 的 heap
從圖中我們看見資料型態 Map,以及子元素的資料型態 HashArrayMapNode,這些都來自 immutable.js 套件。而在我們專案中,相依 immutable 的套件只有 Facebook 開源的文本編輯器 draft.js
經過搜尋,發現我們不是第一個遇到此問題的人,早在 2020 就有人回報這個issue,trace code 就會發現 draft.js 有兩項 global scope 資料是沒有主動回收的:

DraftEntity.js

在這個檔案中,我們首先看到一個宣告在 global scope 的資料 instance。

接著,從這個模組的 __add 方法,我們看見 instance 資料量會不斷增長。

但它並沒有提供一個清除 instance 的方法,還好有一個 setter 可以讓我們利用,只要將它初始化就能順利觸發 JavaScript 的回收機制,把所有子節點佔用的記憶體都釋放掉。
Entity.__loadWithEntities(Map());


CharacterMetadata.js

這裡 我們一樣看到不斷增長的 global scope 物件 pool

比較麻煩的是,這項資料連個 setter 都沒有,我們只能手動增加清除方法將其初始化:

老實說,如果 draft.js 是單純在瀏覽器上執行,應該是不會有問題的,因為使用上並不會一直開新的文本編輯器 instance。只是我們的 Next.js Server 在收到請求並編譯時,就意外地將初始化的資料留一份在 Server 端了(而且也完全用不到這項資料)。隨著使用者在網站上打開文本編輯器,記憶體空間也逐漸變少,最後導致程式崩潰。
修正完程式碼後,終於讓記憶體使用量回歸正常,看看下面這張圖,真的是讓人法喜充滿啊!如果你也因 memory leak 困擾的話,就用 DevTool 來找出病灶吧,希望這篇記錄有幫助到大家,謝謝一起苦惱的隊友們 XD。
終於 ⋯⋯ 看見平原了啊!
稍晚後端又加上了機器分流,讓 loading 變得更低了 🤩🤩!

延伸閱讀









五位玩家用聲音跟你聊桌遊。 近期開始努力經營 YouTube, 希望我們的內容可以為你帶來歡樂, 歡迎一同入席,享受遊戲。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
對坂本龍一的印象原本停留在《俘虜》裡,那位內心熱情如火,卻不斷壓抑情感的軍官世野。而在紀錄片裡看到的坂本龍一,是位嬉戲於音樂世界裡的工匠,好像每天都能從那個世界裡找到一顆令他著迷的貝殼。
讀著川端康成筆下的雪國景色,心中浮現的是記憶裡的小樽市,那是我第一次看到雪的城市。書的開頭寫道:「夜空下一片白茫茫」,這份意象與小樽運河旁,沿著河道不斷延伸到地平線的白色平原重疊在一起。幸好畢業時有到北海道旅行一趟,否則,也許會難以想像這雪國的景象。
會以「海」作為荒島歌單的主題,是因為偶然間在 Youtube 聽到日本樂團 Lamp 的第二張專輯《致戀人》。主唱永井祐介輕柔的嗓音,與專輯封面中海浪拍打在礫石灘上的照片,在腦海中引出了幾幕海濱散步的回憶:輕躍在蘭嶼礁岩上的浪花,或北海岸綿延的海岸線。
會以「海」作為荒島歌單的主題,是因為偶然間在 Youtube 聽到日本樂團 Lamp 的第二張專輯《致戀人》。主唱永井祐介輕柔的嗓音,與專輯封面中海浪拍打在礫石灘上的照片,在腦海中引出了幾幕海濱散步的回憶:輕躍在蘭嶼礁岩上的浪花,或北海岸綿延的海岸線。
上週 Z-Man 出版社釋出了一部雷霆萬鈞的預告片(還沒看過的話,這裡請),一開始我還真不敢相信自己的眼睛,甚至以為是粉絲製作 —— 合作遊戲的大前輩《瘟疫危機》即將推出《魔獸世界:巫妖王之怒》版本!
喔,我的天啊!《Fort》這遊戲到底是想要多可愛?新的擴充《貓與狗》為你的童年帶來忠心的朋友,或是讓你成為一位專業鏟屎官。看到卡片之後,我認真想要買一盒來當畫冊收藏,因為 Kyle Ferrin 筆下的貓貓狗狗會讓你不斷喊著「好可愛!」直到被口水噎到。
對坂本龍一的印象原本停留在《俘虜》裡,那位內心熱情如火,卻不斷壓抑情感的軍官世野。而在紀錄片裡看到的坂本龍一,是位嬉戲於音樂世界裡的工匠,好像每天都能從那個世界裡找到一顆令他著迷的貝殼。
讀著川端康成筆下的雪國景色,心中浮現的是記憶裡的小樽市,那是我第一次看到雪的城市。書的開頭寫道:「夜空下一片白茫茫」,這份意象與小樽運河旁,沿著河道不斷延伸到地平線的白色平原重疊在一起。幸好畢業時有到北海道旅行一趟,否則,也許會難以想像這雪國的景象。
會以「海」作為荒島歌單的主題,是因為偶然間在 Youtube 聽到日本樂團 Lamp 的第二張專輯《致戀人》。主唱永井祐介輕柔的嗓音,與專輯封面中海浪拍打在礫石灘上的照片,在腦海中引出了幾幕海濱散步的回憶:輕躍在蘭嶼礁岩上的浪花,或北海岸綿延的海岸線。
會以「海」作為荒島歌單的主題,是因為偶然間在 Youtube 聽到日本樂團 Lamp 的第二張專輯《致戀人》。主唱永井祐介輕柔的嗓音,與專輯封面中海浪拍打在礫石灘上的照片,在腦海中引出了幾幕海濱散步的回憶:輕躍在蘭嶼礁岩上的浪花,或北海岸綿延的海岸線。
上週 Z-Man 出版社釋出了一部雷霆萬鈞的預告片(還沒看過的話,這裡請),一開始我還真不敢相信自己的眼睛,甚至以為是粉絲製作 —— 合作遊戲的大前輩《瘟疫危機》即將推出《魔獸世界:巫妖王之怒》版本!
喔,我的天啊!《Fort》這遊戲到底是想要多可愛?新的擴充《貓與狗》為你的童年帶來忠心的朋友,或是讓你成為一位專業鏟屎官。看到卡片之後,我認真想要買一盒來當畫冊收藏,因為 Kyle Ferrin 筆下的貓貓狗狗會讓你不斷喊著「好可愛!」直到被口水噎到。
你可能也想看
Google News 追蹤
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
對於程式卡頓的問題,如何分析程式碼占用多少記憶體,如何釋放或改寫,可以先用python內建的tracemalloc模組來追蹤 Python 分配的記憶體區塊。 本文將介紹最簡單的用法,來分析一段程式碼占用了多少記憶體。 結果呈現 印出當前使用的記憶體,與峰值記憶體使用量。 程式範例 i
Thumbnail
今天接洽新工作時,我感受到身體陷入過去一樣的狀態了。 我會失敗。 這個念頭雖被我在對談過程中擱置,但我明白,它未離去,亦未歸檔。 在談完工作後,給自己一段放風時間,迎著熱烈的陽光行走。 我想起了過去被莫名其妙解僱,但原因已不重要。事件已過,情緒卡著,我的身體防衛系統仍感覺到會〔受傷〕仍舊為我
※ 非同步概念總複習 為什麼要使用 Promise? 在 JavaScript 開發中,處理非同步操作是常見需求,涉及如文件讀寫、數據庫查詢或網路請求等耗時任務。傳統的回調方式可能導致代碼結構混亂,稱為「回調地獄」,難以維護和理解。 Promise 是解決這問題的方法。它是一個物件(objec
Thumbnail
標示全部為已讀失效 最近發現留言系統中,"標示全部為已讀"的速度明顯變慢,甚至有時會失效。許多使用者都報告遇到了相同的問題。這實際上是程式設計中一個常見的漏洞。系統沒有充分考慮到整體容量問題與效能,才導致了這樣的情況。(實際原因待查,此處僅為一般解說),當系統開始顯示緩慢或出現其他問題時,通常
Thumbnail
記憶,是時間的罅隙,當生命不斷流動,總有一些渣滓會淤積,落進罅隙。遠遠看,似乎什麼都沒有被留住。但當我們有機會俯身細細查看,我們會發現,罅隙里藏了很多,太多。「多」,意味著我們每個人,無論我們怎麼看待我們自己,我們都過了豐富的一生。
Thumbnail
在這資訊爆炸的時代,你有沒有好奇,為什麼有些事情記得牢牢的,而有些則瞬間忘卻? 本文將與你深入工作記憶的奧秘,解鎖學習和思考的隱藏技巧,讓我們一起探索,如何在生活和工作中,更聰明的運用我們的大腦。 閱讀全文,開始你的認知升級之旅! 工作記憶的奧秘:揭開思考和學習的隱藏限制 在我們繁忙且資
Thumbnail
在寫CI的時候是否曾經遇過out of memory的錯誤呢?CodeIgniter作為輕量化的PHP框架,db物件一直是操作資料庫的好幫手,簡化了下達sql指令時的操作,加快了開發的速度,但其實看似好用的工具裡說不定有著隱藏的問題。
Thumbnail
確保沒有遺漏或錯誤 程式的完整資訊資料對於程式設計至關重要。這是因為只有透過完整的資訊,我們才能確保在程式設計中沒有任何遺漏或錯誤。最終,後台管理扮演著管理系統中所有動作和行為是否符合特定標準的重要角色。 採取不符合預期的行動 這種符合性的重要性在於,當我們設計程式時,希望使用者按照預期的方式
Thumbnail
資料庫之備份工作大都是自動執行,但是執行結果是否成功,需要安排人員去檢查,有時疏忽忘記確認作業,致備份工作失敗仍不知道,等到有一天需要回復舊有資料的場合時,才發現找不到過去某段期間的備份資料,造成無法彌補之後果。   2.    改善: 2.1 設計一執行檔,功能為打開備
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
對於程式卡頓的問題,如何分析程式碼占用多少記憶體,如何釋放或改寫,可以先用python內建的tracemalloc模組來追蹤 Python 分配的記憶體區塊。 本文將介紹最簡單的用法,來分析一段程式碼占用了多少記憶體。 結果呈現 印出當前使用的記憶體,與峰值記憶體使用量。 程式範例 i
Thumbnail
今天接洽新工作時,我感受到身體陷入過去一樣的狀態了。 我會失敗。 這個念頭雖被我在對談過程中擱置,但我明白,它未離去,亦未歸檔。 在談完工作後,給自己一段放風時間,迎著熱烈的陽光行走。 我想起了過去被莫名其妙解僱,但原因已不重要。事件已過,情緒卡著,我的身體防衛系統仍感覺到會〔受傷〕仍舊為我
※ 非同步概念總複習 為什麼要使用 Promise? 在 JavaScript 開發中,處理非同步操作是常見需求,涉及如文件讀寫、數據庫查詢或網路請求等耗時任務。傳統的回調方式可能導致代碼結構混亂,稱為「回調地獄」,難以維護和理解。 Promise 是解決這問題的方法。它是一個物件(objec
Thumbnail
標示全部為已讀失效 最近發現留言系統中,"標示全部為已讀"的速度明顯變慢,甚至有時會失效。許多使用者都報告遇到了相同的問題。這實際上是程式設計中一個常見的漏洞。系統沒有充分考慮到整體容量問題與效能,才導致了這樣的情況。(實際原因待查,此處僅為一般解說),當系統開始顯示緩慢或出現其他問題時,通常
Thumbnail
記憶,是時間的罅隙,當生命不斷流動,總有一些渣滓會淤積,落進罅隙。遠遠看,似乎什麼都沒有被留住。但當我們有機會俯身細細查看,我們會發現,罅隙里藏了很多,太多。「多」,意味著我們每個人,無論我們怎麼看待我們自己,我們都過了豐富的一生。
Thumbnail
在這資訊爆炸的時代,你有沒有好奇,為什麼有些事情記得牢牢的,而有些則瞬間忘卻? 本文將與你深入工作記憶的奧秘,解鎖學習和思考的隱藏技巧,讓我們一起探索,如何在生活和工作中,更聰明的運用我們的大腦。 閱讀全文,開始你的認知升級之旅! 工作記憶的奧秘:揭開思考和學習的隱藏限制 在我們繁忙且資
Thumbnail
在寫CI的時候是否曾經遇過out of memory的錯誤呢?CodeIgniter作為輕量化的PHP框架,db物件一直是操作資料庫的好幫手,簡化了下達sql指令時的操作,加快了開發的速度,但其實看似好用的工具裡說不定有著隱藏的問題。
Thumbnail
確保沒有遺漏或錯誤 程式的完整資訊資料對於程式設計至關重要。這是因為只有透過完整的資訊,我們才能確保在程式設計中沒有任何遺漏或錯誤。最終,後台管理扮演著管理系統中所有動作和行為是否符合特定標準的重要角色。 採取不符合預期的行動 這種符合性的重要性在於,當我們設計程式時,希望使用者按照預期的方式
Thumbnail
資料庫之備份工作大都是自動執行,但是執行結果是否成功,需要安排人員去檢查,有時疏忽忘記確認作業,致備份工作失敗仍不知道,等到有一天需要回復舊有資料的場合時,才發現找不到過去某段期間的備份資料,造成無法彌補之後果。   2.    改善: 2.1 設計一執行檔,功能為打開備