PM2简介
PM2 是一个 基于 node.js 的进程管理工具,本身 node.js 是一个单进程的语言,但是 PM2 可以实现多进程的运行及管理(当然还是基于 node 的 API),还提供程序系统信息的展示,包括 内存、CPU 等数据。
前段时间正好有空,想着用了这么多年的PM2,PM2究竟是怎样做到无入侵代码实现多核的利用的呢?所以趁势看了一下PM2的源码。本文使用的PM2版本是4.0.0。
本文以PM2启动实例过程为例,介绍PM2的模块划分,模块间调用关系及进程创建过程。
PM2源码结构
先来一张架构图
-
- Daemon.js
- 守护进程的主要逻辑实现,包括 rpc server,以及各种守护进程的能力
-
- God.js
- 业务进程的包裹层,负责与守护进程建立连接,以及注入一些操作,我们编写的代码最终是由这里执行的
-
- Client.js
- 执行 PM2 命令的主要逻辑实现,包括与守护进程建立 rpc 连接,以及各种请求守护进程的操作
-
- API.js
- 各种功能性的实现,包括启动、关闭项目、展示列表、展示系统信息等操作,会调用 Client 的各种函数
-
- 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启动的大致流程图如下: