PM2源码浅析

901 阅读3分钟

3b6061092aac0a5c56fd9e04551e36f0.jpeg

PM2简介

PM2 是一个 基于 node.js 的进程管理工具,本身 node.js 是一个单进程的语言,但是 PM2 可以实现多进程的运行及管理(当然还是基于 node 的 API),还提供程序系统信息的展示,包括 内存、CPU 等数据。
前段时间正好有空,想着用了这么多年的PM2,PM2究竟是怎样做到无入侵代码实现多核的利用的呢?所以趁势看了一下PM2的源码。本文使用的PM2版本是4.0.0。
本文以PM2启动实例过程为例,介绍PM2的模块划分,模块间调用关系及进程创建过程。

PM2源码结构

先来一张架构图

image.png

    1. Daemon.js
    • 守护进程的主要逻辑实现,包括 rpc server,以及各种守护进程的能力
    1. God.js
    • 业务进程的包裹层,负责与守护进程建立连接,以及注入一些操作,我们编写的代码最终是由这里执行的
    1. Client.js
    • 执行 PM2 命令的主要逻辑实现,包括与守护进程建立 rpc 连接,以及各种请求守护进程的操作
    1. API.js
    • 各种功能性的实现,包括启动、关闭项目、展示列表、展示系统信息等操作,会调用 Client 的各种函数
    1. binaries/CLI.js
    • 执行 pm2 命令时候触发的入口文件

PM2启动

pm2 start 启动pm2,就会执行bin目录下的pm2这个文件,随后require到lib/binaries/CLI.js这个文件,并开始执行。把这个文件分为3个步骤,初始化pm2对象、connect、处理start指令。

1、初始化pm2对象

在实例化pm2的过程中,也在这个实例上挂载了client属性,这些属性在后续会使用到,这里跳过

this.Client = new Client({
  pm2_home: that.pm2_home,
  conf: this._conf,
  secret_key: this.secret_key,
  public_key: this.public_key,
  daemon_mode: this.daemon_mode,
  machine_name: this.machine_name
});

2、connect

这里主要是ping Deamon进程,若不存在Deamon进程,则创建Deamon进程并与之建立rpc连接
入口就是在lib/binaries/CLI.js中执行的pm2.connect方法

  pm2.connect(function() {
    //省略
  });

这个方法内部调用了client.start方法,简写如下:

  this.pingDaemon(function(daemonAlive) {
    // 省略
    that.launchDaemon(function(err, child) {
      // 省略
      that.launchRPC(function(err, meta) {
        return cb(null, {
          daemon_mode      : that.conf.daemon_mode,
          new_pm2_instance : true,
          rpc_socket_file  : that.rpc_socket_file,
          pub_socket_file  : that.pub_socket_file,
          pm2_home         : that.pm2_home
        });
      });
    });
  });

正如函数名,这三个函数的作用分别是尝试连接Daemon进程,创建Daemon进程,建立通讯。我们重点关注launchDaemon函数,这个函数通过child_process的spawn方法,在子进程中执行Daemon.js,创建rpc通讯服务,并通过expose的方式与God的方法进行关联。

  var ClientJS = path.resolve(path.dirname(module.filename), 'Daemon.js');
  // 省略
  node_args.push(ClientJS);
  // 省略
  var child = require('child_process').spawn(interpreter, node_args, {
    detached   : true,
    cwd        : that.conf.cwd || process.cwd(),
    env        : util._extend({
      'SILENT'      : that.conf.DEBUG ? !that.conf.DEBUG : true,
      'PM2_HOME'   : that.pm2_home
    }, process.env),
    stdio      : ['ipc', out, err]
  });
Daemon.prototype.innerStart = function(cb) {
  // 省略
  this.rep    = axon.socket('rep');
  var server = new rpc.Server(this.rep);
  this.rpc_socket = this.rep.bind(this.rpc_socket_file);
  // 省略
  server.expose({
    killMe: that.close.bind(this),
    ...
  });

至此connect流程大致就介绍完了,接下来就进入执行输入指令的阶段了

3、处理start指令

入口还是在lib/binaries/CLI.js,源码使用commander处理输入指令

commander.command('start [name|file|ecosystem|id...]')
  .action(function(cmd, opts) {
    if (cmd == "-") {
      // 省略
    }
    else {
      // 省略
      forEachLimit(cmd, 1, function(script, next) {
        pm2.start(script, commander, next);
      }, function(err) {
           ...
      });
    }
  });

start函数主要分为两块,_startJson_startScript分别处理文件配置项和输入配置项,以_startScript为例,用rpc的方式调用到God的prepare方法,然后调用到executeApp方法,在该方法中对业务进程的不同模式做了不同的处理,cluster_mode模式采用cluster.fork启动业务进程,否则采用child_process模块的spawn方法启动业务进程

_startScript (script, opts, cb) {
    that.Client.executeRemote('prepare', resolved_paths, function(err, data) {
        // 省略
    });
}

God.executeApp = function executeApp(env, cb) {
  if (env_copy.exec_mode === 'cluster_mode') {
    /**
     * Cluster mode logic (for NodeJS apps)
     */
    God.nodeApp(env_copy, function nodeApp(err, clu) {
         ...
    });
  }
  else {
    /**
     * Fork mode logic
     */
    God.forkMode(env_copy, function forkMode(err, clu) {
        ...
    })
  }
}  

源码看到这就完了吗?当然没有,还有一个疑问。为什么cluster只需要fork就行了?为什么没有看到我们脚本的执行?
答案就在God.js的最上部,有一段对cluster初始化的代码

/**
 * Override cluster module configuration
 */
cluster.setupMaster({
  windowsHide: true,
  exec : path.resolve(path.dirname(module.filename), 'ProcessContainer.js')
});

配置了fork的执行脚本的默认值,为ProcessContainer.js。
那么在ProcessContainer.js中又是怎么处理的呢?
首先是从环境变量中取出要执行脚本的路径:var script = process.env.pm_exec_path,这个pm_exec_path就是执行脚本的值,是在之前prepare的时候处理好的;然后执行require(script);就会获取并且运行脚本了。

  var script      = pm2_env.pm_exec_path;
  // 省略
  require('module')._load(script, null, true);
});

小结

综上,整个PM2启动的大致流程图如下:

屏幕快照 2021-05-07 上午3.05.01.png