简介
Easy-Monitor 3.0是一款基于Node.js Addon 实现的一款企业级 Node.js 应用性能监控与线上故障定位解决方案 项目平台使用Vue+iView+egg进行开发,类似AliNode风格的控制台
特点:
- 项目开源,整体基于 BSD 2-Clause 协议
- Windows、Linux 和 MacOS 全平台支持
- 针对 Node.js 进程与系统指标的性能监控
- 错误日志展示与依赖 Npm 模块安全风险提示
- 自定义智能运维告警与线上进程实时状态导出
- 完整的私有化部署支持
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启动项目
Google浏览器分析堆快照
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
Easy Monitor使用方式
通过easy monitor监控我们开发维护的项目
通过浏览器访问http://127.0.0.1:8443 打开easy monitor控制台
首次会出现一下弹框,可以随意填写一个邮箱和密码,这里可以通过结合公司域账号体系进行使用
在自己的项目下安装性能日志插件和采集器插件
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%
-
灰色:实例已连接但是尚未接收到日志数据
-
实例个数: 连接到这个应用的 ECS 实例或者 Docker 实例的数量
-
依赖风险数: 连接到这个应用的某一个实例依赖的 Npm 包扫描结果(正常情况下所有实例都是一致的)
-
24h 告警数: 在过去的 24 小时内此应用触发了告警规则导致的告警数量
进入实例详情页:
进程时间线:显示应用24h内的运行状态:
查看应用各个指标占用率:
通过抓取性能数据可以分析项目中性能问题
Easy Monitor排查内存泄漏问题
问题
在生产环境中存在这么一个项目,该项目会在不断的并发请求中内存内存会缓慢的上升直到内存溢出
排查过程
通过上面的使用方式启动了easy monitor
对该项目进行监控,使用压测工具
(例如jmeter、apifox)在项目启动过程中不断压测
并使用easy monitor生成不同时段的堆快照
压测请求1000次和2000次对比,发现是匿名函数不能被GC回收,导致占用空间在不断增加:
匿名函数重复10000次
对比1000次请求和2000次请求,发现每次增加大概16M左右,基本增加的内存都是匿名函数导致的,在对匿名函数进行搜索排查,发现以下几个匿名函数重复多次
解决问题
问题一:
问题二:
问题三:
经过搜索发现问题一、问题二这两个匿名函数属于ejs嵌入的匿名函数
图一:
图二:
经过对ejs代码优化:
1.将compileDebug设置为false,解决图一问题
(compileDebug是ejs在编译ejs文件时的调试语句,对于线上不是特别重要)
2.通过将输出函数缓存,解决图二问题
3.问题三
中嵌入的代码,逻辑是用于打开指定页面用的匿名函数,是项目中使用puppeteer
创建Page实例,每次调用接口都会触发setContent
绘制页面,setContent函数每次都会创建一个问题三
所示的匿名函数,导致匿名函数无法被清除,不断累积,使内存不断上升
通过Page.setContent(null)和Page.reload()方法对页面进行置空并重新加载,清空匿名函数
总结
在上述案例排查内存泄漏问题上,通过easy monitor监控项目性能指标,多次测试并抓取性能指标相关文件,对比下会发现两次差异从而找到问题的根本原因