欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一次NodeJS內存泄漏排查的實戰(zhàn)記錄

 更新時間:2022年03月24日 09:51:50   作者:秋水  
這篇文章主要給大家介紹了一次NodeJS內存泄漏排查的實戰(zhàn)記錄,文中給出了詳細的排查過程以及內存泄漏的解決方法,大家可以學習一下以備不時之需,需要的朋友可以參考下

前言

性能問題(內存、CPU 飆升導致服務重啟、異常)排查一直是 Node.js 服務端開發(fā)的難點,去年在經過調研后,在我們項目的 Node.js 服務上都接入了 Easy-Monitor 來幫助排查生產環(huán)境遇到的性能問題。前段時間遇到了兩例內存泄漏的案例,在這里做一個排查經過的整理。

案例一

故障現象

線上的某個服務發(fā)生了重啟,經過觀察 Grafana 得到,該服務在 5 天內內存持續(xù)上漲到達 1.3G+ 從而觸發(fā)了自動重啟。

排查過程

在內存處于高點時抓取了內存快照,在 Easy-Monitor 平臺上進行分析。

圖1

在圖一中能夠看到抓取內存快照的時候 V8 堆內有 1273 個 TCP 對象沒有被釋放從而導致了內存的上漲。接著,我們需要排查具體是哪里發(fā)生了內存泄漏。

圖2

圖二是根據第一個 TCP 對象的內存地址進行搜索得到的結果。簡單點來說:

  • Edge 視圖展示了這個數據擁有的子數據結構。
  • Retainer 視圖展示了這個對象被那些數據結構引用。

我們排查問題的思路就是從泄漏對象出發(fā),一級級向上搜索,直到找到我們眼熟的數據結構來確定是哪一段代碼導致了內存泄漏。

熟悉 Node.js 的同學應該知道 TCP 對象是被 Socket 對象持有的,所以接下來搜索 Socket@328785 這個地址。

圖3

在 Retainer 視圖里顯示 SMTPConnection._socket 指向了我們搜索的 socket 地址,而 SMTP 很明顯和發(fā)送郵件相關,這里我們將問題的范圍縮小到了 node-mailer 這個包上。

圖4

搜索圖三中 Retainer 視圖中的 SMTPConnection@328773,結果如圖4。 SMTPConnection@328773 又指向了 system/Context (上下文)中的 connection@328799 對象。

圖5

從圖5中能看到,這個上下文包含 connection、sendMessage、socketOptions、returned、connection 這些數據結構,經過對 node-mailer 源碼的研究,我們能夠通過這個上下文對象定位到下面中的代碼片段。this.getSocket 函數的回調函數的執(zhí)行上下文即 system/Context@328799,回調函數中的 var connection = new SMTPConnection(options); 就是產生泄漏的對象。

/**
 * Sends an e-mail using the selected settings
 *
 * @param {Object} mail Mail object
 * @param {Function} callback Callback function
 */
SMTPTransport.prototype.send = function (mail, callback) {
    this.getSocket(this.options, function (err, socketOptions) {
        if (err) {
            return callback(err);
        }

        var options = this.options;
        if (socketOptions && socketOptions.connection) {
            this.logger.info('Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
            // only copy options if we need to modify it
            options = assign(false, options);
            Object.keys(socketOptions).forEach(function (key) {
                options[key] = socketOptions[key];
            });
        }

        // 這里的 connection 沒有被釋放。
        var connection = new SMTPConnection(options);
        var returned = false;

        connection.once('error', function (err) {
            if (returned) {
                return;
            }
            returned = true;
            connection.close();
            return callback(err);
        });

        connection.once('end', function () {
            if (returned) {
                return;
            }
            returned = true;
            return callback(new Error('Connection closed'));
        });

        var sendMessage = function () {
            var envelope = mail.message.getEnvelope();
            var messageId = (mail.message.getHeader('message-id') || '').replace(/[<>\s]/g, '');
            var recipients = [].concat(envelope.to || []);
            if (recipients.length > 3) {
                recipients.push('...and ' + recipients.splice(2).length + ' more');
            }

            this.logger.info('Sending message <%s> to <%s>', messageId, recipients.join(', '));

            connection.send(envelope, mail.message.createReadStream(), function (err, info) {
                if (returned) {
                    return;
                }
                returned = true;

                connection.close();
                if (err) {
                    return callback(err);
                }
                info.envelope = {
                    from: envelope.from,
                    to: envelope.to
                };
                info.messageId = messageId;
                return callback(null, info);
            });
        }.bind(this);

        connection.connect(function () {
            if (returned) {
                return;
            }

            if (this.options.auth) {
                connection.login(this.options.auth, function (err) {
                    if (returned) {
                        return;
                    }

                    if (err) {
                        returned = true;
                        connection.close();
                        return callback(err);
                    }

                    sendMessage();
                });
            } else {
                sendMessage();
            }
        }.bind(this));
    }.bind(this));
};

為什么這里創(chuàng)建的 connection 會無法釋放,這個問題留到文章末尾再揭開答案。

案例二

故障現象

線上某個服務在啟動后在短時間(4 小時左右)內存就達到了上限發(fā)生了重啟。

排查過程

同樣在高點抓取了內存快照進行分析。

圖6

在圖6中能看到是因為 TLSSocket 沒有釋放導致了內存泄漏,查詢第一個TLSSocket@4531505。

圖7

圖7中可以看到又指向了 SMTPConnection,由于在案例 1 排查問題的時候已經研究過 node-mailer 包了,所以知道這里的 TLSSocket 是郵箱服務在連接的時候一些通信會使用 TLSSocket。于是接著看查詢SMTPConnection@4531545。

圖8

在圖8中,我們能夠看到 535 的報錯信息,在我們的業(yè)務代碼中,對 535 報錯設置了重試機制(調用 node-mailer 的 api 關閉舊的連接,然后重新發(fā)送),但是這里很明顯舊的連接并沒有被成功關閉。

問題原因

上文中的兩個案例都是因為 Socket/TLSSocket 無法釋放導致的,通過查看 node-mailer 源碼,可以發(fā)現無論是 Socket 發(fā)送郵件成功還是 TLSSocket 報錯后都會調用 SMTPConnection.close(),并最終調用 socket.end() 或者 TLSSocket.end() 來釋放連接。 看了很多源碼才發(fā)現原來問題出在了node-mailer 的版本和 Node.js 的版本問題上。項目中使用的node-mailer版本是比較早的 2.7.2 版本,支持 Node.js 版本也比較低,而 node-v10.x 后調整了流相關的實現邏輯,我們的線上環(huán)境最近也從 node-v8.x 升級到了 node-v12.x ,所以產生了上文中的兩個問題。

node-v9.x 以下的版本

node-v9.x(包括 9.x)以下版本在調用 socket.end() 后會同步調用 TCP.close() 直接銷毀連接。

Socket.prototype.end = function(data, encoding) {
 // 調用雙工流(可寫流)的 end 函數會觸發(fā) finish 事件。
  stream.Duplex.prototype.end.call(this, data, encoding);
  this.writable = false;
  // just in case we're waiting for an EOF.
  if (this.readable && !this._readableState.endEmitted)
    this.read(0);
  else
    maybeDestroy(this);
};

function maybeDestroy(socket) {
  if (!socket.readable &&
      !socket.writable &&
      !socket.destroyed &&
      !socket.connecting &&
      !socket._writableState.length) {
    // 這里調用的也是可寫流的 destroy 函數
    socket.destroy();
  }
}

// 可寫流 destroy 函數
function destroy(err, cb) {
   // 省略其余代碼
   // destroy 函數會調用 socket._destroy。
  this._destroy(err || null, (err) => {
    if (!cb && err) {
      process.nextTick(emitErrorNT, this, err);
      if (this._writableState) {
        this._writableState.errorEmitted = true;
      }
    } else if (cb) {
      cb(err);
    }
  });
}

Socket.prototype._destroy = function(exception, cb) {
  this.connecting = false;
  this.readable = this.writable = false;
  if (this._handle) {
    this[BYTES_READ] = this._handle.bytesRead;
    // this._handle = TCP(),調用TCP.close函數來關閉連接。
    this._handle.close(() => {
      debug('emit close');
      this.emit('close', isException);
    });
    this._handle.onread = noop;
    this._handle = null;
    this._sockname = null;
  }

  if (this._server) {
    COUNTER_NET_SERVER_CONNECTION_CLOSE(this);
    debug('has server');
    this._server._connections--;
    if (this._server._emitCloseIfDrained) {
      this._server._emitCloseIfDrained();
    }
  }
};

node-v10.x 以上的版本

// socket 實現了Duplex,end 函數直接調用了 writableStream.end
Socket.prototype.end = function(data, encoding, callback) {
  stream.Duplex.prototype.end.call(this, data, encoding, callback);
  DTRACE_NET_STREAM_END(this);
  return this;
};

// _stream_writable.js
// writableStream.end 最終會調用如下函數
function finishMaybe(stream, state) {
  const need = needFinish(state);
  if (need) {
    prefinish(stream, state);
    if (state.pendingcb === 0) {
      state.finished = true;
      stream.emit('finish');

      // 這里的 state 存放可讀流的狀態(tài)變量
      // @node10 新增:autoDestroy 標志流是否在調用 end()后自動調用自身的 destroy,在 v12 版本默認是 false。v14 版本開始默認為 true。
      // 所以當我們調用 socket.end()的時候,不會立刻銷毀自己,僅僅會觸發(fā) finish 事件。
      if (state.autoDestroy) {
        const rState = stream._readableState;
        if (!rState || (rState.autoDestroy && rState.endEmitted)) {
          stream.destroy();
        }
      }
    }
  }
  return need;
}

// 那么 socket 什么時候會被銷毀呢?
// socket 構造函數
function Socket(options) {
     // 忽略
     // 注冊了end事件,觸發(fā)的時候這個函數會調用自己的 destroy。
     this.on('end', onReadableStreamEnd);
}

function onReadableStreamEnd() {
  // 省略
  if (!this.destroyed && !this.writable && !this.writableLength)
    // 同樣會調用可寫流的 destroy 然后調用 socket._destory()
    this.destroy();
}

// Socket 的 end 事件是可讀流 read()的時候觸發(fā)的。
// n 參數指定要讀取的特定字節(jié)數,如果不傳,每次返回內部buffer中的全部數據。
Readable.prototype.read = function(n){
  const state = this._readableState;

  // 計算可以從緩沖區(qū)中讀取多少數據。
  n = howMuchToRead(n, state);

  // 本次可以讀取的字節(jié)數為0
  // 流內部緩沖區(qū)buffer中的字節(jié)數為0
  // 可讀流的 ended 狀態(tài)為 true
  if (n === 0 && state.ended) {
    if (state.length === 0)
      // 結束自己
      endReadable(this);
    return null;
  }
}

function endReadable(stream) {
  const state = stream._readableState;
  debug('endReadable', state.endEmitted);
  if (!state.endEmitted) {
    state.ended = true;
    process.nextTick(endReadableNT, state, stream);
  }
}

function endReadableNT(state, stream) {
  debug('endReadableNT', state.endEmitted, state.length);
  if (!state.endEmitted && state.length === 0) {
    state.endEmitted = true;
    stream.readable = false;
    // 觸發(fā) stream(socket)的 end 事件。
    stream.emit('end');

    //這里和可寫流一樣也有個 autoDestroy 參數,同樣是默認 false。
    if (state.autoDestroy) {
      // In case of duplex streams we need a way to detect
      // if the writable side is ready for autoDestroy as well
      const wState = stream._writableState;
      if (!wState || (wState.autoDestroy && wState.finished)) {
        stream.destroy();
      }
    }
  }
}

線上環(huán)境的 node-v12.x 版本中,由于 autoDestroy 默認是 false,所以在調用 socket.end() 的時候并不會同步的摧毀流,而是依賴 socket.read() 時觸發(fā) end 事件,然而在低版本的 node-mailer 實現邏輯里,會移除 socket 所有的監(jiān)聽器,所以也就導致了在 node-v12.x 環(huán)境下永遠無法觸發(fā) socket.destroy() 來銷毀連接。

SMTPConnection.prototype._onConnect = function () {
    // 省略
    // clear existing listeners for the socket
    this._socket.removeAllListeners('data');
    this._socket.removeAllListeners('timeout');
    this._socket.removeAllListeners('close');
    this._socket.removeAllListeners('end');
     // 省略
};

修復泄露

通過上述排查過程,從根因上找到了生產環(huán)境中 node-v12.x 運行低版本的 node-mailer 產生內存泄露的原因,那么要解決此問題也變得非常簡單。

通過升級 node-mailer 的版本以支持 node-v12.x ,困擾多時的線上內存泄露問題至此完美解決。

總結

到此這篇關于一次NodeJS內存泄漏排查的文章就介紹到這了,更多相關NodeJS內存泄漏排查內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Node.js 阻塞與非阻塞的實現

    Node.js 阻塞與非阻塞的實現

    本文主要介紹了Node.js中阻塞和非阻塞調用之間的區(qū)別,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-05-05
  • node.js學習之斷言assert的使用示例

    node.js學習之斷言assert的使用示例

    assert 模塊主要用于編寫程序的單元測試時使用,通過斷言可以提早發(fā)現和排查出錯誤。下面這篇文章主要給大家介紹了關于node.js學習之斷言assert的相關資料,需要的朋友可以參考借鑒,下面隨著小編來一起學習學習吧。
    2017-09-09
  • Thinkjs3新手入門之添加一個新的頁面

    Thinkjs3新手入門之添加一個新的頁面

    Thinkjs 是一個快速、簡單的基于MVC和面向對象的輕量級Node.js開發(fā)框架,下面這篇文章主要給大家介紹了關于Thinkjs3新手入門之添加一個新的頁面的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下。
    2017-12-12
  • Node.js?express中的身份認證的實現

    Node.js?express中的身份認證的實現

    本文主要介紹了Node.js?express中的身份認證的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-01-01
  • node.js解決獲取圖片真實文件類型的問題

    node.js解決獲取圖片真實文件類型的問題

    這篇文章主要介紹了node.js解決獲取圖片真實文件類型的問題,本文根據二進制流及文件頭獲取文件類型mime-type,然后讀取文件二進制的頭信息,獲取其真實的文件類型,需要的朋友可以參考下
    2014-12-12
  • Node.js 使用request模塊下載文件的實例

    Node.js 使用request模塊下載文件的實例

    今天小編就為大家分享一篇Node.js 使用request模塊下載文件的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-09-09
  • node.js平臺下利用cookie實現記住密碼登陸(Express+Ejs+Mysql)

    node.js平臺下利用cookie實現記住密碼登陸(Express+Ejs+Mysql)

    這篇文章主要介紹了node.js平臺下利用cookie實現記住密碼登陸(Express+Ejs+Mysql),具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-04-04
  • Centos6.8下Node.js安裝教程

    Centos6.8下Node.js安裝教程

    這篇文章主要為大家詳細介紹了Centos6.8下Node.js安裝教程,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-05-05
  • nodejs讀取圖片返回給瀏覽器顯示

    nodejs讀取圖片返回給瀏覽器顯示

    這篇文章主要為大家詳細介紹了nodejs讀取圖片返回給瀏覽器顯示,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-07-07
  • 解決Nodejs全局安裝模塊后找不到命令的問題

    解決Nodejs全局安裝模塊后找不到命令的問題

    今天小編就為大家分享一篇解決Nodejs全局安裝模塊后找不到命令的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-05-05

最新評論