pm2のクラスターモードとフォークモード

物語の理由:

ストーリーの理由は、私たちのプロジェクトでは、pm2を使用して複数のnodejsプロジェクトが1つのマシンにデプロイされていますが、各プロジェクトには指定されたnodejsバージョンが必要です(各プロジェクトは異なるバージョンのノードのc ++アドオンに依存しているため)。プロジェクトは実行Aに使用する必要がnode12あり、プロジェクトは実行にB使用node14します。

問題の説明:

マシンを拡張すると、新しく展開されたマシンが、開始されるべきnode12プロジェクトの開始に使用さAれたことがわかりましたnode14

image.png

最初は、運用・保守拡張のためのマシンのノードバージョン環境が以前のマシンとなぜ違うのか疑問に思ったのではないでしょうか。これまで容量を拡張したときに同様の問題が発生したことはありませんが、今回拡張したマシンに問題があるのはなぜですか?

もちろん、運用・保守の学生はとても無邪気です〜、変更はありませんし、拡張プロセスも標準化されており問題ありません。
そのため、フロントエンドの学生は長い調査を開始し、環境変数のノードポイント、ポイントのノードがインストールされているかどうか、CIを介して展開するときにインストールされている依存関係など、さまざまなチャネルを確認しました。会社の展開プロセスとその「成熟」のために、許可の申請からトラブルシューティングまでに長い時間がかかりました。

pm2のフィールドに気付くまでmode

image.png

そこで、問題をローカルで再現しようとしました。

デモを再現

  • pm2の構成ファイルecosystem.config.jsを介して2つのノードサーバーを起動します。2つのサーバーの名前はそれぞれnode10appとnode14appであり、スクリプトファイルはそれぞれ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`);
复制代码
  • 次に、ターミナルpm2startを開始します。

image.png

  • ブラウザが開きlocalhost:8000localhost:8001

image.png

すべて正常に動作します!!両方のプロジェクトは、構成されたノードバージョンで実行されています。

  • 之后我们稍微修改一下配置文件改成如下:
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正常:

image.png

浏览器访问两个端口号: image.png 一个是v16.13.0,一个是v14.18.1?我们明明设置的是v10.0.0,这个v16.13.0是哪里来的?

image.png

茶泡好,烟点起,让我们一步一步来

instances配置:

首先当然是查看instance这个配置是什么意思,干啥用的,配置了instance进入的cluster模式又是什么?pm2 cluster模式,简单理解下来,cluster模式就是让你的服务尽可能的利用你的计算机性能(如多个cpu),创建多个子进程,均衡服务的负载,在不改变代码的前提下尽可能大的提升服务的性能,而instances就是允许你的服务可以用上几个cpu。

image.png 说到这里我们看下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我们来仔细看下:

image.png 忽略最后的一个grep命令,有三个和pm2相关的命令,其中第一个叫做God Daemon,之后的两个就是我们对应的两个node-server(node10 app和node14 app)。当我们pm2 delete 0或者pm2 delete 1对应kill掉的线程其实是后面两个,而god daemon是伴随pm2启动的,所谓的master process(老外现在叫做primary process)。我们试下: image.png 所以我们的项目在进行pm2 start命令时,所有的进程如下:

image.png 这也是上面我们为什么执行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分别是啥: image.png

  • 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都是些什么: image.png 其中的一些内容和上面的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如下 image.png 没错是我来秀我的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​​で開始されたサービスが、インタープリターがnode10として構成されている場合に、node16でサービスを開始する理由がついにわかりました。

結論は

この記事を通じて、フォークおよびクラスターモードでのpm2のいくつかのメカニズムについて学び、最終的に、pm2のクラスターモードでは、構成されたノードバージョンは有効にならず、初めて、つまり、Godデーモンが実行されるノードバージョンが決定されます。

参照:#nodejsのシングルスレッドvs子プロセスvsワーカースレッドvsクラスター

おすすめ

転載: juejin.im/post/7085270158381416462