Причина истории:
Причина истории в том, что в нашем проекте несколько проектов nodejs развернуты на одной машине с помощью pm2, но каждому проекту нужна определенная версия nodejs (поскольку каждый проект зависит от надстройки c++ разных версий узла), например, проект A
должен использовать node12
для запуска, проект B
использует node14
для запуска.
Описание проблемы:
Когда машина была расширена, было обнаружено, что вновь развернутая машина использовалась для node12
запуска проекта , который должен был A
быть node14
запущен.
Сначала, должно быть, возникло раздражение, когда я спросил, почему среда версии узла машины для расширения эксплуатации и обслуживания отличается от предыдущей машины? Никогда раньше не было подобной проблемы при расширении емкости, почему на этот раз проблема с расширенной машиной?
Конечно, студенты по эксплуатации и техническому обслуживанию очень невинны ~, мы не вносили никаких изменений, и процесс расширения стандартизирован, и проблем не возникнет.
Итак, фронтенд-студенты начали долгое расследование и проверяли по разным каналам, таким как: точка узла в переменной окружения, установлен ли узел в точке, установлены ли зависимости при развертывании через CI и так далее. . . Из-за процесса развертывания компании и его «зрелости» потребовалось много времени от подачи заявки на разрешения до устранения неполадок. .
Пока мы не заметили поле в pm2 mode
:
Поэтому я попытался воспроизвести проблему локально:
Воспроизвести ДЕМО
- Мы запускаем два сервера узлов через файл конфигурации экосистемы.config.js pm2. Имена двух серверов — приложение node10 и приложение node14 соответственно, а файлы сценариев — index1.js и index2.js соответственно. Параметр интерпретатора версии узла
module.exports = {
apps : [{
out_file: './out.log',
name : "node 10 app",
script : "./index1.js",
interpreter: "/Users/haochenli/.nvm/versions/node/v10.0.0/bin/node" //node路径
}, {
out_file: './out.log',
name : "node 14 app",
script : "./index2.js",
interpreter: "/Users/haochenli/.nvm/versions/node/v14.18.1/bin/node", //node路径
}]
}
复制代码
// index1.js
const http = require('http')
const process = require('process')
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n' + process.version);
}).listen(8000);
console.log(`Worker ${process.pid} started`);
复制代码
// index2.js
const http = require('http')
const process = require('process')
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n' + process.version);
}).listen(8001); // 对比index1.js只是改了个端口号
console.log(`Worker ${process.pid} started`);
复制代码
- Затем запускаем терминал, pm2 start:
- Браузер открывается
localhost:8000
иlocalhost:8001
:
Все работает отлично!Оба проекта работают с нашей настроенной версией узла.
- 之后我们稍微修改一下配置文件改成如下:
module.exports = {
apps : [{
// 对应端口8000的服务
+ instances: 1, //增加instance配置,使服务启动在cluster模式下
out_file: './out.log',
name : "node 10 app",
script : "./index1.js",
interpreter: "/Users/haochenli/.nvm/versions/node/v10.0.0/bin/node"
}, {
// 对应端口8001的服务
out_file: './out.log',
name : "node 14 app",
script : "./index2.js",
interpreter: "/Users/haochenli/.nvm/versions/node/v14.18.1/bin/node",
}]
}
复制代码
先杀掉所有进程(为啥不用pm2 delete和pm2 stop我们后面再说),执行pkill -f pm2
。之后再次执行pm2 start
,来看下结果:pm2运行log正常:
浏览器访问两个端口号: 一个是v16.13.0
,一个是v14.18.1
?我们明明设置的是v10.0.0
,这个v16.13.0
是哪里来的?
茶泡好,烟点起,让我们一步一步来
instances配置:
首先当然是查看instance这个配置是什么意思,干啥用的,配置了instance进入的cluster模式又是什么?pm2 cluster模式,简单理解下来,cluster模式就是让你的服务尽可能的利用你的计算机性能(如多个cpu),创建多个子进程,均衡服务的负载,在不改变代码的前提下尽可能大的提升服务的性能,而instances就是允许你的服务可以用上几个cpu。
说到这里我们看下pm2中的源码:
//pm2/lib/God.js
env.instances = parseInt(env.instances);
if (env.instances === 0) {
env.instances = numCPUs;
} else if (env.instances < 0) {
env.instances += numCPUs;
}
if (env.instances <= 0) {
env.instances = 1;
}
timesLimit(env.instances, 1, function (n, next) {
// 执行env.instances次的executeApp
....
return God.executeApp()
})
复制代码
可以看出和文档一致,instances相当于是执行多少次App。
God Daemon进程:
那么问题又来了,这个God
又是什么,并且上面的代码所在文件也是叫做God.js,经过我的查找,当我们在终端中查一下进程就知道这个God
意味着啥了,在终端中运行ps -aef | grep pm2
我们来仔细看下:
忽略最后的一个grep命令,有三个和pm2相关的命令,其中第一个叫做God Daemon,之后的两个就是我们对应的两个node-server(node10 app和node14 app)。当我们pm2 delete 0
或者pm2 delete 1
对应kill掉的线程其实是后面两个,而god daemon是伴随pm2启动的,所谓的master process
(老外现在叫做primary process)。我们试下: 所以我们的项目在进行pm2 start命令时,所有的进程如下:
这也是上面我们为什么执行pkill -f pm2
来杀掉所有的pm2指令(后来查文档知道pm2提供一个杀掉god进程的方式,pm2 kill)
PM2 中的Fork模式
现在已经理清了pm2启动项目的进程创建过程,接下来看fork是如何实现的:
从/lib/god.js中看到God.executeApp = function executeApp(env, cb) {...}
方法,里面有个大的ifelse:
require('./God/ForkMode.js')(God);
require('./God/ClusterMode.js')(God);
...
God.executeApp = function executeApp(env, cb) {
if (env_copy.exec_mode === 'cluster_mode') {
God.nodeApp(env_copy, function nodeApp(err, clu) {
var old_env = God.clusters_db[clu.pm2_env.pm_id]; // 这里会根据id保存node执行的env
if (old_env) {
old_env = null;
God.clusters_db[clu.pm2_env.pm_id] = null;
}
God.clusters_db[clu.pm2_env.pm_id] = clu;
// 下面一堆监听事件
clu.once('error', function(err) {...});
clu.once('disconnect', function() {...});
clu.once('exit', function cluExit(code, signal) {...});
return clu.once('online', function () {...});
});
} else {
God.forkMode(env_copy, function forkMode(err, clu) {
if (cb && err) return cb(err);
if (err) return false;
var old_env = God.clusters_db[clu.pm2_env.pm_id];
if (old_env) old_env = null;
God.clusters_db[env_copy.pm_id] = clu;
// 下面一堆监听事件
clu.once('error', function cluError(err) {...});
clu.once('exit', function cluClose(code, signal) {...});
});
}
}
复制代码
原来重点在./God/ForkMode.js
中:(省略掉不关心的部分)
module.exports = function ForkMode(God) {
God.forkMode = function forkMode(pm2_env, cb) {
...
var spawn = require('child_process').spawn;
....
var cspr = spawn(command, args, options);
...
}
}
复制代码
原来fork模式下利用了node的child_process.spawn方式运行应用,接下来我们在其中加入log,看一下传入的command, args, options分别是啥:
- commands就是我们通过config文件的interpreter指定的node版本的目录
- args是pm2项目中的processContainerFork.js
- 我没有截全,但是能看出来是一些node执行的环境变量,会在processContainerFork中读取并使用
所以简单来说fork的模式就是执行一个 path/to/node processContainerFork.js env
, 和我们本地执行一个node xxx.js
的方式一致,path/to/node
实现了利用我们配置的node版本去运行app。 另外多说一句,如果你没有配置interpreter
,pm2会去取pm_exec_path
中的内容,就是你在terminal中运行pm2时跑起god daemon
的node版本。
PM2中的cluster模式:
接下来我们看看./God/ClusterMode.js
中的内容:(省略掉不关心的部分)
var cluster = require('cluster');
module.exports = function ClusterMode(God) {
God.nodeApp = function nodeApp(env_copy, cb){
...
var clu = null;
clu = cluster.fork({pm2_env: JSON.stringify(env_copy), windowsHide: true});
...
}
}
复制代码
cluster模式原来是调用了nodejs提供的cluster模式启动一个服务,我们再看下入参重的env_copy都是些什么: 其中的一些内容和上面的fork模式下的env一致,显然cluster的使用方式我们还不是很清楚,它不像child_process那样的node xxx.js
的调用方式,那么node的cluster怎么使用呢?
nodejs中的cluster.fork如何使用?
这里就简单使用node官方文档的demo:
//cluster.js
import cluster from 'cluster';
import http from 'http';
import { cpus } from 'os';
import process from '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 {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
复制代码
代码非常简单,首先你第一次执行这个cluster.js
文件的时候会启动一个进程,该进程作为primary
并标记为isPrimary,之后会根据cpu的数量执行对应次数的cluster.fork
,每次cluster.fork
被调用,相当于这个cluster.js文件又被执行一遍,但是此时执行该cluster.js文件的线程不再是isPrimary的,所以会走else中的内容,最终的log如下 没错是我来秀我的12核电脑的。
cluster模块其实是封装的child_process,在http服务中,cluster模块会自动建立一个master-slave的架构,master进程会将收到的request自动分发给slave进程,父子进程通过ipc进行通讯。至于如何讲任务,官方文档中有提到两种方式:1.round-robin。(大学学过来着,忘干净了) 2.master进程建立一个监听socket,然后分发给子进程。
回到问题
我们之前的问题是发现在cluster模式下我们配置的node版本并没有生效,结合了cluster模块的使用和pm2中的源码分析可知:
当模式是在cluster的时候,首先会通过setupMaster
来配置exec
参数(在god.js中),来决定要反复执行的js文件,再根据配置的instances数量去决定执行多少次。
//下面代码在god.js中
cluster.setupMaster({
windowsHide: true,
exec : path.resolve(path.dirname(module.filename), 'ProcessContainer.js')
});
复制代码
所以在./God/ClusterMode.js
中cluster.fork反复执行的就是ProcessContainer.js
文件, 而在processContainer中并没有像是在Fork模式下
指定node的执行目录,而是直接使用的process.versions
延用God.js执行时的node实例,也就是你运行pm2 start
时候第一次启动God daemon
时的node版本。
Таким образом, мы наконец-то знаем, почему служба, запущенная на порту 8000 в демонстрации, запустит службу с node16, когда интерпретатор настроен как node10.
В заключение
Благодаря этой статье я узнал о некоторых механизмах в pm2 в режиме форка и кластера и, наконец, пришел к выводу, что в кластерном режиме pm2 действует не настроенная версия узла, а версия узла, запускающая службу pm2 для первый раз. , то есть определяется версия узла, на котором работает демон God.
ссылки: # Одиночный поток против дочернего процесса против рабочих потоков против кластера в nodejs