注: 本文為第12屆D2前端技術論壇《打造高可靠與高性能的React同構解決方案》分享內容,已經過數據脫敏處理。
本文發表于北斗同構github, 轉載請注明出處。
菜鳥物流大市場是菜鳥旗下的一條業務線,可以簡單地理解為物流領域的淘寶,是為撮合物流需求方和物流提供方搭建的一個平臺。其中搜索頁、詳情頁、買家中心等頁面是基于beidou同構框架開發的。隨著node、react同構等技術越來越廣泛地使用, 內存泄漏的事情時有發生,應當引起足夠的重視。最近在做菜鳥物流市場的技術支持,就“中獎”了,把實踐過程中的經驗和心得整理了下,供大家參考。
先介紹幾個基本術語:
SSR:服務端渲染,簡而言之就是把頁面在服務端渲染好直接返回給瀏覽器以提升展示性能。
同構:在SSR的基礎上, 應用既可以在服務端渲染又可以在瀏覽器渲染,即一套代碼兩端運行。
Beidou(北斗): 基于eggjs的react同構框架, 開源地址。
內存泄漏:是指程序中已動態分配的堆內存由于某種原因未釋放或無法釋放,通常是應用層不合理的邏輯代碼引起的。
OOM:Out Of Memory, 簡單地說就是內存消耗完了,分配不出內存了。內存泄漏是導致OOM的最常見的因素。OOM導致的直接后果就是進程Crash掉。
RSS:Resident Set Size 實際使用物理內存(包含共享庫占用的內存)。
案例分析
回到之前說到的菜鳥物流大市場。
發現問題
菜鳥物流大市場上線之后,經常收到alimonitor的告警通知,如下圖:
于是打開了 Node.js 性能平臺查看慢日志,果然有不少慢日志記錄:
分析&驗證&排查
分析
當時主要有以下幾個現象:
詳情頁面有時打開很快,有時打開需要 4 - 5 秒。
重啟之后會明顯變好, 響應速度很快。
機器負載采樣:CPU 消耗很低、內存消耗高達 53.5%。
根據當時的現象做了簡單的分析并制定了具體的action:
響應很慢 --> 1) 可能HSF接口慢 2) 可能渲染慢 --> action: 分別打點記錄日志
時快時慢 --> 可能不同的機器當前狀況不一樣導致響應速度差別很大--> action: 對比各機器負載情況
重啟后速度很快 --> 可能發生了某事件導致了性能變差,重點排查內存泄漏 --> action: 通過 Node.js 性能平臺堆快照分析
CPU低、內存消耗高 --> 極有可能是內存泄漏 --> action: 通過 Node.js 性能平臺堆快照分析
從上面的推斷來看,發生內存泄漏的可能性非常大,但仍然需要通過實際數據進行驗證,于是根據制定的action進行數據采集
驗證
再次發布之后,采集到了數據:
從上圖中可以看出, 隨著時間的推移,進程1694的hsf調用耗時始終穩定,但是服務端渲染的時間卻逐步飆升到3700多毫秒,然后在某個臨界值之后瞬間降低到50毫秒左右。可能是由于某某事件(猜測是內存泄漏引起OOM)導致了進程崩潰,接下來beidou框架會自動重啟進程又恢復良好的狀態。打開 sandbox 一看進程生命周期,果然如此, 進程 1694 掛了,然后重新啟動了一個 29649 進程。
從上圖中也可以看到RSS(實際使用物理內存)高達1880.93MB,至此基本上可以確定是內存泄漏了。查看內存占用曲線,內存呈現鋸齒狀,先一路飆升,到達臨界點之后瞬間下降,如此周而復始。和我們的推斷完全一致,這是典型的內存泄漏曲線。
最終結論: 訪問速度慢是因為內存泄漏消耗了過多的資源。
排查
定位到是內存泄漏之后,還需要進一步排查具體是什么代碼導致了內存泄漏。這時候就要用到排查神器 - Node.js 性能平臺了。
先創建堆快照:
在分析頁面打開對象簇視圖
, 可以看到里面有大量的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等監控平臺監控應用健康度。
如若出現異常,通過 Node.js 性能平臺堆快照排查問題。
如若異常難以復現,可以在預發或者隔離某臺線上機器進行壓測,壓測能夠有效放大問題。
在壓測過程中,通過 Node.js 性能平臺堆快照排查問題。
建議
最重要的一條:開發階段就壓測、開發階段就壓測、開發階段就壓測,重要的事情說三遍。古語云:
上醫治未病,中醫治欲病,下醫治已病
,說的是醫術最高明的醫生并不是擅長治病的人,而是能夠預防疾病的人。讓問題在開發階段就暴露出來, 而不是等到線上告警了再搶救。避免在
constructor
中做事件綁定,建議放到componentDidMount生命周期中。不支持SSR的組件放到componentDidMount中,同理,createElement、appendChild等dom原生操作也放到componentDidMount中。
其它詳見同構注意事項。