使用Easy monitor3排查Node内存泄露问题

914 阅读5分钟

简介

Easy-Monitor 3.0是一款基于Node.js Addon 实现的一款企业级 Node.js 应用性能监控与线上故障定位解决方案 项目平台使用Vue+iView+egg进行开发,类似AliNode风格的控制台

特点:

  • 项目开源,整体基于 BSD 2-Clause 协议
  • Windows、Linux 和 MacOS 全平台支持
  • 针对 Node.js 进程与系统指标的性能监控
  • 错误日志展示与依赖 Npm 模块安全风险提示
  • 自定义智能运维告警与线上进程实时状态导出
  • 完整的私有化部署支持

image.png

HeadDump

HeadDump被称为堆快照,会保留应用在当前机器上实时的内存状态,方便我们查看内存在应用中的使用情况。

Node应用生成堆快照

方式一

安装heapdump插件

npm install heapdump

在应用入口引入插件:

// index.js
const headdump = require('heapdump');
const app = (require('express'))();
app.listen(3000);
node index.js

获取快照:

// 代码中加入将快照存入磁盘的逻辑
heapdump.writeSnapshot('/var/local/' + Date.now() + '.heapsnapshot');

或者

# 命令行中强制关闭应用进程,生成heapdump
ps aux|grep index.js # 查询应用进程ID
kill -31 [进程ID] # 生成heapdump

方式二

vscode通过debug启动项目

iShot_2022-05-06_16.05.22.png

iShot_2022-05-06_16.08.39.png

iShot_2022-05-06_16.09.58.png

截屏2022-05-06 下午4.15.27.png

Google浏览器分析堆快照

iShot_2022-05-06_16.20.12.png

Easy Monitor准备工作

首先要在本地部署监控服务端,服务端由控制台,采集器管理服务,采集器长连接服务组成

服务端依赖:

  • MySQL
  • Redis

Mysql 初始化部分,库 xprofiler_console 使用 xprofiler-console/db/init.sql 进行初始化,库 xprofiler_logs 使用 xtransit-manager/db/init.sql 以及 xtransit-manager/db/date.sql 进行初始化。

部署控制台

克隆项目到本地

git clone https://github.com/X-Profiler/xprofiler-console

添加配置:

// xprofiler-console/config/config.local.js
'use strict';

module.exports = () => {
  const config = {};

  config.mysql = {
    app: true,
    agent: false,
    clients: {
      xprofiler_console: {
        host: '127.0.0.1',
        port: 3306,
        user: '****',
        password: '********',
        database: 'xprofiler_console',
      },
      xprofiler_logs: {
        host: '127.0.0.1',
        port: 3306,
        user: '****',
        password: '********',
        database: 'xprofiler_logs',
      },
    },
  };

  config.redis = {
    client: {
      sentinels: null,
      port: 6379,
      host: '127.0.0.1',
      password: '',
      db: 0,
    },
  };

  config.xprofilerConsole = 'http://127.0.0.1:8443'; // 部署时请使用外部访问域名替换

  config.xtransitManager = 'http://127.0.0.1:8543'; // 部署时请使用外部访问域名替换

  return config;
};

启动

npm run dev

部署采集器管理服务

git clone https://github.com/X-Profiler/xtransit-manager

添加配置:

// xtransit-manager/config/config.local.js
'use strict';

module.exports = () => {
  const config = {};

  config.mysql = {
    app: true,
    agent: false,
    clients: {
      xprofiler_console: {
        host: '127.0.0.1',
        port: 3306,
        user: '****',
        password: '********',
        database: 'xprofiler_console',
      },
      xprofiler_logs: {
        host: '127.0.0.1',
        port: 3306,
        user: '****',
        password: '********',
        database: 'xprofiler_logs',
      },
    },
  };

  config.redis = {
    client: {
      sentinels: null,
      port: 6379,
      host: '127.0.0.1',
      password: '',
      db: 0,
    },
  };

  config.mailer = {
    host: 'smtp.**.com',
    port: 25,
    secure: false,
    auth: {
      user: 'test@mail.com',
      pass: '********',
    },
  };

  config.xprofilerConsole = 'http://127.0.0.1:8443';

  return config;
};

启动

npm run dev

部署采集器长连接服务

git clone https://github.com/X-Profiler/xtransit-server

添加配置:

// xtransit-server/config/config.local.js
'use strict';

module.exports = () => {
  const config = {};

  config.xtransitManager = 'http://127.0.0.1:8543';

  return config;
};

启动

npm run dev

docker方式部署easy monitor

参考 github.com/x-profiler/…

Easy Monitor使用方式

通过easy monitor监控我们开发维护的项目

通过浏览器访问http://127.0.0.1:8443 打开easy monitor控制台

首次会出现一下弹框,可以随意填写一个邮箱和密码,这里可以通过结合公司域账号体系进行使用

image.png

iShot_2022-05-06_16.57.48.png

iShot_2022-05-06_17.01.05.png

在自己的项目下安装性能日志插件和采集器插件

npm install xprofiler --save --xprofiler_binary_host_mirror=https://npmmirror.com/mirrors/xprofiler
npm install xtransit --save
// 应用入口启动xprofiler
require('xprofiler').start();
const xtransit = require('xtransit');
xtransit.start({
  // 必须的配置(一定要写)
  server: `ws://127.0.0.1:9090`, // 填写前一节中部署的 xtransit-server 地址
  appId: 1, // 创建应用得到的应用 ID
  appSecret: 'f58615bb5e5cdb0b377b3ec7c192fba8' // 创建应用得到的应用 Secret
});

采集器详细配置

每一个xprofiler监控的项目都会生成一个egg也就是下面的一个蛋蛋,用来显示项目各个指标状态

这里的各种颜色的含义如下:

  • 绿色:负载占比 < 60%

  • 黄色:60% <= 负载占比 < 85%

  • 红色:负载占比 >= 85%

  • 灰色:实例已连接但是尚未接收到日志数据 截屏2022-05-07 下午3.47.03.png

  • 实例个数: 连接到这个应用的 ECS 实例或者 Docker 实例的数量

  • 依赖风险数: 连接到这个应用的某一个实例依赖的 Npm 包扫描结果(正常情况下所有实例都是一致的)

  • 24h 告警数: 在过去的 24 小时内此应用触发了告警规则导致的告警数量 iShot_2022-05-07_14.28.43.png

进入实例详情页: iShot_2022-05-07_15.25.15.png

进程时间线:显示应用24h内的运行状态: 截屏2022-05-07 下午4.10.15.png

查看应用各个指标占用率: 截屏2022-05-07 下午4.13.02.png

通过抓取性能数据可以分析项目中性能问题

截屏2022-05-07 下午4.17.28.png

Easy Monitor排查内存泄漏问题

问题

在生产环境中存在这么一个项目,该项目会在不断的并发请求中内存内存会缓慢的上升直到内存溢出

排查过程

通过上面的使用方式启动了easy monitor对该项目进行监控,使用压测工具(例如jmeter、apifox)在项目启动过程中不断压测并使用easy monitor生成不同时段的堆快照

压测请求1000次和2000次对比,发现是匿名函数不能被GC回收,导致占用空间在不断增加:

1000次请求,匿名函数重复5000次

image.png 匿名函数重复10000次

image.png

对比1000次请求和2000次请求,发现每次增加大概16M左右,基本增加的内存都是匿名函数导致的,在对匿名函数进行搜索排查,发现以下几个匿名函数重复多次

解决问题

问题一: image.png

问题二: image.png

问题三: image.png 经过搜索发现问题一、问题二这两个匿名函数属于ejs嵌入的匿名函数 图一: image.png 图二: image.png 经过对ejs代码优化:

1.将compileDebug设置为false,解决图一问题(compileDebug是ejs在编译ejs文件时的调试语句,对于线上不是特别重要)

2.通过将输出函数缓存,解决图二问题

3.问题三中嵌入的代码,逻辑是用于打开指定页面用的匿名函数,是项目中使用puppeteer创建Page实例,每次调用接口都会触发setContent绘制页面,setContent函数每次都会创建一个问题三所示的匿名函数,导致匿名函数无法被清除,不断累积,使内存不断上升

通过Page.setContent(null)和Page.reload()方法对页面进行置空并重新加载,清空匿名函数

总结

在上述案例排查内存泄漏问题上,通过easy monitor监控项目性能指标,多次测试并抓取性能指标相关文件,对比下会发现两次差异从而找到问题的根本原因