Node應用內存泄漏分析方法論與實戰

本文發表於 北斗同構github, 轉載請註明出處

注: 本文爲第12屆D2前端技術論壇《打造高可靠與高性能的React同構解決方案》分享內容,已通過數據脫敏處理。javascript

前言

菜鳥物流大市場是菜鳥旗下的一條業務線,能夠簡單地理解爲物流領域的淘寶,是爲撮合物流需求方和物流提供方搭建的一個平臺。其中搜索頁詳情頁、買家中心等頁面是基於beidou同構框架開發的。隨着node、react同構等技術愈來愈普遍地使用, 內存泄漏的事情時有發生,應當引發足夠的重視。最近在作菜鳥物流市場的技術支持,就「中獎」了,把實踐過程當中的經驗和心得整理了下,供你們參考。前端

先介紹幾個基本術語:java

  • SSR: 服務端渲染, 簡而言之就是把頁面在服務端渲染好直接返回給瀏覽器以提高展現性能
  • 同構: 在SSR的基礎上, 應用既能夠在服務端渲染又能夠在瀏覽器渲染,既一套代碼兩端運行。
  • Beidou(北斗): 基於eggjs的react同構框架, 開源地址
  • 內存泄漏: 是指程序中己動態分配的堆內存因爲某種緣由未釋放或沒法釋放,一般是應用層不合理的邏輯代碼引發的。
  • OOM: Out Of Memory, 簡單地說就是內存消耗完了,分配不出內存了。內存泄漏是致使OOM的最多見的因素。OOM致使的直接後果就是進程Crash掉。
  • RSS: Resident Set Size 實際使用物理內存(包含共享庫佔用的內存)

案例分析

回到以前說到的菜鳥物流大市場node

發現問題

菜鳥物流大市場上線以後,常常收到alimonitor的告警通知,以下圖react

因而打開了alinode查看慢日誌, 果真有很多慢日誌記錄git

分析&驗證&排查

分析

當時主要有如下幾個現象:github

  • 詳情頁面有時打開很快,有時打開須要4 - 5 秒
  • 重啓以後會明顯變好, 響應速度很快,
  • 機器負載採樣: CPU消耗很低、 內存消耗高達 53.5%

根據當時的現象作了簡單的分析並制定了具體的action:web

  • 響應很慢 --> 1) 可能HSF接口慢 2) 可能渲染慢 --> action: 分別打點記錄日誌
  • 時快時慢 --> 可能不一樣的機器當前情況不同致使響應速度差異很大--> action: 對比各機器負載狀況
  • 重啓後速度很快 --> 可能發生了某事件致使了性能變差,重點排查內存泄漏 --> action: 經過alinode堆快照分析
  • CPU低、內存消耗高 --> 極有多是內存泄漏 --> action: 經過alinode堆快照分析

從上面的推斷來看,發生內存泄漏的可能性很是大,但仍然須要經過實際數據進行驗證,因而根據制定的action進行數據採集chrome

驗證

再次發佈以後,採集到了數據:api

從上圖中能夠看出, 隨着時間的推移,進程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中
  • 其它詳見同構注意事項
相關文章
相關標籤/搜索