最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

前言

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

案例一

故障現(xiàn)象

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

排查過程

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

圖1

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

圖2

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

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

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

熟悉 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 這些數(shù)據(jù)結構,經(jīng)過對 node-mailer 源碼的研究,我們能夠通過這個上下文對象定位到下面中的代碼片段。this.getSocket 函數(shù)的回調(diào)函數(shù)的執(zhí)行上下文即 system/Context@328799,回調(diào)函數(shù)中的 var connection = new SMTPConnection(options); 就是產(chǎn)生泄漏的對象。

/**
 * 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 會無法釋放,這個問題留到文章末尾再揭開答案。

案例二

故障現(xiàn)象

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

排查過程

同樣在高點抓取了內(nèi)存快照進行分析。

圖6

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

圖7

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

圖8

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

問題原因

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

node-v9.x 以下的版本

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

Socket.prototype.end = function(data, encoding) {
 // 調(diào)用雙工流(可寫流)的 end 函數(shù)會觸發(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) {
    // 這里調(diào)用的也是可寫流的 destroy 函數(shù)
    socket.destroy();
  }
}

// 可寫流 destroy 函數(shù)
function destroy(err, cb) {
   // 省略其余代碼
   // destroy 函數(shù)會調(diào)用 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(),調(diào)用TCP.close函數(shù)來關閉連接。
    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 實現(xiàn)了Duplex,end 函數(shù)直接調(diào)用了 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 最終會調(diào)用如下函數(shù)
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 標志流是否在調(diào)用 end()后自動調(diào)用自身的 destroy,在 v12 版本默認是 false。v14 版本開始默認為 true。
      // 所以當我們調(diào)用 socket.end()的時候,不會立刻銷毀自己,僅僅會觸發(fā) finish 事件。
      if (state.autoDestroy) {
        const rState = stream._readableState;
        if (!rState || (rState.autoDestroy && rState.endEmitted)) {
          stream.destroy();
        }
      }
    }
  }
  return need;
}

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

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

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

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

  // 本次可以讀取的字節(jié)數(shù)為0
  // 流內(nèi)部緩沖區(qū)buffer中的字節(jié)數(shù)為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 參數(shù),同樣是默認 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,所以在調(diào)用 socket.end() 的時候并不會同步的摧毀流,而是依賴 socket.read() 時觸發(fā) end 事件,然而在低版本的 node-mailer 實現(xiàn)邏輯里,會移除 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');
     // 省略
};

修復泄露

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

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

總結

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

相關文章

  • Node.js基礎入門之path模塊,url模塊,http模塊使用詳解

    Node.js基礎入門之path模塊,url模塊,http模塊使用詳解

    這篇文章主要為大家介紹了Node.js中的三個模塊(path、url、http)的使用詳解,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下
    2022-03-03
  • nodejs開發(fā)——express路由與中間件

    nodejs開發(fā)——express路由與中間件

    本篇文章主要介紹了nodejs開發(fā)——express路由與中間件 ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-03-03
  • 用node開發(fā)并發(fā)布一個cli工具的方法步驟

    用node開發(fā)并發(fā)布一個cli工具的方法步驟

    這篇文章主要介紹了用node開發(fā)并發(fā)布一個cli工具的方法步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-01-01
  • 基于node.js實現(xiàn)微信支付退款功能

    基于node.js實現(xiàn)微信支付退款功能

    在微信開發(fā)中有有付款就會有退款,這樣的功能非常常見,這篇文章主要介紹了node.js實現(xiàn)微信支付退款功能,需要的朋友可以參考下
    2017-12-12
  • node.js中的path.resolve方法使用說明

    node.js中的path.resolve方法使用說明

    這篇文章主要介紹了node.js中的path.resolve方法使用說明,本文介紹了path.resolve的方法說明、接收參數(shù)、語法、使用實例和實現(xiàn)源碼,需要的朋友可以參考下
    2014-12-12
  • Egret引擎開發(fā)指南之創(chuàng)建項目

    Egret引擎開發(fā)指南之創(chuàng)建項目

    Egret Engine(白鷺引擎)是一款使用TypeScript語言構建的開源免費的移動游戲引擎。白鷺引擎的核心定位是開放,高效,優(yōu)雅。通過它,你可以快速地創(chuàng)建HTML5類型的移動游戲,也可以將游戲項目編譯輸出成為目標移動平臺的原生游戲應用。
    2014-09-09
  • node下使用UglifyJS壓縮合并JS文件的方法

    node下使用UglifyJS壓縮合并JS文件的方法

    下面小編就為大家分享一篇node下使用UglifyJS壓縮合并JS文件的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-03-03
  • node.js中 redis 的安裝和基本操作示例

    node.js中 redis 的安裝和基本操作示例

    這篇文章主要介紹了node.js中 redis 的安裝和基本操作,結合實例形式分析了node.js中Redis下載、安裝、數(shù)據(jù)類型及基本操作技巧,需要的朋友可以參考下
    2020-02-02
  • NodeJs的優(yōu)勢和適合開發(fā)的程序

    NodeJs的優(yōu)勢和適合開發(fā)的程序

    做頁游或webqq這樣的應用nodejs有優(yōu)勢,但如果做微博、豆瓣、facebook這樣的社交網(wǎng)絡,nodejs還有優(yōu)勢嗎?另外不知道大家是什么原因選擇的nodejs?是因為應用需求還是對javascript這門語言的喜歡?
    2016-08-08
  • 利用node.js制作命令行工具方法教程(一)

    利用node.js制作命令行工具方法教程(一)

    這篇文章主要給大家介紹了node.js制作命令行工具方法教程,文中介紹的非常詳細,對大家具有一定的參考學習價值,需要的朋友們下面跟著小編一起來學習學習吧。
    2017-06-06

最新評論

自治县| 怀远县| 留坝县| 南木林县| 娱乐| 西林县| 元阳县| 广饶县| 磴口县| 清徐县| 聂拉木县| 鲁甸县| 东海县| 方山县| 广南县| 富蕴县| 乐昌市| 长武县| 龙井市| 会泽县| 南康市| 通城县| 六盘水市| 犍为县| 乐亭县| 松桃| 公主岭市| 石景山区| 荥阳市| 凤翔县| 顺平县| 布尔津县| 嵊州市| 巴林左旗| 青河县| 渝北区| 宜黄县| 阆中市| 南雄市| 兖州市| 沐川县|