Node應用內存泄漏分析方法論與實戰
Node應用內存泄漏分析方法論與實戰
本文發表於北斗同構github, 轉載請註明出處
註: 本文為第12屆D2前端技術論壇《打造高可靠與高性能的React同構解決方案》分享內容,已經過數據脫敏處理。
前言
菜鳥物流大市場是菜鳥旗下的一條業務線,可以簡單地理解為物流領域的淘寶,是為撮合物流需求方和物流提供方搭建的一個平台。其中搜索頁、詳情頁、買家中心等頁面是基於beidou同構框架開發的。隨著node、react同構等技術越來越廣泛地使用, 內存泄漏的事情時有發生,應當引起足夠的重視。最近在做菜鳥物流市場的技術支持,就「中獎」了,把實踐過程中的經驗和心得整理了下,供大家參考。
先介紹幾個基本術語:
- SSR: 服務端渲染, 簡而言之就是把頁面在服務端渲染好直接返回給瀏覽器以提升展示性能
- 同構: 在SSR的基礎上, 應用既可以在服務端渲染又可以在瀏覽器渲染,既一套代碼兩端運行。
- Beidou(北斗): 基於eggjs的react同構框架, 開源地址
- 內存泄漏: 是指程序中己動態分配的堆內存由於某種原因未釋放或無法釋放,通常是應用層不合理的邏輯代碼引起的。
- OOM: Out Of Memory, 簡單地說就是內存消耗完了,分配不出內存了。內存泄漏是導致OOM的最常見的因素。OOM導致的直接後果就是進程Crash掉。
- RSS: Resident Set Size 實際使用物理內存(包含共享庫佔用的內存)
案例分析
回到之前說到的菜鳥物流大市場
發現問題
菜鳥物流大市場上線之後,經常收到alimonitor的告警通知,如下圖

於是打開了alinode查看慢日誌, 果然有不少慢日誌記錄

分析&驗證&排查
分析
當時主要有以下幾個現象:
- 詳情頁面有時打開很快,有時打開需要4 - 5 秒
- 重啟之後會明顯變好, 響應速度很快,
- 機器負載採樣: CPU消耗很低、 內存消耗高達 53.5%
根據當時的現象做了簡單的分析並制定了具體的action:
- 響應很慢 --> 1) 可能HSF介面慢 2) 可能渲染慢 --> action: 分別打點記錄日誌
- 時快時慢 --> 可能不同的機器當前狀況不一樣導致響應速度差別很大--> action: 對比各機器負載情況
- 重啟後速度很快 --> 可能發生了某事件導致了性能變差,重點排查內存泄漏 --> action: 通過alinode堆快照分析
- CPU低、內存消耗高 --> 極有可能是內存泄漏 --> action: 通過alinode堆快照分析
從上面的推斷來看,發生內存泄漏的可能性非常大,但仍然需要通過實際數據進行驗證,於是根據制定的action進行數據採集
驗證
再次發布之後,採集到了數據:

從上圖中可以看出, 隨著時間的推移,進程1694的hsf調用耗時始終穩定,但是服務端渲染的時間卻逐步飆升到3700多毫秒,然後在某個臨界值之後瞬間降低到50毫秒左右。可能是由於某某事件( 猜測是內存泄漏引起OOM )導致了進程崩潰,接下來beidou框架會自動重啟進程又恢復良好的狀態。打開sandbox一看進程生命周期,果然如此, 進程1694掛了,然後重新啟動了一個29649進程。

從上圖中也可以看到RSS(實際使用物理內存)高達1880.93MB,至此基本上可以確定是內存泄漏了。查看內存佔用曲線,內存呈現鋸齒狀,先一路飆升,到達零界點之後瞬間下降,如此周而復始。和我們的推斷完全一致,這是典型的內存泄漏曲線。

最終結論: 訪問速度慢是因為內存泄漏消耗了過多的資源
排查
定位到是內存泄漏之後,還需要進一步排查具體是什麼代碼導致了內存泄漏。這時候就要用到排查神器 - alinode了。
先創建堆快照:

在分析頁面打開對象簇視圖, 可以看到裡面有大量的Window對象, 搜索下竟然高達390個

採樣了幾個Window對象,通過GC Root展開,發現掛載了無數個定時器。

分析代碼找到了兩處定時器的設置,看代碼邏輯,該定時器在服務端根本不會被釋放。
componentWillMount(){
let _this = this;
window.handler = window.setInterval(function(){
if(typeof AMap){
_this.renderMap(, AMap);
window.clearInterval(window.handler);
}
}, 300);
}
注釋掉之後在預發驗證沒有再出現window相關的內存泄漏。
PS.
後來的驗證發現,除了定時器的問題,還有另外兩處內存泄漏,不再贅述, 貼上其中一處(高德地圖)內存泄漏的代碼供讀者參考
componentWillMount(){
this.createAmapScript();
}
createAmapScript(){
let script = document.createElement(script),
body = document.getElementsByTagName(body)[0];
script.type = text/javascript;
script.src = https://webapi.amap.com/maps?v=1.3&key=59699a8cfee7c52f58390357cbdbf27d;
body.appendChild(script);
}
解決問題
從上述兩處代碼可以看出,定時器無需在服務端執行, 而高德地圖本身就不支持服務端渲染,因此可將二者放到客服端渲染即可。根據react的特性,componentDidMount生命周期函數在服務端不會執行,因此將上述代碼從componentWillMount移到componentDidMount中即可。具體修復如下:

通過loadtest在本地壓測驗證下:

單個進程同樣以10個QPS進行施壓,對比下可以看出,修復前RT時間一路上升,而修復後RT始終穩定在200毫秒左右。
再看看線上數據, 內存佔用率始終穩定,沒有出現飆升現象。

至此,打完收工。
方法論
看完了案例,是時候系統化地總結下方法論了。
現象
從剛才的案例中可以看出來,內存泄漏最典型的現象就是內存佔用率會隨著時間的推移而逐步上升,就算沒有流量了,內存佔用率也不會下降。而健康的應用是流量上升內存佔用會上升,而流量下降之後內存佔用率就會回到原水平。

原因
通常造成內存泄漏的有以下幾個因素
- 緩存
- 隊列消費不及時
- 作用域未釋放
本文中的案例就屬於作用域未釋放
解決方案
- 本地
- 通過loadtest壓測,觀察應用是否健康
- 如若出現異常,通過node-heapdump對v8堆內存抓取快照, 並通過chrome開發者工具profiles來導入快照進行分析。
- 線上
- 通過alimonitor、eagleeye等監控平台監控應用健康度
- 如若出現異常,通過alinode堆快照排查問題
- 如若異常難以復現,可以在預發 或者隔離某台線上機器進行壓測,壓測能夠有效放大問題
- 在壓測過程中,通過alinode堆快照排查問題
建議
- 最重要的一條:開發階段就壓測、開發階段就壓測、開發階段就壓測,重要的事情說三遍。古語云:
上醫治未病,中醫治欲病,下醫治已病,說的是醫術最高明的醫生並不是擅長治病的人,而是能夠預防疾病的人。讓問題在開發階段就暴露出來, 而不是等到線上告警了再搶救。 - 避免在
constructor中做事件綁定,建議放到componentDidMount生命周期中 - 不支持SSR的組件放到componentDidMount中,同理,createElement、appendChild等dom原生操作也放到componentDidMount中
- 其它詳見同構注意事項
推薦閱讀:
※狼叔的2017年總結
※如何分析 Node.js 中的內存泄漏
※node學習的第一步
※swagger-decorator:註解方式為 Koa2 應用動態生成 Swagger 文檔
