看了pm2的源码后,发现了集群模式下无法切换node版本的奥秘

使用 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 模式

  1. 设置 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二进制文件路径不一样,需要区分环境配置不同路径

推荐使用。

  1. 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版本,只能先停掉所有再重启",并且又拿不出有力证据时,这证明这个问题价值更高,看到这种问题要像在沙漠看到水一样兴奋、好奇,然后扑上去把它解决掉,类似别人解决不了的问题你解决的多了,你与别人的差距也就慢慢拉开了。

相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_7482402510 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar10 小时前
纯前端实现更新检测
开发语言·前端·javascript