pm2常用功能好不容易写了一个nodejs应用,不知道该怎么部署才好:
js复制代码
const app = express(); ... app.listen(3000, '0.0.0.0, () => { console.log("server started at 0.0.0.0:3000") });
直接一把node server.js部署之后,当服务挂了,却根本不知道原因,可能这个时候想起来是不是该给我们的应用加一个日志,是不是应该给我们展示一下node应用的状态,是不是应该做一个负载均衡等等,我们写业务都忙不过来哪里有时间去写这么多东西啊!但是不写的话如果出现各种问题那就只能歇菜了,没办法就只能加班了啊!
幸好有pm2,它是nodejs进程管理工具,目前如果说它排第二没人敢排第一;它正好帮助我们做了这些功能,我们可以早点下班了!下面我们会从这些方面来学习pm2:
pm2常用功能 实战用pm2部署next应用 pm2线上故障排查 pm2原理
首先我们新建一个service.js来练习一下pm2常用功能,server.js代码如下:
js复制代码const http = require('node:http');
// Server has a 5 seconds keep-alive timeout by default
http
.createServer((req, res) => {
res.write('hello\n');
res.end();
})
.listen(3000);
console.log("http server started at 3000......")
- pm2 start [options] [name|namespace|file|ecosystem|id...]
- 启动pm2进程管理,注意第一次启动pm2进程管理必须使用这个命令,options可以是一个file,或者我们的应用id,或者配置文件(这个后面再讲),我们可以运行一下pm2 start server.js这样一个node服务就在3000端口跑起来了;那么怎么看它是否运行起来了呢?
- pm2 restart 重启应用,第一次启动应用必须使用pm2 start
- pm2 reload 重新加载程序;与restart不同,restart会*死进程并重启,而reload实现了0秒停机时间重新加载;但是需要注意的是该方式可能会报错,例如在部署next应用时如果使用reload则会报端口号被占用的错误,所以reload之前经常需要先停机(pm2 stop),但是这样的话和restart一样了,pm2 stop pm2 reload = pm2 restart
- pm2 ls
- 查看当前pm2进程列表,如图1可以看到我们刚才的服务status是online说明一直在运行;我们在js文件中打印了一些文字,这个又在哪里查看呢?
图1
- pm2 logs
- 查看pm2日志,此时我们可以观察看日志默认存放位置是~/.pm2/logs/service-out[error].log,其中sevice就是我们的appName,后面当日志比较多的时候我们可以直接翻这个文件来查看日志,划重点了,这个地址在我们后面会经常用到,一定要在脑子里面给它记住!
- 拿到了日志文件,下面就是一顿操作猛如虎了。我们做前端的同学可能对于linux操作并不熟悉,一上手就用cat或者vi查看日志,但是日志太多眼都瞎了根本看不过来;有没有一种动态滚动查看日志的方法?是有的,用less命令;如图2就是我们的日志;
图2
- pm2 monit 这个命令是一个神器,它提供了可视化界面实时监控线上应用,可以看到所有实例的所有日志;在线上排查问题的时候非常实用,使用该命令查看的结果如图3:
图3
- pm2 flush 当日志文件比较多之后会占用大量的磁盘空间,需要主动清除
next是一个react服务端渲染框架,与vue的nuxt和angular的nest齐名,我们先初始化一个项目npx create-next-app@latest
然后运行npm run dev就可以将项目运行起来,然后我们运行npm run build,得到打包文件再运行npm run start,可以在3000端口访问到该应用,接下来我们用pm2部署该应用;由于next的部署是通过npm命令而不是一个js文件来执行的所以我们需要这样来执行pm2 start "npm run start",项目就跑起来了,如图4
图4
但是问题并没有这么简单,我们有测试环境也有生产环境,都需要部署,这个时候我们还用原来的命令来部署,就无法区分环境了
如果测试环境和生产环境端口号不一样怎么办?
第一次启动应用使用pm2 start,第二次需要使用pm2 restart,这好像也不好办?
别急我们接着讲;如果我们需要区分环境了,那么单单使用命令已经不能够满足我们的需要了,我们可以创建一个配置文件ecosystem.config.js来配置不同的环境,它的配置如下:
我们先为我们的项目创建一个配置文件:
js复制代码module.exports = {
apps : {
name:'template',
},
};
突然发现我们没有nodejs的脚本文件,我们只有一个"npm run start"命令,这时可以直接调用node-modules中的next脚本,配置如下:
js复制代码 module.exports = {
apps : {
name:'template',
script:'./node_modules/.bin/next',
args:'start'
},
};
当我们的应用运行时间较长,为了防止内存泄漏,我们配置一个max_memory_restart:"1G",超过这个值之后应应用重启,这样不至于应用崩溃,接下来将自动重启设置为true:autorestart: true,我们如果说想要手动进行部署控制的话还需要把watch改为false,默认情况为true,也就是说有文件改变的时候应用会自动重启,有时候我们可能会远程传输一些文件到public文件夹但是这种情况并不需要重启应用
接下来我们解决第一个问题:有测试环境和生产环境或者还有预发环境,那么我们就在配置文件中配置一下env对象,一般需要配置端口号、环境变量、主机,默认为生产环境,其他的就用env_表示:
js复制代码 module.exports = {
apps : {
name:'template',
script:'./node_modules/.bin/next',
args:'start',
max_memory_restart:'1G',
autorestart: true,
watch: false,
env_test: {
ENV: 'test',
PORT: 8000,
HOST: '0.0.0.0',
},
env: {
ENV: 'prod',
PORT: 8001,
HOST: '0.0.0.0',
},
},
};
这样配置之后我们执行pm2 start ecosystem.config.js,默认是使用的env配置,所以我们可以在8001端口访问到node应用;如果需要部署测试环境那么需要加一个env参数:pm2 start ecosystem.config.js --env test,一顿操作之后8001无法访问了,8000可以访问了,测试环境部署成功!这样我们的pm2应用就部署成功了!部署成功之后难免有时候会出现问题,或者某些接口报错了,这个时候我们需要进行故障排查
pm2线上故障排查可以用更加复杂的案例来进行练习,但是我们这里先使用较为简单的案例进行讲解,因为故障排查的一般步骤都是差不多的;我们先运行我们当前的应用pm2 start ecosystem.config.js --env test,可以看到运行之后状态为online,别急可以等一等再查看一下状态pm2 ls,如果这个时候还是online,那么应用启动成功了一半。为什么这么说呢?我们来看一看产生故障时的两种情况:
- 重启了应用发现应用跑不起来了,首先我们应该想到的就是日志:pm2 logs appName:最后发现日志很多那么直接使用less查看日志,然后command G跳转到日志最后一行往上稍微翻一翻就可以看到报错,然后修复就行了
- 应用启动成功了,但是页面显示502,这说明应用可能一直在重复地重启,既然应用状态是online那么就可以使用可视化页面监控当前pm2进程:pm2 monit,查看当前应用内存占用、CPU占用以及实时日志,可以定位到问题;这种方式也可以排查刚出现的线上问题;这种情况就是虚假的启动成功,需要仔细斟酌
要了解pm2原理首先需要了解一下nodejs的cluster模块
cluster模块如果像我们文章开头那样直接node server.js部署nodejs应用,由于js是单线程的所以只能一个进程,只能在一个CPU中进行计算,无法应用服务器的多核CPU,所以需要通过多进程分发策略调度起所有的CPU;
nodejs的cluster模块是这样进行分发的:主进程和子进程分别监听不同端口,通过主进程将请求分发给子进程,从而实现负载均衡,那么cluster如何进行进程分发呢?让我们看看主进程是如何分发的(子进程仍然使用我们开篇写的service.js):
js复制代码const cluster = require('node:cluster') ;
const { cpus } = require('node:os') ;
const process = require('node:process');
const numCPUs = cpus().length;
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
// Fork workers.
for (let i = 0; i < numCPUs; i ) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
require('./service.js')
console.log(`Worker ${process.pid} started`);
}
可以看到判断只要是主进程那么直接调用fork,fork内部其实是启动了一个Worker继续调用当前这个js但是这个时候cluster.isPrimary为false,进入子进程判断,执行service.js,有了这个基础我们再来看pm2源码
pm2源码解析首先我们需要寻找pm2项目的入口文件,先看一看它的package.json: