背景
之前 Node.js 应用服务出现过内存泄露的情况,当时通过阅读代码定位问题来处理,但这终究不是长久之计,还是需要通过应用性能监控来处理该问题。
公司的服务目前都依赖于腾讯云,但其提供的应用监控服务并不支持开箱即用的 Node.js 应用性能监控能力,需要自行性能埋点,也无法进行线上故障定位。对比了 PM2 Monitoring、newrelic、和 easy-monitor,感觉还是 easy-monitor 更适合我们,因为我们已经有了日志服务,系统监控服务、数据库监控服务,并且我们的性能瓶颈主要在数据库端,所以 easy-monitor 提供的应用性能监控与线上故障定位能力就是很好的补充。
介绍
Easy-monitor 是企业级 Node.js 应用性能监控与线上故障定位解决方案,开源免费,全平台支持,提供系统监控、针对 Node.js 进程与系统指标的性能监控、错误日志和依赖 NPM 模块安全风险提示和运维告警能力
部署 easy-monitor
部署监控服务端
前置依赖
预先安装
- MySQL
- Redis
初始化 MySQL 数据库
mysql -u <用户名> -p
# 创建数据库 xprofiler_console
CREATE DATABASE xprofiler_console;
# 选择数据库 xprofiler_console
USE xprofiler_console;
# 初始化数据库 xprofiler_console,使用初始化文件 https://github.com/X-Profiler/xprofiler-console/blob/master/db/init.sql
SOURCE /path/to/init.sql;
# 创建数据库 xprofiler_logs
CREATE DATABASE xprofiler_logs;
# 选择数据库 xprofiler_logs
USE xprofiler_logs;
# 初始化数据库 xprofiler_console,使用初始化文件 https://github.com/X-Profiler/xtransit-manager/blob/master/db/init.sql
SOURCE /path/to/init.sql;
# 初始化数据库 xprofiler_console,使用初始化文件 https://github.com/X-Profiler/xtransit-manager/blob/master/db/date.sql
SOURCE /path/to/date.sql;
一键全套部署
下载全套部署仓库
git clone https://github.com/X-Profiler/all-in-one.git
本地验证
编辑文件 config.local.js,配置 MySQL 和 Redis
module.exports = () => {
const config = {};
// mysql
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',
},
},
};
// redis
config.redis = {
client: {
sentinels: null,
port: 6379,
host: '127.0.0.1',
password: '',
db: 0,
},
};
return config;
};
在开发环境启动
npm run dev
打开 http://[IP 地址]:8443 就可以访问了,默认需要登录,第一次登录的用户信息会被保存下来。
线上部署
编写 config.prod.js,配置 MySQL 和 Redis,具体内容可参考如上。 执行线上部署的启动和暂停
// 启动全套服务
npm start
// 停止全套服务
npm stop
自定义存储
xprofiler 插件生成的诸如堆快照等性能分析文件默认存储在本机上,如果你想存储在其他地方可实现自定义存储。 以腾讯 COS 为例:
// all-in-one/lib/plugin/egg-storage/app/extend/application.js
const COS = require('cos-nodejs-sdk-v5')
const config = {
Bucket: process.env.COS_BUCKET ?? "",
Region: process.env.COS_REGION ?? "",
secretId: process.env.COS_SECRET_ID ?? "",
secretKey: process.env.COS_SECRET_KEY ?? "",
}
const prefix = process.env.COS_PREFIX ?? "" //文件夹路径前缀
const cos = new COS({
SecretId: config.secretId,
SecretKey: config.secretKey,
})
module.exports = {
get storage() {
const { logger } = this
return {
/**
*
* @param {string} fileName
* @param {Buffer|import("stream").Stream} stream
*/
async saveFile(fileName, stream) {
try {
// ...保存文件的逻辑
await cos.putObject({
Bucket: config.Bucket,
Region: config.Region,
Key: prefix + fileName,
Body: stream
})
} catch (error) {
logger.error(`save file failed: ${error}`)
}
},
async deleteFile(fileName) {
// ...删除文件的逻辑
try {
await cos.deleteObject({
Bucket: config.Bucket,
Region: config.Region,
Key: prefix + fileName,
})
} catch (err) {
logger.error(`delete file failed: ${err}`)
}
},
downloadFile(fileName) {
return cos.getObjectStream({
Bucket: config.Bucket,
Region: config.Region,
Key: prefix + fileName
})
},
}
},
}
开启插件
// all-in-one/config/plugin.js
exports.storage = {
enable: true,
path: path.join(__dirname, '../lib/plugin/egg-storage'),
};
NodeJS 应用接入
支持 Egg.js 和 非 Egg.js 应用的接入,以非 Egg.js 应用为例
开启性能日志插件
安装 xprofiler
npm install xprofiler --save --xprofiler_binary_host_mirror=https://npmmirror.com/mirrors/xprofiler
在 Node.js 应用入口文件顶部添加如下代码
require('xprofiler').start();
部署日志采集器
创建应用:
访问 http://[IP地址]:8443/ 打开监控控制台,创建新应用
创建完成后生成 appId 和 appSecret
配置采集器:
安装 xtransit
npm install xtransit --save
配置 xtransit
const xtransit = require('xtransit');
// 获取 HOME PATH
const homePath = os.homedir();
const config = {
// I. 必须的配置(一定要写)
server: 'ws://127.0.0.1:9190', // 填写前一节中部署的 xtransit-server 地址,如果非同一台机器,则需要填写外网地址
appId: 1, // 创建应用得到的应用 ID
appSecret: 'xxx', // 创建应用得到的应用 Secret
errors: [path.resolve(homePath, './.pm2/logs/weapp-error.log')], // 数组,每一项为配置需要监控的 error 日志文件全路径,此处配置 PM2 错误日志,用于显示“错误日志”
packages: [path.resolve(__dirname, './package.json')], // 数组,每一项为配置需要监控的 package 日志文件全路径,用于显示“依赖风险”
};
xtransit.start(config);
因为线上一般会启动多个业务进程,但仅需启动一个采集器,如果你是使用 PM2 启动服务的话,可以如下配置:
// PM2 启动配置样例,process.json
{
"apps": [
{
"name": "xtransit 进程",
"script": "xtransit.js",
"exec_mode": "fork"
},
{
"name": "你的业务进程",
"script": "dispatch.js",
"instances": 4
}
]
}
使用 PM2 启动服务
pm2 start process.json
问题
错误日志无法显示
错误日志的时间格式必须是YYYY-MM-DD HH:mm:ss,或者修改 config.js 里的 errexp 配置
// config.js
module.exports = {
errexp: /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/i, // 匹配错误日志起始的正则,默认为匹配到 YYYY-MM-DD HH:mm:ss 时间戳即认为是一条错误日志的起始
}
参考 issue: 非egg应用获取不到error logs
依赖风险无法显示
2023-09-05 17:38:50,877 ERROR 46543 [-/127.0.0.1/-/16892809ms GET /] getAudit failed: Error: 400 Bad Request - POST https://registry.npmjs.org/-/npm/v1/security/audits - Bad Request
2023-09-05 17:38:52,193 ERROR 46542 [-/127.0.0.1/-/31ms GET /xapi/module?appId=1&agentId=jinghonggangdeMacBook-Pro-2.local&moduleFile=%2FUsers%2Fjinghonggang%2FWork%2Fnode-service%2Fpackage.json] request failed: SyntaxError: Unexpected token < in JSON at position 0, url: http://127.0.0.1:8543/xprofiler/modules, data: {"appId":"1","agentId":"jinghonggangdeMacBook-Pro-2.local","moduleFile":"/Users/jinghonggang/Work/node-service/package.json","options":{"fromCache":true},"signature":"a71d6bc4e9d32edb7f1aa0335ff60c9e7b3eaee4"}
网络原因原因,获取 registry.npmjs.org/-/npm/v1/se… 失败。
独立部署后转储失败
原因是 xprofilerConsole 地址有误,需要将 config/config.prod.js 文件文件里的 config.xprofilerConsole 配置为独立部署的外网地址。
与 Alinode 的区别
Alinode 的优点是能力更强,缺点是需要安装 AliNode Runtime,和 NodeJS 强绑定,最新对应 Node.js 版本只支持到 v16.15.0,版本有点旧。