由浅入深带你玩透pm2
前言
好不容易写了一个
nodejs
应用,不知道该怎么部署才好: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原理
pm2常用功能
首先我们新建一个service.js来练习一下pm2常用功能,server.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 当日志文件比较多之后会占用大量的磁盘空间,需要主动清除
实战-用pm2部署next应用
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
来配置不同的环境,它的配置如下:
我们先为我们的项目创建一个配置文件:
module.exports = {
apps : {
name:'template',
},
};
突然发现我们没有nodejs的脚本文件,我们只有一个"npm run start"命令,这时可以直接调用node-modules中的next脚本,配置如下:
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_
表示:
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原理
要了解pm2原理首先需要了解一下nodejs的cluster模块
cluster模块
如果像我们文章开头那样直接node server.js
部署nodejs应用,由于js是单线程的所以只能一个进程,只能在一个CPU中进行计算,无法应用服务器的多核CPU,所以需要通过多进程分发策略调度起所有的CPU;
nodejs的cluster模块是这样进行分发的:主进程和子进程分别监听不同端口,通过主进程将请求分发给子进程,从而实现负载均衡,那么cluster如何进行进程分发呢?让我们看看主进程是如何分发的(子进程仍然使用我们开篇写的service.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:
入口文件就是main
字段,也就是index.js
,接下来查看index.js
:
接下来直接点进去看API.js
,这个文件夹长达1928行,一看就头大,不知道怎么去看,从上文我们学到了利用pm2 start
命令去运行node应用,那么我猜测这个里面一定也有start
方法,果不其然是有的:
这个方法一看重点就在于_startJson
和_startScript
,这一路上我们沿着index.js ---> API.js ---> start方法 ----> _startJson方法
,还没有找到它的核心功能,不过不要气馁继续往下进行!
_startJson
中有一行代码尤其引人注意:
看来 pm2 启动的时候其实是 new 了一个 Client,至于说 Client 是干什么的我们继续往下看:
如上图,Client 本身只干了一件事情,那就是创建所有 pm2 所需的文件和文件夹,后面的内容都是在 Client 原型上添加的一些方法,我们重点看一下Client.prototype.launchDaemon
:
可以看到它就是利用 child_process
创建了一个 Daemon
子进程
再回过头来看看_startJson
中Client
做了什么事情:
然后顺着Client.prototype.executeRemote ---> Client.prototype.start ---> Client.prototype.pingDaemon ----> Client.prototype.launchDaemon ---> Client.prototype.launchRPC
依次查看发现它启动了一个Daemon进程
还有一个重点就是var req = axon.socket('req');this.client = new rpc.Client(req);
也就是创建了一个rpcClient
连接到了Daemon
进程,那么这里的this.client
就相当于Daemon实例
,那么rpc
是什么意思呢?rpc就是指调用另一个进程或者服务器上的函数
,再回到_startJson
:
沿着这个方法调用查看最终是调用了this.client
:
也就是调用了Daemon进程中的prepare方法,prepare方法又调用了God方法中的prepare,God中的prepare则进行了主子进程的创建
,其创建过程就是我们上面讲到的cluster模块
总结一下:
CLI
为pm2
入口调用了API
的start
方法start
方法调用_startJson
方法,_startJson
一边启动Daemon
进程,一边远程调用God
去创建主进程和子进程实现负载均衡
我们能学到什么
- 首先我们实战练习了一些
pm2
命令并结合实际场景部署了一个Next
应用 - 其次我们研究了一下
pm2
的源码,发现它通过rpc调用God进程来创建cluster
,加深了我们对于rpc的理解 - 最后我们能够在发生线上故障的时候快速通过日志定位错误
转载自:https://juejin.cn/post/7138572390199459871