Node.js 應用故障排查手冊 —— 冗餘配置傳遞引起的內存溢出

楔子

前面一小節咱們以一個真實的壓測案例來給你們講解如何利用 Node.js 性能平臺 生成的 CPU Profile 分析來進行壓測時的性能調優。那麼與 CPU 相關的問題相比,Node.js 應用中因爲不當使用產生的內存問題是一個重災區,並且這些問題每每都是出如今生產環境下,本地壓測都難以復現,實際上這部份內存問題也成爲了不少的 Node.js 開發者不敢去將 Node.js 這門技術棧深刻運用到後端的一大阻礙。node

本節將以一個開發者容易忽略的生產內存溢出案例,來展現如何藉助於性能平臺實現對線上應用 Node.js 應用出現內存泄漏時的發現、分析、定位問題代碼以及修復的過程,但願能對你們有所啓發。git

本書首發在 Github,倉庫地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,雲棲社區會同步更新。github

 

最小化復現代碼

由於內存問題相對 CPU 高的問題來講比較特殊,咱們直接從問題排查的描述可能不如結合問題代碼來看比較直觀,所以在這裏咱們首先給出了最小化的復現代碼,你們運行後結合下面的分析過程應該能更有收穫,樣例基於 Egg.js:以下所示:json

'use strict';

const Controller = require('egg').Controller;

const DEFAULT_OPTIONS = { logger: console };

class SomeClient {
  constructor(options) {
    this.options = options;
  }
  async fetchSomething() {
    return this.options.key;
  }
}

const clients = {};

function getClient(options) {
  if (!clients[options.key]) {
    clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options));
  }
  return clients[options.key];
}

class MemoryController extends Controller {
  async index() {
    const { ctx } = this;
    const options = { ctx, key: Math.random().toString(16).slice(2) };
    const data = await getClient(options).fetchSomething();
    ctx.body = data;
  }
}

module.exports = MemoryController;

而後在 app/router.js 中增長一個 Post 請求路由:後端

router.post('/memory', controller.memory.index);

形成問題的 Post 請求 Demo 這裏也給出來,以下所示:服務器

'use strict';

const fs = require('fs');
const http = require('http');

const postData = JSON.stringify({
  // 這裏的 body.txt 能夠放一個比較大 2M 左右的字符串
  data: fs.readFileSync('./body.txt').toString()
});

function post() {
  const req = http.request({
    method: 'POST',
    host: 'localhost',
    port: '7001',
    path: '/memory',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(postData)
    }
  });

  req.write(postData);

  req.end();

  req.on('error', function (err) {
    console.log(12333, err);
  });
}

setInterval(post, 1000);

最後咱們在啓動完成最小化復現的 Demo 服務器後,再運行這個 Post 請求的客戶端,1s 發起一個 Post 請求,在平臺控制檯能夠看到堆內存在一直增長,若是咱們按照本書工具篇中的 Node.js 性能平臺使用指南 - 配置合適的告警 一節中配置了 Node.js 進程堆內存告警的話,過一會就會收到平臺的 短信/郵件 提醒。app

 

問題排查過程

收到性能平臺的進程內存告警後,咱們登陸到控制檯而且進入應用首頁,找到告警對應實例上的問題進程,而後參照工具篇中的 Node.js 性能平臺使用指南 - 內存泄漏 中的方法抓取堆快照,而且點擊 分析 按鈕查看 AliNode 定製後的分解結果展現:dom

這裏默認的報表頁面頂部的信息含義已經提到過了,這裏再也不重複,咱們重點來看下這裏的可疑點信息:提示有 18 個對象佔據了 96.38% 的堆空間,顯然這裏就是咱們須要進一步查看的點。咱們能夠點擊 對象名稱 來看到這18 個 system/Context 對象的詳細內容:async

這裏進入的是分別以這 18 個 system/Context  爲根節點起始的支配樹視圖,所以展開後能夠看到各個對象的實際內存佔用狀況,上圖中顯然問題集中在第一個對象上,咱們繼續展開查看:ide

很顯然,這裏真正吃掉堆空間的是 451 個 SomeClient 實例,面對這樣的問題咱們須要從兩個方面來判斷這是否真的是內存異常的問題:

  • 當前的 Node.js 應用在正常的邏輯下,是否單個進程須要 451 個 SomeClient 實例
  • 若是確實須要這麼多 SomeClient 實例,那麼每一個實例佔據 1.98MB 的空間是否合理

對於第一個判斷,在對應的實際生產面臨的問題中,通過代碼邏輯的從新確認,咱們的應用確實須要這麼多的 Client 實例,顯然此時排查重點集中在每一個實例的 1.98MB 的空間佔用是否合理上,假如進一步判斷仍是合理的,這意味着 Node.js 默認單進程 1.4G 的堆上限在這個場景下是不適用的,須要咱們來經過啓動 Flag 調大堆上限。

正是基於以上的判斷需求,咱們繼續點開這些 SomeClient 實例進行查看:

這裏能夠很清晰的看到,這個 SomeClient 自己只有 1.97MB 的大小,可是下面的 options 屬性對應的 Object@428973 對象一個就佔掉了 1.98M,進一步展開這個可疑的 Object@428973 對象能夠看到,其 ctx 屬性對應的 Object@428919 對象正是 SomeClient 實例佔據掉如此大的對空間的根本緣由所在!

咱們能夠點擊其它的 SomeClient 實例,能夠看到每個實例均是如此,此時咱們須要結合代碼,判斷這裏的 options.ctx 屬性掛載到 SomeClient 實例上是否也是合理的,點擊此問題 Object 的地址

進入到這個 Object 的關係圖中:

Search 展現的視圖不一樣於 Dom 結果圖,它實際上展現的是從堆快中解析出來的原始對象關係圖,因此邊信息是必定會存在的,靠邊名稱和對象名稱,咱們比較容易判斷對象在代碼中的位置。

可是在這個例子中,僅僅依靠以 Object@428973 爲起始點的內存原始關係圖,看不到很明確的代碼位置,畢竟不論是 Object.ctx 仍是 Object.key 都是至關常見的 JavaScript 代碼關係,所以咱們繼續點擊 Retainer 視圖:

獲得以下信息:

這裏的 Retainer 信息和 Chrome Devtools 中的 Retainer 含義是同樣的,它表明了節點在堆內存中的原始父引用關係,正如本文的內存問題案例中,僅靠可疑點自己以及其展開沒法可靠地定位到問題代碼的狀況下,那麼展開此對象的 Retainer 視圖,能夠看到它的父節點鏈路能夠比較方便的定位到問題代碼。

這裏咱們顯然能夠經過在 Retainer 視圖下的問題對象父引用鏈路,很方便地找到代碼中建立此對象的代碼:

function getClient(options) {
  if (!clients[options.key]) {
    clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options));
  }
  return clients[options.key];
}

結合看 SomeClient 的使用,看到用於初始化的 options 參數中實際上只是用到了其 key 屬性,其他的屬於冗餘的配置信息,無需傳入。

 

代碼修復與確認

知道了緣由後修改起來就比較簡單了,單獨生成一個 SomeClient 使用的 options 參數,而且僅將須要的數據從傳入的 options 參數上取過來以保證沒有冗餘信息便可:

function getClient(options) {
  const someClientOptions = Object.assign({ key: options.key }, DEFAULT_OPTIONS);
  if (!clients[options.key]) {
    clients[options.key] = new SomeClient(someClientOptions);
  }
  return clients[options.key];
}

從新發布後運行,能夠到堆內存降低至只有幾十兆,至此 Node.js 應用的內存異常的問題完美解決。

 

結尾

本節中也比較全面地給你們展現瞭如何使用 Node.js 性能平臺 來排查定位線上應用內存泄漏問題,其實嚴格來講本次問題並非真正意義上的內存泄漏,像這種配置傳遞時開發者圖省事直接全量 Assign 的場景咱們在寫代碼時或多或少時都會遇到,這個問題帶給咱們的啓示仍是:當咱們去編寫一個公共組件模塊時,永遠不要去相信使用者的傳入參數,任什麼時候候都應當只保留咱們須要使用到的參數繼續往下傳遞,這樣能夠避免掉不少問題。

原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索