使人「頭大」的 Nodejs 內存泄露

線上環境竟然遇到了內存泄露,通過 3 天的摸索,算是解決了:javascript

更換 pm2 版本,由 v3.5.1 降爲 v3.4.1

對於這個結論仍是不太滿意,算是歪打正着。不過仍是記錄下這幾天積累的經驗,說不定對正在看此文的你會有幫助。html

前言(鼓勵)

有幸,我此次有充足的時間去排查,沒有其餘事情干擾。但線上出現內存泄漏,解決起來比業務代碼 bug 更難解決,那種頭皮發麻的難受。緣由以下:前端

  • 項目代碼複雜。排查「泄漏點」,猶如大海撈針
  • 開放的前端模式。顧及不暇的 node_modules 模塊
  • 無形的壓力。須要時間拿數據作佐證,時間等的越長,壓力越來

無論有沒有精力 or 能力,我認爲 解決內存泄漏的最佳實踐是:積極的心態 + 冷靜的問題定位。(沒錯,這是對目前沒有解決問題的你說的)java

內存增加的緣由

能夠看下這篇文章 Finding And Fixing Node.js Memory Leaks: A Practical Guidenode

  • 全局變量
  • 代碼緩存
  • 閉包
  • ...

Anyway,其實無論什麼緣由,主要仍是 各類引用 得不到 V8 GC 的釋放。扔一段很經典的代碼:python

function calc(data) {
  return Math.round((data / 1024 / 1024) * 100) / 100 + " MB";
}
function logger() {
  let mem = process.memoryUsage();
  console.log(new Date(), "memory now:", calc(mem.rss));
}
var theThing = null;
var replaceThing = function() {
  logger();
  var originalThing = theThing;
  var unused = function foo() {
    if (originalThing) {
      console.log("未被調用,但 originalThing 有個 someMethod 的引用");
    }
  };
  theThing = {
    longStr: new Array(1000000).join("*"),
    someMethod: function() {
      console.log("沒作任何事情,但我是閉包");
    }
  };
  console.log("parse");
};
setInterval(replaceThing, 10);

執行沒多久,就從 20M 飈到 幾百 M。究其緣由,仍是由於閉包引用沒有及時被銷燬。linux

具體緣由以下:c++

雖然 unused 沒有被調用,可是其中包含 originalThing 並指向 theThing ,theThing 在定義時有個 someMethod 方法,其就是個閉包(能夠訪問到 originalThing) ,該閉包在 unused 中因爲引用了 originalThing 一直沒有被釋放。git

層層嵌套的引用

對內存進行「檢查」

若是你對這塊有查詢過相關資料, heapdump 這模塊應該頻繁出現,這裏說下如何使用它來監控項目的內存狀況。固然還有 memwatch ...github

安裝

npm install heapdump -S

若是你足夠幸運,確定會出現以下問題:

error: #error This version of node/NAN/v8 requires a C++11 compiler

原版本太低,須要更新 linux 系統的 gcc 等相關庫。

參考以下安裝說明:

# 安裝 repo 倉庫
wget http://people.centos.org/tru/devtools-2/devtools-2.repo
mv devtools-2.repo /etc/yum.repos.d

# 安裝新庫
yum install devtoolset-2-gcc devtoolset-2-binutils devtoolset-2-gcc-c++

# 備份原庫
mv /usr/bin/gcc /usr/bin/gcc-4.4.7
mv /usr/bin/g++ /usr/bin/g++-4.4.7
mv /usr/bin/c++ /usr/bin/c++-4.4.7

# 建立快捷方式,將新庫鏈到須要目錄
ln -s /opt/rh/devtoolset-2/root/usr/bin/gcc /usr/bin/gcc
ln -s /opt/rh/devtoolset-2/root/usr/bin/c++ /usr/bin/c++
ln -s /opt/rh/devtoolset-2/root/usr/bin/g++ /usr/bin/g++

# 檢查版本,肯定生效
gcc --version

固然系統是 window 可能還會有更坑的問題:安裝 node-gyp、python 出錯。

推薦以下 npm 模塊:

windows-build-tools

一鍵安裝相關組件依賴(咱們只要靜靜的等待,由於時間有些久)。他會幫你安裝 window 對應的 NET Framework,python 這些插件。

npm install --global --production windows-build-tools

使用

線上很簡單的作了一個控制檯輸出,用於定位問題:

var heapdump = require("heapdump");
let startMem = process.memoryUsage();

function calc(data) {
  return Math.round((data / 1024 / 1024) * 10000) / 10000 + " MB";
}
// 使用的是 koa
router.all("/foo", async (ctx, next) => {
  let mem = process.memoryUsage();
  logger.debug("memory before", calc(startMem.rss), "memory now:", calc(mem.rss), "diff increase", calc(mem.rss - startMem.rss));
  // ...
});

這樣就能實時看到系統的內存消耗(對比剛啓動時):

2019-09-06 16:30 +08:00: [2019-09-06T16:30:06.700] [DEBUG] transfer - memory before 55.5898 MB memory now: 95.1484 MB diff increase 39.5586 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:07.724] [DEBUG] transfer - memory before 56.2148 MB memory now: 69.8438 MB diff increase 13.6289 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:10.406] [DEBUG] transfer - memory before 56.2148 MB memory now: 70.5977 MB diff increase 14.3828 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:11.018] [DEBUG] transfer - memory before 55.5898 MB memory now: 95.4219 MB diff increase 39.832 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:12.827] [DEBUG] transfer - memory before 55.5898 MB memory now: 95.6797 MB diff increase 40.0898 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:12.952] [DEBUG] transfer - memory before 55.5898 MB memory now: 94.9688 MB diff increase 39.3789 MB

添加個快照路由,在按照須要抓取內存此刻使用狀況:

router.all("/snapshot", async (ctx, next) => {
  heapdump.writeSnapshot("./dump-" + Date.now() + ".heapsnapshot", function(err) {
    if (err) console.error(err);
  });
});

導入到 chrome 的 profile 面板中,對比先後兩個文件的變化,定位問題

圖片描述

一切順利就能很快定位到問題代碼。但實際要更困難,更摸不着頭腦。

猜想的可能點

若是按照上述的「檢查」操做仍是沒有定位到問題點,可能這段會對你有所幫助。

首先要知道本身着手的項目的用途,所用技術,它對你排查問題更有指向意義。

好比:此項目是基於 node 的中間層服務,對 api 接口進行轉換。用於由後端 api 服務的「升級」平滑各客戶端的發版時差。(服務可能在 api 調用上有性能瓶頸?)

技術棧:sequelize + koa + pm2 (熟悉項目的主要框架,從大技術方向着手)

有幸有個 項目 B 和此項目相似,技術上略有差別。

綜上所述,就猜想了幾個可能的 內存泄露 緣由(附參考文章):

驗證

可能緣由 測試方式 驗證結果 備註
代碼問題 ab 壓測 ok 但需增長重視
sequlize 版本問題 ab 壓測 ok 暫不嘗試。目前使用 4.42.0,線上沒法承擔更換版本的風險
Ladash _.template ab 壓測 ok 保持現狀
log4js 有背壓問題 ab 壓測 ok pm2 & log4js 使用不夠友好,在有替代方案前(好比 winston),保持現狀
pm2 版本問題 線上下降版本 3.5.1 -> 3.4.1 待驗證 項目 B 使用 3.4.1

結論

這兩天從線上狀況上看,修改 pm2 版本後,內存泄漏獲得控制。下面由此結論反推驗證步驟:

  1. 使用 devtools ,發現一天的內存差別,TIMERWRAP 指標突增:

    profile

    TIMERWRAP 是 Node 裏 Timer 相關定義,猜想是否有定時器在有規律的無限刷新佔用性能。

  2. 繼續查看,發現 pm2 有些「格格不入」

    profile

    metrics 心跳檢測,是否有 setTimeout 之類的代碼?

  3. 查看資料,發現相關 issus 中說代碼有 leak

    @pm2/io

    查看線上項目相關代碼,的確有這段問題代碼:

    @pm2/io

    對於爲什麼這段 if/else 會形成內存泄漏,有空再研究下。估計相似 dom 的 event 綁定沒有解除所致。

參考

我只是知識點的「加工者」, 更多內容請查閱原文連接 , 同時感謝原做者的付出:

關於我

若是你以爲這篇文章對你有幫助, 請點個或者分享給更多的道友。

也能夠掃碼關注個人 微信訂閱號 - [ 前端雨爸 ], 第一時間收到技術文章 , 工做之餘我會持續輸出

bVbtL5R?w=258&h=258

最後感謝閱讀, 大家的支持是我寫做的最大動力

相關文章
相關標籤/搜索