因为我只关注PM2的核心功能,相对于辅助的功能,我就不多花时间去看了,只要实现了核心功能,辅助功能不看源码,相信我也能实现的。
阅读本文的时候,我默认您已经了解了Node的基本知识和Cluster的用法,对于Cluster不熟悉的,可以查看这篇文章
我选择直接查看PM2前期的代码,前期代码没有多余的技巧和辅助代码干扰,对于库设计者的思路体现的比较好,这个技巧在大家看别的源码的时候也可以使用。我选定的是0.4.10版本。
首先,我们找到程序的入口,也就是在bin目录下的pm2可执行文件。
我们的目标很明确,了解pm2 start app.js
这样的命令的运作方式,那么,就直奔重点,找到start
命令的代码(逻辑执行部分):
commander.command('start <script>')
.description('start specific part')
.action(function(cmd) {
if (cmd.indexOf('.json') > 0)
CLI.startFromJson(cmd);
else // 我们从 pm2 start app.js 这种命令格式看出,应该是调用的下面的方法
CLI.startFile(cmd);
});
直接调用了startFile
方法,主要代码如下:
// ... 其他代码
// appConf 为启动命令配置
Satan.executeRemote('findByScript', {script : appConf.script}, function(err, exec) {
if (exec && !commander.force) {
console.log(PREFIX_MSG_ERR + 'Script already launched, add -f option to force re execution');
process.exit(ERROR_EXIT);
}
Satan.executeRemote('prepare', resolvePaths(appConf), function() {
console.log(PREFIX_MSG + 'Process launched');
speedList();
});
});
首先执行了executeRemote
方法,要说这个方法的作用,就要说道Satan和God这两个对象的意义了。从名称可以看出Satan是邪恶的,God是正义的,Satan负责对实例进行维护操作,比如杀死,重启等,God负责实例的功能。那么executeRemote
方法就是Satan通过rpc调用God的方法。
传入了findByScript参数,其实就是God中的findByScript
方法。
findByScript
方法作用是在God保存的实例缓存中,用脚本名作为key,查找相关脚本的实例,如果找到相关的实例信息,则证明已经启动了当前脚本实例,不需要启动,除非是强制启动,则会报错退出。
在确定了没有相同实例的情况下,又传入了prepare,调用God的prepare
方法:
God.prepare = function(opts, cb) {
if (opts.instances) { // 如果配置了启动实例数量
if (opts.instances == 'max') // 如果配置的是max,则默认启动最大CPU核数相同的实例
opts.instances = numCPUs;
opts.instances = parseInt(opts.instances);
var arr = [];
(function ex(i) {
if (i <= 0) { // 输入校验
if (cb != null) return cb(null, arr);
return true;
}
return execute(JSON.parse(JSON.stringify(opts)), function(err, clu) { // 深度克隆
arr.push(clu);
ex(i - 1); // 递归调用自己
});
})(opts.instances);
}
else return execute(opts, cb);
};
可以看到将参数传入了execute
函数,并且,在execute
函数的回调中,递归了自己,这样就达到了启动多个实例的目的。而这个execute
函数就是核心逻辑了:
function execute(env, cb) {
var id;
if (env.pm_id && env.opts && env.opts.status == 'stopped') {
delete God.clusters_db[env.pm_id];
}
id = God.next_id; // 获取一个新id
God.next_id += 1;
// 对信息更新
env['pm_id'] = id;
env['pm_uptime'] = Date.now();
// First time the script is exec
if (env['restart_time'] === undefined) {
env['restart_time'] = 0;
}
// 使用cluster的fork方法,创建一个worker
var clu = cluster.fork(env);
clu['pm_id'] = id;
clu['opts'] = env;
clu['status'] = 'launching';
// 保存在缓存中
God.clusters_db[id] = clu;
clu.once('online', function() {
God.clusters_db[id].status = 'online';
if (cb) return cb(null, clu);
return true;
});
return clu;
}
execute
函数完善了一下附带的cluster状态信息,然后fork出一个新的worker,将worker存在缓存中,并且处理好上线事件。
源码看到这就完了吗?当然没有,还有很多疑问。为啥cluster只需要fork就行了?为什么没有看到我们脚本的执行?那就带着疑问继续看吧。
继续看God.js,在最上部,引入cluster的时候,有一句对cluster初始化的代码:
cluster.setupMaster({
exec : p.resolve(p.dirname(module.filename), 'ProcessContainer.js')
});
配置了fork的执行脚本的默认值,为ProcessContainer.js,并且紧接着,对cluster的上线和掉线进行了处理,在cluster上线的时候,更新cluster的状态,在cluster掉线退出的时候,对cluster进行自动重启。
那么在ProcessContainer.js中又是怎么处理的呢?
首先是从环境变量中取出要执行脚本的路径:var script = process.env.pm_exec_path;
,这个pm_exec_path
就是执行脚本的值,是在之前prepare的时候处理好的。并且同样在这一步的时候,处理了日志文件的分隔,将各个不同实例的日志输出在不同的文件中。
然后执行require(script);
就会获取并且运行脚本了。
到此,pm2的核心逻辑已经走了一个流程了。其他诸如监控之类的功能,相信有了核心逻辑的基础应该很容易实现。
总结
读完pm2的源码,了解了怎样无入侵代码实现多核的利用,这是一种比较优雅的实现,让我们可以写出一个通用工具而不是框架来做这件事情。
看到这里,我推荐大家都一起去实现一个简单的pm2,看过不代表掌握,一定要动手实现。
0 条评论