方格精選

從你的 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
查看全部
發表第一個留言支持創作者!
對坂本龍一的印象原本停留在《俘虜》裡,那位內心熱情如火,卻不斷壓抑情感的軍官世野。而在紀錄片裡看到的坂本龍一,是位嬉戲於音樂世界裡的工匠,好像每天都能從那個世界裡找到一顆令他著迷的貝殼。
讀著川端康成筆下的雪國景色,心中浮現的是記憶裡的小樽市,那是我第一次看到雪的城市。書的開頭寫道:「夜空下一片白茫茫」,這份意象與小樽運河旁,沿著河道不斷延伸到地平線的白色平原重疊在一起。幸好畢業時有到北海道旅行一趟,否則,也許會難以想像這雪國的景象。
會以「海」作為荒島歌單的主題,是因為偶然間在 Youtube 聽到日本樂團 Lamp 的第二張專輯《致戀人》。主唱永井祐介輕柔的嗓音,與專輯封面中海浪拍打在礫石灘上的照片,在腦海中引出了幾幕海濱散步的回憶:輕躍在蘭嶼礁岩上的浪花,或北海岸綿延的海岸線。
會以「海」作為荒島歌單的主題,是因為偶然間在 Youtube 聽到日本樂團 Lamp 的第二張專輯《致戀人》。主唱永井祐介輕柔的嗓音,與專輯封面中海浪拍打在礫石灘上的照片,在腦海中引出了幾幕海濱散步的回憶:輕躍在蘭嶼礁岩上的浪花,或北海岸綿延的海岸線。
上週 Z-Man 出版社釋出了一部雷霆萬鈞的預告片(還沒看過的話,這裡請),一開始我還真不敢相信自己的眼睛,甚至以為是粉絲製作 —— 合作遊戲的大前輩《瘟疫危機》即將推出《魔獸世界:巫妖王之怒》版本!
喔,我的天啊!《Fort》這遊戲到底是想要多可愛?新的擴充《貓與狗》為你的童年帶來忠心的朋友,或是讓你成為一位專業鏟屎官。看到卡片之後,我認真想要買一盒來當畫冊收藏,因為 Kyle Ferrin 筆下的貓貓狗狗會讓你不斷喊著「好可愛!」直到被口水噎到。
對坂本龍一的印象原本停留在《俘虜》裡,那位內心熱情如火,卻不斷壓抑情感的軍官世野。而在紀錄片裡看到的坂本龍一,是位嬉戲於音樂世界裡的工匠,好像每天都能從那個世界裡找到一顆令他著迷的貝殼。
讀著川端康成筆下的雪國景色,心中浮現的是記憶裡的小樽市,那是我第一次看到雪的城市。書的開頭寫道:「夜空下一片白茫茫」,這份意象與小樽運河旁,沿著河道不斷延伸到地平線的白色平原重疊在一起。幸好畢業時有到北海道旅行一趟,否則,也許會難以想像這雪國的景象。
會以「海」作為荒島歌單的主題,是因為偶然間在 Youtube 聽到日本樂團 Lamp 的第二張專輯《致戀人》。主唱永井祐介輕柔的嗓音,與專輯封面中海浪拍打在礫石灘上的照片,在腦海中引出了幾幕海濱散步的回憶:輕躍在蘭嶼礁岩上的浪花,或北海岸綿延的海岸線。
會以「海」作為荒島歌單的主題,是因為偶然間在 Youtube 聽到日本樂團 Lamp 的第二張專輯《致戀人》。主唱永井祐介輕柔的嗓音,與專輯封面中海浪拍打在礫石灘上的照片,在腦海中引出了幾幕海濱散步的回憶:輕躍在蘭嶼礁岩上的浪花,或北海岸綿延的海岸線。
上週 Z-Man 出版社釋出了一部雷霆萬鈞的預告片(還沒看過的話,這裡請),一開始我還真不敢相信自己的眼睛,甚至以為是粉絲製作 —— 合作遊戲的大前輩《瘟疫危機》即將推出《魔獸世界:巫妖王之怒》版本!
喔,我的天啊!《Fort》這遊戲到底是想要多可愛?新的擴充《貓與狗》為你的童年帶來忠心的朋友,或是讓你成為一位專業鏟屎官。看到卡片之後,我認真想要買一盒來當畫冊收藏,因為 Kyle Ferrin 筆下的貓貓狗狗會讓你不斷喊著「好可愛!」直到被口水噎到。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
近幾年占星研究發展出的一個新領域,就跟「月週期」有關。這個「月週期」,是以每個月的新月為起點,跟星盤上的十二宮有點像,只不過它只有八等分而已。這個週期叫做月相(moon phase),從出生盤上的月相,可以看出靈魂此生的使命所在。
當背負著世界之惡 超人也終有一天會成為世界之惡,原因是世界容不下超人。曾經我們都懷抱著多大的報復,期許自己能在這社會的舞台大顯身手,然而這一切看似理想的夢想,卻在登上舞台的那一刻粉碎。 來自世界的嘲諷、自私、不諒解、甚至是傷害,從沒想過那個我期盼已久的舞台,成了讓我跌最慘傷最痛的地方。我開始害怕,因
Thumbnail
【Bluesound香港】致力為音響愛好者提供一個使用簡便,而且售價親民的串流播放方案。你想要簡單的系統,他們有主動式的無線喇叭,喇叭就是音響,非常簡單。倘若你已經有一套音響了,想為系統加上串流播放的功能,Bluesound Node 2i 就是你需要的。
Thumbnail
如果以提供一項服務,要對方接受;或是陳述一個概念,要對方買單,都算是廣義的銷售,那麼在醫院工作的我去上一堂銷售課也就不是一件稀奇的事了。 不上則已,一上驚為天人。
任由我放縱淚水 就在那個毫無預警地當下 我哭了 一個人安靜地坐在書桌旁 上一秒不是還在讀書嗎? 可是,為甚麼,我哭得好慘 彷彿有種難過的心情趨勢著我 我很累,面對現實我沒有站起來的信心 放縱了淚水,聲嘶力竭去訴說這不公平的世界 大家都以為,有些人很堅強無論何時都沒有留過淚 可是他們錯了,其實那些人都
離去的第99天 謝謝妳,我得離開了,沒有辦法再陪伴妳了 我知道幾個月前妳就已經連離去 是我執著 是我放不下 如今淚水乾凅 也不知道能在為了甚麼悲傷 照片,禮物  我將它葬於火海 留下回憶 對不起,我走了
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
近幾年占星研究發展出的一個新領域,就跟「月週期」有關。這個「月週期」,是以每個月的新月為起點,跟星盤上的十二宮有點像,只不過它只有八等分而已。這個週期叫做月相(moon phase),從出生盤上的月相,可以看出靈魂此生的使命所在。
當背負著世界之惡 超人也終有一天會成為世界之惡,原因是世界容不下超人。曾經我們都懷抱著多大的報復,期許自己能在這社會的舞台大顯身手,然而這一切看似理想的夢想,卻在登上舞台的那一刻粉碎。 來自世界的嘲諷、自私、不諒解、甚至是傷害,從沒想過那個我期盼已久的舞台,成了讓我跌最慘傷最痛的地方。我開始害怕,因
Thumbnail
【Bluesound香港】致力為音響愛好者提供一個使用簡便,而且售價親民的串流播放方案。你想要簡單的系統,他們有主動式的無線喇叭,喇叭就是音響,非常簡單。倘若你已經有一套音響了,想為系統加上串流播放的功能,Bluesound Node 2i 就是你需要的。
Thumbnail
如果以提供一項服務,要對方接受;或是陳述一個概念,要對方買單,都算是廣義的銷售,那麼在醫院工作的我去上一堂銷售課也就不是一件稀奇的事了。 不上則已,一上驚為天人。
任由我放縱淚水 就在那個毫無預警地當下 我哭了 一個人安靜地坐在書桌旁 上一秒不是還在讀書嗎? 可是,為甚麼,我哭得好慘 彷彿有種難過的心情趨勢著我 我很累,面對現實我沒有站起來的信心 放縱了淚水,聲嘶力竭去訴說這不公平的世界 大家都以為,有些人很堅強無論何時都沒有留過淚 可是他們錯了,其實那些人都
離去的第99天 謝謝妳,我得離開了,沒有辦法再陪伴妳了 我知道幾個月前妳就已經連離去 是我執著 是我放不下 如今淚水乾凅 也不知道能在為了甚麼悲傷 照片,禮物  我將它葬於火海 留下回憶 對不起,我走了