yo_waka's blog

418 I'm a teapot

Node.jsのcluster.disconnectの挙動とGracefulリスタート

clusterモジュールを使ってサーバアプリ管理ツールを作ってるのだけど、cluster.disconnectのコールバックが実行されてもワーカープロセスが死なないことがある。
Chromeでサーバにアクセスすると、cluster.disconnectしてもワーカーの'exit'イベントが発火されずにプロセスが残ってしまう。

しばらく放置しておくとワーカープロセスが死ぬんだけど、これはどういうことだと思ったらこういうことらしい。keep-aliveかー
タブを閉じてもダメ。どうもChromeのkeep-aliveのタイムアウト時間は5分みたいですね。
先の記事でいうところの、「すべてのコネクションが終了し、サーバが 'close' イベントを発したときに最終的に閉じます。」という実装をちゃんとしていないサーバの場合、5分経つまで接続が閉じない。

サーバ側の実装がよくないのが根本的な原因なんだけど、それはそれとして、そんなサーバでもcluster.disconnectを使ってGracefulな再起動をやりたい場合はどうしよう。

いろいろ考えてみたけど、ワーカーを個別に監視して一定時間後に死んでいなければ強制的にワーカーのdestroyを呼んでやるのがいいのかなーと思った。
clusterの'disconnect'イベントハンドラでタイマーでワーカーのdestroyを呼ぶ処理をしておいて、'exit'イベントハンドラでタイマーをクリアしてあげればいい。

ざっくりとこんな感じ。

var forks = 3;
var destroyTimers = {};

cluster.on('exit', function(worker, code, signal) {
  var timer = destroyTimers[worker.id];
  if (timer) {
    clearTimeout(timer);
    delete destroyTimers[worker.id];
  }
});

cluster.on('disconnect', function(worker) {
  destroyTimers[worker.id] = setTimeout(function() {
    worker.destroy();
  }, 5000);
});

// ワーカー再起動
function restart() {
  killWorkers();
  forkWorkers(function(err) {
    if (err) {
      return;
    } 
    console.log('Restarted workers');
  });
}

function forkWorkers(opt_callback) {
  var domain = Domain.create();

  domain.on('error', function(err) {
    var errMessage = 'Failed to start new workers.\n' + err.message;
    console.error(errMessage);
    opt_callback && opt_callback(new Error(errMessage));
  }); 

  domain.run(function() {
    for (var i = 0; i < forks; i++) {
      cluster.fork();
    }   
    opt_callback && opt_callback();
  });
}

function killWorkers(opt_callback) {
  var domain = Domain.create();

  domain.on('error', function(err) {
    var errMessage = 'Failed to disconnect workers.\n' + err.message;
    console.error(errMessage);

    // 強制的にワーカーをdestroyするメソッド
    killWorkersForce(opt_callback);
  });

  domain.run(function() {
    cluster.disconnect(function() {
      opt_callback && opt_callback();
    });
  });
}

これでrestartメソッドで古いワーカーが遅くとも5秒後にはすべて死ぬようになった。

コールバックもちゃんとやるなら、全部のワーカーが立ち上がったか/死んだかをチェックして実行しないとダメ ('listening'か'online'イベントハンドラが何回呼ばれたかチェックする感じ?)。

cluster.forkでエラーになった際にDomainsのエラーハンドラで何もしていないのは、この場合ワーカーのスタートアップ処理に致命的な不具合があると思われるから。
コンソール上で派手にエラーアピールしてあげる。

タイマーのインターバルは、サーバアプリの応答時間にもよるので設定で変えられるようにするのがよさげ。