pm2中的cluster模式和fork模式
故事起因:
故事的起因是我們專案中在一臺機器利用pm2部署了多個nodejs的專案,但是每個專案需要一個指定的nodejs版本(因為每個專案中依賴不同版本node的c++ addon),例如專案A
需要使用node12
來執行,專案B
中使用node14
來執行。
問題描述:
在進行機器擴容的時候發現新部署的機器上對於本應該用node12
啟動的專案A
死活使用的是node14
來啟動的。
最開始肯定是帶著氣憤的心情去質疑運維擴容的機器為什麼node版本環境和之前的機器不同?之前擴容的時候都沒出現過類似的問題,為什麼這次擴容的機器就會有問題。
運維同學當然很無辜呀~,我們也沒做什麼改動呀,擴容的流程也是規範化的不會出問題才對。
於是前端同學開始了漫長的排查,通過各種渠道排查了諸如:環境變數中的node指向,指向中的node是否安裝,通過CI部署時安裝的依賴內容等等。。。因為所在公司的部署流程及其”成熟“,從申請許可權到排查問題花了很久很久。。
直到我們注意到了pm2中的一個欄位mode
:
於是我嘗試在本地復現這個問題:
復現DEMO
- 我們通過pm2的配置檔案ecosystem.config.js來啟動兩個node server,兩個server的名字分別為node10 app和node14 app,並且script檔案分別為index1.js和index2.js,分別配置對應的啟動的node版本interpreter引數
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路徑 }] }
```js // 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
);
js
// 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
);
```
- 然後我們起一個terminal,pm2 start一下:
- 瀏覽器分別開啟
localhost:8000
和localhost:8001
:
Everything works fine!! 兩個專案分別使用了我們配置的node版本運行了。
- 之後我們稍微修改一下配置檔案改成如下: ```js 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中的原始碼:
js
//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:
js
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
中:(省略掉不關心的部分)
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
中的內容:(省略掉不關心的部分)
```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: ```js //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數量去決定執行多少次。
js
//下面程式碼在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版本。
綜上我們終於得知了在demo中啟動在8000埠的服務為什麼在配置了interpreter為node10的情況下,會用node16啟動服務了。
結論
通過本文了解了pm2中在fork和cluster模式下的一些機制,最終也得出了結論,pm2在cluster模式下,配置的node版本並不會生效,而是由第一次啟動pm2服務的node版本,即God daemon執行所在的node版本決定。
references:# Single thread vs child process vs worker threads vs cluster in nodejs