基于 ThinkJS 程序的自动更新

众所周知,PHP 是文件型程序,每次请求访问的时候会去读取 PHP 文件然后执行程序。这种机制的程序做更新是非常简单的,把文件更新了下一次请求读取的就会是最新的程序。和 PHP 文件型程序不同,Node 是常驻“内存型”程序,代码在最开始执行脚本的时候就已经读取,模块和依赖都会缓存在内存中。这样的好处就是减少了请求每次读取的时间,能够更快的响应。但是也注定了不能像 PHP 一样只是简单的更新下文件就能达到更新的目的了。本文就 Firekylin 的在线更新功能来说说 ThinkJS 程序更新的那些事。

重启更新

简单的思路来说,其实只要在更新完文件后同时更新下内存中的“文件”就能完成更新。比较简单的方式就是直接重启进程了,所以之前 Firekylin 的更新流程基本上是如下步骤:

$ cd /var/www/xxx

# 获取最新程序
$ wget http://firekylin.org/release/latest.tar.gz
$ tar zvxf latest.tar.gz

# 替换文件
$ cp -r firekylin/* ./

# 安装依赖
$ npm install
$ rm -rf firekylin latest.tar.gz

# 重启程序
$ pm2 restart pm2.json

所以你能看到在替换文件安装依赖的最后一步还有一个重启进程的命令。如果是自用的程序,这样其实也就足够了。但是 Firekylin 是一款面向大众的开源博客程序,对于用户是使用何种守护进程(pm2, forever, supervisor, systemd 等等)是没办法知道的,这样也就无法得到一个通用的重启命令。

代码热更新

重启的目的是为了更新内存中的缓存,那问题来了,为什么我们不直接更新内存呢?Node 程序是建立在模块之上,所有的模块都是通过 require 加载的。而 require 加载过的模块会缓存在 require.cache 变量中。加载的模块如果存在缓存的话则优先使用缓存中的内容。可以在 Node 的源码 中一窥究竟。

所以我们只要更新文件的同时清除 require.cache 的缓存,那么下一次 Node 就会重新读取文件获得最新的程序。实际上在 ThinkJS 的开发者模式中,也一直都是使用的这种方式进行不重启热更新

不过这种方法仅仅只是更新了模块的代码,对于一些在启动时的初始化操作并没有重新执行,这对于 Firekylin 这种可能会在启动时初始化环境的程序来说,有点不太适合。

自守护重启

排除掉热更新的方案之后又回归到了简单粗暴的重启方案。重启的问题在于进程守护是在程序之上的,程序并不清楚用户到底使用的是那种进程守护工具。那是不是可以让程序自己守护自己,让外部的守护进程守护这个程序的守护呢?答案是可以的,ThinkJS 的 Cluster 模式就可以实现这个需求。

PM2 守护 ThinkJS,ThinkJS 内部父进程守护 Cluster 子进程

当 ThinkJS 开启 Cluster 模式之后,存在 Master 和 Worker 两种进程角色。其中 Master 父进程只做监管和任务分发的功能,将任务实际派发到 Worker 子进程中执行。Master 会监听 Worker 的退出事件,当 Worker 挂掉之后 Master 会立即 fork 一个新的 Worker 出来(代码):

  static http(){
    let nums = think.config('cluster_on');
    if (!nums) {
      this.createServer();
      return this.log();
    }
    if (nums === true) {
      nums = os.cpus().length;
    }
    if (cluster.isMaster) {
      for (let i = 0; i < nums; i++) {
        cluster.fork();
      }
      cluster.on('exit', worker => {
        think.log(new Error(think.locale('WORKER_DIED', worker.process.pid)), 'THINK');
        process.nextTick(() => cluster.fork());
      });
      this.log();
    }else {
      this.createServer();
    }
  }

利用这个机制,我们可以对程序开启 Cluster 模式,并在更新的时候直接让进程自杀掉完成一次“重启”的过程。由于自杀和新开 Cluster 的逻辑都是在程序内部实现,对于外部守护进程是何种工具实现也就不需要关心了。子进程自杀实现代码

/** 重启服务 */
if( cluster.isWorker ) {
  this.success();
  setTimeout(() => cluster.worker.kill(), 200);
}

后记

简单的一段代码让 ThinkJS 实现了进程守护的功能,达到了更新的目的,虽然多进程的功效远非于此。当然如果不是 ThinkJS 的程序,在自己的代码中实现也是非常简单的。

参考资料:

基于 ThinkJS 程序的自动更新》上有 2 条评论

发表评论

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