调研了一下 pm2 的 reload 功能。pm2 的很多功能都没有在文档中体现,这一点不太好。
需要对照着代码看,源码阅读文章都只是个记录,只看文章肯定看不懂。pm2 版本 4.5.6
。
TLDR:直接看 hardReload
CLI 命令行工具使用 commander 来实现,调用的是 PM2 类中的 reload 方法:
1 2 3 4 5 commander.command('reload <id|name|namespace|all>' ) .description('reload processes (note that its for app using HTTP/HTTPS)' ) .action(function (pm2_id ) { pm2.reload(pm2_id, commander); });
PM2.reload PM2 类在文件 lib/API.js
中。
检查目前是否已经在 reload 了 1 2 3 4 5 var delay = Common.lockReload();if (delay > 0 && opts.force != true ) { Common.printError('xxx); return cb ? cb(new Error(' Reload in progress')) : that.exitCli(conf.ERROR_EXIT); }
如果第二个参数是配置文件,会进行配置文件的解析 1 2 3 4 5 6 7 if (Common.isConfigFile(process_name)) that._startJson(process_name, opts, 'reloadProcessId' , function (err, apps ) { Common.unlockReload(); if (err) return cb ? cb(err) : that.exitCli(conf.ERROR_EXIT); return cb ? cb(null , apps) : that.exitCli(conf.SUCCESS_EXIT); });
由于我们重点关注 reload 过程,解析部分就不看了,直接到 else 部分。
调用 _operate 方法,action_name 为 reloadProcessId 1 2 3 4 5 6 7 that._operate('reloadProcessId' , process_name, opts, function (err, apps ) { Common.unlockReload(); if (err) return cb ? cb(err) : that.exitCli(conf.ERROR_EXIT); return cb ? cb(null , apps) : that.exitCli(conf.SUCCESS_EXIT); });
所以其实主要的操作都是在 _operate 中进行的。
_operate 开头处理了一堆环境变量,可以先跳过。
之后创建了一个 processIds 函数,该函数就是真正执行 action 的地方。
后面有几个 if 判断,是为了兼容 pm2 reload 后面跟正则、名称等情况,最终都是要转成 processId,再调用 processIds 函数处理。
所以我们直接看 processIds 函数。
processIds 里面使用了 async/eachLimit 来遍历执行所有的 id,并且限制了同时执行的并发数。
1 2 3 4 5 6 7 8 9 10 11 12 13 function processIds (ids, cb ) { Common.printOut(conf.PREFIX_MSG + 'Applying action %s on app [%s](ids: %s)' , action_name, process_name, ids); if (ids.length <= 2 ) concurrent_actions = 1 ; if (action_name == 'deleteProcessId' ) concurrent_actions = 10 ; eachLimit(ids, concurrent_actions, function (id, next ) { }, function (err ) { }); }
这里面的 concurrent_actions 代码的就是同时有几个进程在重启。
eachLimit 第一个参数为需要遍历的数组,第二个参数为并发数,第三个参数为要执行的函数,最后一个参数为执行完毕后的回调。
所以直接看第三个参数的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function (id, next ) { var opts; if (action_name == 'restartProcessId' || action_name == 'reloadProcessId' || action_name == 'softReloadProcessId' ) { } else { } that.Client.executeRemote(action_name, opts, function (err, res ) { return next(); }); }
前面又是一些参数的处理,直接看 that.Client.executeRemote
。
executeRemote 用于 Client 给 God 发消息,pm2 中 God 是最终控制子进程的类 。
executeRemote 方法在 lib/Client.js
中定义,做了一些初始化操作,直接关注重点,就是这句:
1 return self.client.call(method, app_conf, fn);
这里面的 client 指的是 rpc 的 client,容易混淆。接收消息的自然就是 rpc 的 server,这个 server 在 lib/Daemon.js
中进行定义,暴露了 God 上的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server.expose({ startProcessId : God.startProcessId, stopProcessId : God.stopProcessId, restartProcessId : God.restartProcessId, deleteProcessId : God.deleteProcessId, sendLineToStdin : God.sendLineToStdin, softReloadProcessId : God.softReloadProcessId, reloadProcessId : God.reloadProcessId, });
现在看 God.reloadProcessId
就可以了!
God.reloadProcessId 其实到这里才真正开始
God.reloadProcessId 在 lib/God/Reload.js
中进行定义。
这里面给 God 加了两个方法,softReloadProcessId
和 reloadProcessId
。softReloadProcessId
会等待进程同意才会 reload。
pm2 reload
执行的就是 reloadProcessId
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 God.reloadProcessId = function (opts, cb ) { var id = opts.id; var env = opts.env || {}; if (!(id in God.clusters_db)) return cb(new Error ('PM2 ID unknown' )); if (God.clusters_db[id].pm2_env.status == cst.ONLINE_STATUS && God.clusters_db[id].pm2_env.exec_mode == 'cluster_mode' ) { Utility.extend(God.clusters_db[id].pm2_env.env, opts.env); Utility.extendExtraConfig(God.clusters_db[id], opts); var wait_msg = God.clusters_db[id].pm2_env.wait_ready ? 'ready' : 'listening' ; return hardReload(God, id, wait_msg, cb); } else { console .log('Process %s in a stopped status, starting it' , id); return God.restartProcessId(opts, cb); } };
好吧,又调用了 hardReload,传入了对应的进程 id。
hardReload 这个函数是最终执行 reload 的方法,他做了以下几件事情:
根据要重启的进程 id,生成一个新的 key:'_old_' + id
。
把旧进程的索引 key,从索引中删掉,替换成新的 key。
复制当前进程的参数,调用 executeApp
方法新建一个进程。
进程创建完成后,调用 deleteProcessId
方法将旧进程删除。