因为我只关注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,看过不代表掌握,一定要动手实现。
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。