新聞中心
最近又用node寫了一個小工具,需要常駐進程,經(jīng)過幾天的觀察,發(fā)現(xiàn)內(nèi)存占用有持續(xù)增加的趨勢(雖然不明顯,但還是讓我察覺到了,我真屌)。突然發(fā)現(xiàn),我竟然不知道怎么排查nodejs的內(nèi)存泄漏,嚇死寶寶了!

十年的上饒網(wǎng)站建設(shè)經(jīng)驗,針對設(shè)計、前端、開發(fā)、售后、文案、推廣等六對一服務,響應快,48小時及時工作處理。成都營銷網(wǎng)站建設(shè)的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整上饒建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計,從而大程度地提升瀏覽體驗。成都創(chuàng)新互聯(lián)從事“上饒網(wǎng)站設(shè)計”,“上饒網(wǎng)站推廣”以來,每個客戶項目都認真落實執(zhí)行。
花時間看了一下相關(guān)資料(google真好,外果仁真屌),看來這部分也已經(jīng)有比較完善的方法論+工具了。所以這篇文章記錄一下自己從不懂到入門的經(jīng)歷~~
我希望這篇文章不僅能提供具體的工具供大家使用,還提供足夠的理論知識來輔助大家思考,當然,也可能是我自己想多了~~哇哈
發(fā)現(xiàn)問題
由于沒有太多運維經(jīng)驗,也不知道啥逆天的工具來幫我一鍵式監(jiān)控所需要的指標,如果你和我情況一樣,那我們只能手動來造個簡陋的但夠用的監(jiān)控腳本了。
別告訴我你和我一樣shell也不熟,直接就node吧。少廢話~
先裝上pm2,然后寫一個腳本,來定時打印目標應用的內(nèi)存使用率,當然,前提是目標應用也都放在pm2中管理。
- const exec = require('child_process').exec;
- var Later = require('later');
- var schedule = Later.parse.text('every 5 mins'); // 每5分鐘正點觸發(fā)
- Later.setInterval(function(){
- exec('pm2 jlist', { // 打印出pm2中應用的基本狀態(tài)信息,輸出是json字符串
- timeout: 2000
- }, (err, data, stderr)=>{
- if (err) {
- console.error(err, err.stack); // an error occurred
- return;
- }
- //將結(jié)果寫入日志
- data = JSON.parse(data);
- if(data[0]){ // 這里取0是因為我希望監(jiān)控的應用在pm2中的順序是第一位
- console.log(data[0].monit.memory/(1024*1024)); // 直接輸出到pm2的log中
- }
- });
- }, schedule);
然后就等一段時間,就會在對應的log文件中拿到相關(guān)的內(nèi)存數(shù)據(jù),然后只需要用電子表格生成一個圖標即可,我推薦使用google drive的spreadsheet:
上面的圖是我收集了大概2天的內(nèi)存數(shù)據(jù)繪制成的圖標,可以看出內(nèi)存使用量成上升趨勢。沒錯,就是泄漏了!!
友情提醒:修復內(nèi)存泄漏可能會耗時很久,你最好先找一個臨時方案來維持生計,例如定期重啟程序。
搭建環(huán)境
本著實戰(zhàn)為主的策略,我們先從搭建內(nèi)存泄漏監(jiān)控環(huán)境開始。剛開始參考 node-memory-leak-tutorial ,以為會很順利搭建好的,不過碰到了 這個error 。看Issus應該是個很常見的錯誤,按照別人的解決方案,嘗試 切換成 nodejs 6.3.1 版本 進行了測試,確實可以繞過那個錯誤:
- // 在項目目錄下
- node-debug leak.js
然后終端會啟動你的chrome,并停在代碼的斷點位置,深吸一口氣你就可以點擊執(zhí)行了。
備注:若遇到無法創(chuàng)建快照問題,需要多刷新幾次喲~
其它工具我也順便試了一下:
- node-memwatch
- node-webkit-agent
- node-heapdump
因為它們都需要根據(jù)操作系統(tǒng)進行編譯,我的本地環(huán)境是 win7 64bit ,這并不是一個理想的nodejs環(huán)境,至少我這么認為,否則也不會碰到惡心的“.net framework”問題。我勸大家千萬別學我輕易的就刪除了 .net framework 3.5 這個安裝包,因為這是win7自帶的,刪了以后就裝不上了,而裝更新的4.0+版本的話我這邊很重要的一個軟件就無法運行了(翻墻你懂的)。 在Windows 7系統(tǒng)上安裝.NET Framework 3.5框架 很不容易的!建議可以用上docker來搭建一個專門用來分析用的容器,這里我就不折騰下去了,its your turn~~
nodejs內(nèi)存分析的理論姿勢
在開始聽我正兒八經(jīng)胡說八道之前,推薦你先看幾個文檔:
- Memory Terminology
- How to Record Heap Snapshots
- Debugging Memory Leaks in Node.js Applications
- Simple Guide to Finding a JavaScript Memory Leak in Node.js
- Understanding Garbage Collection and hunting Memory Leaks in Node.js
- writing fast memory efficient javascript
- Error Handling in Node.js
一次性看完這些,可能要花很久,如此貼心的我已經(jīng)幫你看過了,根據(jù)我的理解,總結(jié)如下:
- javascript的v8內(nèi)存管理和java jvm類似,都有新生代(To-Space and From-Space),老年代等;
- 排查內(nèi)存泄漏需要分析內(nèi)存快照,可以使用已有的工具以devtool的profile面板或代碼的方式創(chuàng)建snapshot;
- 創(chuàng)建的快照文件可以導入devtool的profile進行分析;
- 快照生成的最佳實踐是:先保證程序已經(jīng)預熱,然后進行快照1(先觸發(fā)GC),然后對程序進行一些交互(例如:對于web服務即http請求),再次創(chuàng)建快照2,如此循環(huán)來生成多個版本的快照;
- 合理的利用devtool的profile提供的功能,正確的選擇視圖;
- 理解profie中的字段含義:
- 對象上的黃色標識表示的是javascript直接引用,紅色表示間接依賴引用,不太需要關(guān)注的是無底色對象,其代表被其它資源引用(如:natvie code);
- profile會根據(jù)對象的構(gòu)造方法對對象進行分組歸類,每個組對應的“Shallow Size”表示的是該組對象的直接內(nèi)存占用大小(例如:該類對象自身的原始類型數(shù)據(jù)的內(nèi)存占用),對應的“Retained Size”表示的是該組對象依賴的其它對象而造成的內(nèi)存占用總數(shù)(等于自身的Shallow Size + 依賴對象的Shallow Size [ + 依賴對象的依賴對象的Shallow Size [ + 遞歸下去]]);
- 由于性能原因,profile中不會顯示對象的整型類型的屬性,但是它們并沒有丟失,僅僅是工具沒有顯示出來而已。
- 應該警惕“distance”比較大或比較小的對象,總之和其它同類型對象的distance不一樣就意味著可能有問題;
- 盡量不要用匿名函數(shù),函數(shù)有名字會讓分析更容易,其實更推薦的是使用OOP,這樣會最容易定位需要追蹤的變量,畢竟都是構(gòu)造器創(chuàng)建出來的嘛;
- 閉包(匿名函數(shù),定時器等)創(chuàng)建的上下文引用很容易造成不易察覺的內(nèi)存泄漏;
- console的相關(guān)函數(shù)(log, error等)在實際分析中發(fā)現(xiàn)其引用的變量無法釋放,可以參考 #1741 ,所以你可以在測試代碼中替換掉console的相關(guān)函數(shù)(這樣你就不需要改動被測代碼邏輯了);
- 對象上的事件監(jiān)聽器的閉包最容易造成泄漏,即便是使用once,也可能一次都沒有觸發(fā)而導致該回調(diào)函數(shù)無限期引用數(shù)據(jù)。
ok,一大堆姿勢足夠你花很久時間閱讀了。不過并不是說你看了這些內(nèi)容,就可以輕松戰(zhàn)勝困難了。還有一個環(huán)節(jié)我們沒有討論: 若你的項目足夠復雜(大),那要怎么搭建項目的測試環(huán)境呢?
這里我認為,大概需要按照下面的步驟來做:
- 將完整的項目拆解成獨立的不同塊,并為每個拆解后的小模塊寫測試代碼
- 針對定時器相關(guān)的邏輯,最好改成手動觸發(fā),或利用測試庫(sinonjs)模擬時間片段
- 初期可以先盡可能排除依賴的第三方庫,最后酌情去測試它們(如果你懷疑是它們的問題的話)
- 低級別異常偽造(例如socket,file等)要靠偽造對應方法(不推薦使用sinonjs.stubs,因為它保存每次調(diào)用時的參數(shù)數(shù)據(jù),影響你觀察,不妨試試 mockery )
- 最終還是有必要放在線上環(huán)境實測一段時間來觀測問題是否真的修復了
我們主要來說一下第5條,其意味著你要在線上環(huán)境以debug的形式啟動程序,并想辦法導出快照到本地來分析。下面來看看怎么做:
首先,你給線上環(huán)境中安裝 v8-profiler 庫,它用來提供創(chuàng)建快照的功能。
然后,看一下下面的這段樣板代碼,其意義在于在你的項目中加載 v8-profiler 庫,并提供一個對外指令用來通知它創(chuàng)建快照文件。
- var fs = require('fs');
- var profiler = require('v8-profiler');
- // ---------------
- // 測試目標
- functionLeakingClass(){}
- var leaks = [];
- setInterval(function(){
- for(var i = 0; i < 100; i++){
- leaks.push(new LeakingClass);
- }
- console.error('Leaks: %d', leaks.length);
- }, 1000);
- // ---------------
- // 指令服務
- var koa = require('koa');
- var route = require('koa-route');
- var service = koa();
- var snapshotNum = 1; // 用于為生成的快照進行編號
- service.use(route.get('/snapshot', function*(){
- var response = this;
- var snapshot = profiler.takeSnapshot();
- snapshot.export(function(error, result){
- fs.writeFileSync((snapshotNum++) + '.heapsnapshot', result);
- snapshot.delete();
- response.body = 'done';
- });
- }));
- service.listen(2333, '127.0.0.1'); // 推薦綁定內(nèi)網(wǎng)ip,不要允許外網(wǎng)訪問該服務
每次請求 http://127.0.0.1:2333/snapshot ,你都會在項目根目錄生成一個快照文件,然后把它下載到本地磁盤就可以在chrome里隨時進行分析了。
總結(jié)
在實際排查過程中,發(fā)現(xiàn)最難測試的還是依賴的第三方庫的泄漏問題。畢竟你無法理解它們的實現(xiàn)。但不可能所有邏輯都自己來完成,所以面對各種各樣的第三方類庫,還是建議選擇盡可能權(quán)威的,主流的。剩下那些很小的功能模塊,就只能花時間研讀其實現(xiàn)代碼了。
如果你的業(yè)務采用了生產(chǎn)消費者模式, 你的測試腳本一定要保證任務的生產(chǎn)和消費的速率保持同速率(或者干脆確保消費者處理完一批次的任務耗時一定要大于批次創(chuàng)建的間隔時間) ,不然由于任務得不到處理,必然會產(chǎn)生任務積累,看起來就好像有內(nèi)存泄漏一樣,但其實這種情況其實是合理的,只是說明你的消費者太少了而已。
另外,一定要最大頻度的,盡可能長時間的運行測試代碼,才能明顯的暴露出問題,例如:
- setInterval(functionmemoryleakBlock(){
- // 待測試的代碼塊
- }, 100);
注意在上面 memoryleakBlock 中避免引用全局變量喲,這樣你運行一夜,第二天上班來看結(jié)果(如果它還跑著的話)。
本文名稱:看我如何搞定Nodejs內(nèi)存泄漏問題
分享網(wǎng)址:http://www.dlmjj.cn/article/djgesgs.html


咨詢
建站咨詢
