likes
comments
collection
share

由浅入深带你玩透pm2

作者站长头像
站长
· 阅读数 19

前言

好不容易写了一个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文件中打印了一些文字,这个又在哪里查看呢?

    由浅入深带你玩透pm2

图1

  • pm2 logs

    查看pm2日志,此时我们可以观察看日志默认存放位置是~/.pm2/logs/service-out[error].log,其中sevice就是我们的appName,后面当日志比较多的时候我们可以直接翻这个文件来查看日志,划重点了,这个地址在我们后面会经常用到,一定要在脑子里面给它记住!

    拿到了日志文件,下面就是一顿操作猛如虎了。我们做前端的同学可能对于linux操作并不熟悉,一上手就用cat或者vi查看日志,但是日志太多眼都瞎了根本看不过来;有没有一种动态滚动查看日志的方法?是有的,用less命令;如图2就是我们的日志;

    由浅入深带你玩透pm2

图2

  • pm2 monit 这个命令是一个神器,它提供了可视化界面实时监控线上应用,可以看到所有实例的所有日志;在线上排查问题的时候非常实用,使用该命令查看的结果如图3:

    由浅入深带你玩透pm2

图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

由浅入深带你玩透pm2

图4

但是问题并没有这么简单,我们有测试环境也有生产环境,都需要部署,这个时候我们还用原来的命令来部署,就无法区分环境了

如果测试环境和生产环境端口号不一样怎么办?

第一次启动应用使用pm2 start,第二次需要使用pm2 restart,这好像也不好办?

别急我们接着讲;如果我们需要区分环境了,那么单单使用命令已经不能够满足我们的需要了,我们可以创建一个配置文件ecosystem.config.js来配置不同的环境,它的配置如下:

由浅入深带你玩透pm2

由浅入深带你玩透pm2

我们先为我们的项目创建一个配置文件:

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,那么应用启动成功了一半。为什么这么说呢?我们来看一看产生故障时的两种情况:

  1. 重启了应用发现应用跑不起来了,首先我们应该想到的就是日志:pm2 logs appName:最后发现日志很多那么直接使用less查看日志,然后command+G跳转到日志最后一行往上稍微翻一翻就可以看到报错,然后修复就行了
  2. 应用启动成功了,但是页面显示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:

由浅入深带你玩透pm2

入口文件就是main字段,也就是index.js,接下来查看index.js:

由浅入深带你玩透pm2

接下来直接点进去看API.js,这个文件夹长达1928行,一看就头大,不知道怎么去看,从上文我们学到了利用pm2 start命令去运行node应用,那么我猜测这个里面一定也有start方法,果不其然是有的:

由浅入深带你玩透pm2

这个方法一看重点就在于_startJson_startScript,这一路上我们沿着index.js ---> API.js ---> start方法 ----> _startJson方法,还没有找到它的核心功能,不过不要气馁继续往下进行!

_startJson中有一行代码尤其引人注意:

由浅入深带你玩透pm2

看来 pm2 启动的时候其实是 new 了一个 Client,至于说 Client 是干什么的我们继续往下看:

由浅入深带你玩透pm2

如上图,Client 本身只干了一件事情,那就是创建所有 pm2 所需的文件和文件夹,后面的内容都是在 Client 原型上添加的一些方法,我们重点看一下Client.prototype.launchDaemon:

由浅入深带你玩透pm2

可以看到它就是利用 child_process 创建了一个 Daemon 子进程

再回过头来看看_startJsonClient做了什么事情:

由浅入深带你玩透pm2

然后顺着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:

由浅入深带你玩透pm2

沿着这个方法调用查看最终是调用了this.client

由浅入深带你玩透pm2

也就是调用了Daemon进程中的prepare方法,prepare方法又调用了God方法中的prepare,God中的prepare则进行了主子进程的创建,其创建过程就是我们上面讲到的cluster模块

总结一下:

  • CLIpm2入口调用了APIstart方法
  • start方法调用_startJson方法,_startJson一边启动Daemon进程,一边远程调用God去创建主进程和子进程实现负载均衡

我们能学到什么

  • 首先我们实战练习了一些pm2命令并结合实际场景部署了一个Next应用
  • 其次我们研究了一下pm2的源码,发现它通过rpc调用God进程来创建cluster,加深了我们对于rpc的理解
  • 最后我们能够在发生线上故障的时候快速通过日志定位错误