因为我只关注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 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注