使用 pm2 管理的项目多了时,难免有更新node版本或者不同应用运行在不同版本的需求,网上许多人遇到了 pm2 的集群模式无法切换 node 版本的情况,不出意外,我也遇到了,今天这篇文章把这个事为什么,怎么解决说明白,希望能帮到大家伙。
解决问题过程
各种搜,无果
看着最靠谱的答案,pm2 开发者亲自下场给出方案,大概思路是 copy 一个pm2 的可执行文件,把其中的 #!/usr/bin/env node
改为指定版本如 #!/home/ubuntu/.nvm/versions/node/v13.14.0/bin node
,怎么样,看着是不是及其靠谱?
结果经过小半天的尝试之后,宣告失败,我觉得可能是问题描述没到位,大佬没理解关键点。
大家感兴趣可以去看看这个 issue:github.com/Unitech/pm2...
看源码,有果
往往网上搜不着答案的时候,就是时候祭出大杀器了,看源码!
不看不知道啊,一看吓一跳,我算是见识到了什么叫做真正的"回调地狱",拿出一小段,大家感受一下:
从一个方法开始,一层一层的找,看的我感觉自己快升仙了,直接给我血压低的老毛病整犯了,所幸,我找到了这篇文章,juejin.cn/post/708527...,在这里感谢作者救了我的老命。
具体源码就不带大家分析了,这篇文章已经分析的非常好,咱们直接说结论。
分析后的结论
守护进程是啥
要理解后面的问题,必须先搞懂守护进程这个概念。
PM2的核心模块包括Client.js和Daemon.js。Client.js负责建立工作进程,而Daemon.js则是守护进程,负责创建守护进程以及RPC服务。
当你启动一个应用时,PM2客户端会通过RPC向Daemon发送信息,Daemon则负责创建进程。这样,进程不是由客户端创建的,而是由Daemon创建的。因此,即使客户端退出,应用也不会受到影响,这就是为什么PM2启动的应用可以在后台运行的原因。
注意:守护进程是pm2实例首次启动时创建的,之后执行一些操作时不会重新创建,除非把守护进程杀掉。
为啥 fork 模式下支持设置 interpreter 来切换 node 版本?
fork 模式下,PM2会使用Node.js的 child_process.spawn
API来创建一个新的进程。
child_process.spawn
用于创建子进程执行任何可执行文件,例如外部命令、脚本文件等,而不仅仅是 Node.js 脚本。
用法:
javascript
const { spawn } = require('child_process');
// spawn方法接受的参数依次为命令、参数、环境变量
const child = spawn('command', ['arg1', 'arg2'], {
cwd: '/usr/local',
env: { /* environment variables */ },
stdio: [/* stdin, stdout, stderr */],
});
const child = spawn('ls', ['-l', '-a']); // 启动一个新的进程执行 "ls -l -a" 命令
const child = spawn('xxx/bin/node', ['a.js']); // 启动一个新的进程执行 node a.js 命令
spawn是可以指定脚本解释器的,例如node,所以,PM2 会取我们设置的interpreter
值来作为spawn方法的第一个参数,如果没传的话就用当前终端激活的node版本,如果传入none
的话,pm2会去取pm_exec_path
中的内容,就是你首次启动pm2实例时守护进程运行的node版本。
源码可以很清晰看到:
为什么 fork 模式切换node版本后,直接启动应用版本并没有切换,需要删除应用才可以
上面说了 fork 模式会使用 child_process.spawn 创建一个新进程,前提是当前应用不存在,你原来的进程还存在的话当然不会创建新进程,只会重用之前的进程id,所以要么删除进程,要么换个应用名字
为什么 cluster 模式不支持配置 interpreter 切换版本
cluster 模式 pm2 使用 node 的 cluster 模块实现,cluster 模块使用的是 child_process.fork
方法来创建子进程,而不是 spawn 方法。
child_process.fork
用于创建一个新的 Node.js 子进程,该子进程执行指定的 JavaScript 文件,通常用于并行执行 js 代码。
用法:
javascript
const { fork } = require('child_process');
const child = fork('child.js'); // 启动一个新的 Node.js 子进程执行 child.js
与 spawn 方法的区别:
-
child_process.fork 用于执行 Node.js 脚本,通常用于并行执行 JavaScript 代码。
-
child_process.spawn 用于执行外部可执行文件,包括系统命令、脚本文件等。
-
child_process.fork 具有内置的消息传递机制,可以方便地在主进程和子进程之间发送和接收消息。
-
child_process.spawn 不具备内置的消息传递机制,需要手动设置通信方式。
child_process.fork 使用与主进程相同的 Node.js 版本来执行 js 文件,子进程间要进行通讯而且要保证一致性,所以子进程不能指定版本。
回到pm2,因为是守护进程调用的 cluster
模块创建的应用,所以cluster模块会使用守护进程运行的node版本。
与 fork 模式不同的是,即使你删除应用,切换版本后重新启动应用node版本也不会更新,因为守护进程的版本并没有改变。
调用 pm2 kill
或者通过其他方式杀死守护进程后,再切换版本启动应用,守护进程会重新启动,这时才会使用最新的版本创建集群模式的应用。
创建多个守护进程是不是就能解决集群模式的版本问题?
既然守护进程的版本不会轻易改变,而守护进程又起着绝对性作用,那创建多个守护进程是不是就能解决集群模式的版本问题?
答案是肯定的,顺着这个思路,我也确实找到了解决方案,大家继续往下看啦。
解决方案
fork 模式
- 设置 interpreter
bash
pm2 start app.js --interpreter=/usr/bin/node
如果启动的是配置文件,则在配置文件里设置
json
// ecosystem.config.js
{
interpreter: '/Users/yululiu/.nvm/versions/node/v14.21.3/bin/node',
}
bash
pm2 start ./ecosystem.config.js
优点:
- 简单,快速,安全,可立即生效,
- 可以定制化单个应用使用任意已安装版本
缺点:
- 需要手动配置node路径
- 可能线上和测试环境的node二进制文件路径不一样,需要区分环境配置不同路径
推荐使用。
- delete项目,切换node版本,重新启动
bash
pm2 delete app
nvm use 14
pm2 start ./ecosystem.config.js
优点:
- 使用 nvm 自动切换 node 版本
- 可以定制化单个应用使用任意已安装版本
缺点:
- 应用会被短暂终止
使用 pm2 delete 停止进程并清理相关配置和缓存后切换版本重新启动,会短暂终止应用,不推荐使用
cluster 模式
需要终止pm2守护程序,切换node环境,然后再恢复
bash
nvm use 14 # 先切换版本
pm2 save # 保存程序列表 (此时会记录切换激活后的node版本)
pm2 kill # 该操作会杀掉守护进程以及所有pm2已启动应用
pm2 resurrect # 恢复程序列表并启动 (所有应用都将统一使用切换后的版本)
优点:
- 适合统一切换版本的场景
缺点:
- 不能个别应用定制化版本
- 操作麻烦,成本高,有一定风险
单项目切换时不推荐使用。
启动多个 pm2 实例,分别使用不同node版本
先使用 nvm 或 export 变量 等方式切换到目标 node 版本
bash
nvm use 14
export PATH=/usr/node/v14.19.0/bin:$PATH
执行此命令前确保已切换到正确的版本
bash
# 使用新 pm2 实例启动应用
PM2_HOME="$HOME/.pm2_14" pm2 start ./ecosystem.config.js
"$HOME/.pm2_14"
表示将实例的配置文件存储到用户主目录的.pm2_14
目录下,这样14版本的实例只创建一个,避免创建过多实例占用过多内存,文件夹名字可以自由定义,如果设置为".pm2_14"
,则会在应用下创建,这样会创建过多实例,没有必要。
pm2_14
表示 pm2 node14版本的实例,你可以使用 ls -a ~
查看主目录下的文件,一般情况下,原pm2的配置文件也在该目录下,名字为 .pm2
。
指定 PM2_HOME 后,pm2将在新目录下保存应用状态,pm2 实例之间是隔离的,不会影响原有的 pm2 实例。
后续对新实例下的应用进行操作需要把所有 pm2 的命令都替换为 PM2_HOME="$HOME/.pm2_14" pm2
,注意不要搞错,以免误操作了别的实例的应用。
bash
PM2_HOME="$HOME/.pm2_14" pm2 show "$APP_NAME"
PM2_HOME="$HOME/.pm2_14" pm2 list
...
如果你觉得这个命令太长,可以设置一个简短的别名:
bash
alias pm14='PM2_HOME="$HOME/.pm2_14" pm2'
这样设置仅临时shell窗口有效,想要永久有效的话加到 ~/.bashrc 或 ~/.zshrc 中即可。
bash
# 查看你现在使用的终端
echo $0
# 如果是bash,配置文件就是~/.bashrc,zsh则是~/.zshrc
# 把别名设置加进去
vim ~/.bashrc
# source 执行一下配置文件
source ~/.bashrc
之后就可以使用更短的命令了,例如: pm14 list
。
优点:
- 可以定制化单个应用使用任意已安装版本
- 简单,安全
缺点:
- 旧应用如果还在其他 pm2 实例中运行,则需要把旧实例中的该应用删除,使用
pm2 delete
,不然端口号被旧实例占用,新实例里的应用无法启动
旧的已运行应用切换版本时,可以挑在访问量最小的时间点操作。
集群场景下推荐使用该方案。
总结
以上就是 pm2 进行 node 版本升级的一些解决方案了,为什么以及如何做基本比较清晰明了,大家可以去尝试一下了。
这种问题其实挺考验一个人的能力的,同时解决了之后对自身会有很大的提升,尤其在其他同事研究了很久之后给出结论:"pm2 集群模式没法单个项目切node版本,只能先停掉所有再重启",并且又拿不出有力证据时,这证明这个问题价值更高,看到这种问题要像在沙漠看到水一样兴奋、好奇,然后扑上去把它解决掉,类似别人解决不了的问题你解决的多了,你与别人的差距也就慢慢拉开了。