Blog

Node.js で重い処理をしてしまったときにタイムアウトするの法

(この記事は Node.js アドベントカレンダー不参加記事です)

チャットサーバー的な使い方とか意外とみんな興味なくて、普通のウェブアプリケーションなどをかく、という用途にちょっと node.js がつかえたらいいのにな、とおもっている人がおおいようにかんじています。Node.js が人気なのは、v8 をうまくパッケージングしているのが node.js ぐらいで、そして v8 をうまくパッケージングするのが結構めんどくさいから、というところが大きいのです。ぶっちゃけ node.js が〜 とさわいでる人のうち8割は I/O multiplexing だからとかそういう理由で支持しているわけではなかったりするのです(偏見)。

さて、普通の web application のようなものを書こうとしたときに Node.js って基本シングルスレッドだし、なんかうっかり重い処理したときにどうしたらいいの? みたいなことをきくと、「ブロックしちゃうような処理とか書かないし!!」みたいなことを顔を真っ赤にしてこたえられるばっかりで、まったく参考にならなかったりする今日この頃でした。
ブロックしないしないといっても、開発にたずさわる人数が増えたりすれば、開発者の品質もそろいずらくなるし、みたいなことを思ってしまう僕は異端なのでしょうか。
重い処理が一箇所にあるだけでサービスが全体的に利用不可能になるのは避けたいのが心情というものです。普通に node.js で処理をかくと、重い処理が一箇所あるだけで、永久にそのプロセスは応答しなくなってしまいます。

最初におもいつく手段としては setTimeout がありますが、非常におもい処理を JS レベルでかいた場合、setTimeout などによるタイムアウト処理は、よばれません。意味がありません。

次に、常套手段である setitimer(2) ですが、node.js では setitimer(2) 的なものを呼ぶ手段はないようです。

じゃあみんなどうしてんの! とおもったわけですが、そうです。 Node.js には cluster がありました。cluster というのは、子プロセスを管理する仕組みのことですが、とくに何にもかんがえなくても TCP port を共有できますので以下のようにかくことができます。また、ワーカープロセスからマスタープロセスにメッセージを簡単におくる手段も用意されているので、これを利用してみました。

リクエストを処理する前にマスタープロセスに通知し、マスタープロセス側で setTimeout を発行。リクエストが終了したらワーカープロセス側からマスタープロセスに通知する。通知がこなければワーカープロセスを殺す。というフローです。

このフローの問題点としては、

といったところがありますね。後者については graceful にできるといいのかなとおもったりしますが、むずかしそうです。

というようなことをかんがえてみましたがどうでしょうか。みなさんもっといい解決策をもって運用されているのでしょうか。

最後に、僕のかんがえた Node.js を安定運用するの仕組みをのっけておきますね。

var cluster = require('cluster');
var http = require('http');
var numReqs = 0;
var numCPUs = require('os').cpus().length;
var REQUESTS_PER_CHILD = 10;
var REQUEST_TIMEOUT = 3000;

if (cluster.isMaster) {
  // Fork workers.
  (function () {
    var pids = {};

    cluster.on('death', function () {
        spawn();
    });

    function spawn() {
        var worker = cluster.fork();
        pids[worker.pid] = true;
        worker.timers = {};

        worker.on('message', function(msg) {
            if (msg.cmd) {
                switch (msg.cmd) {
                case 'notifyRequest':
                    numReqs++;
                    break;
                case 'beg_req':
                    var that = this;
                    console.log('set: ' + this.pid + " : " + msg.i);
                    worker.timers[msg.i] = setTimeout(
                        function () {
                            console.log('timeout : ' + msg.i + " PID: " + that.pid);
                            process.kill(that.pid, 'SIGTERM');
                        }, msg.timeout
                    );
                    break;
                case 'end_req':
                    console.log('clear: ' + this.pid + " : " + msg.i);
                    clearTimeout(worker.timers[msg.i]);
                    delete worker.timers[msg.i];
                    break;
                case 'close':
                    console.log('closed child');
                    break;
                }
            }
        });
    };
    for (var i = 0; i < Math.max(numCPUs-1, 1); i++) {
        spawn();
    }

    setInterval(function() {
        console.log("numReqs =", numReqs);
    }, 1000);
  })();
} else {
  // Worker processes have a http server.
  console.log('spawned worker: ' + process.pid);
  var i=0;
  var server = http.Server(function(req, res) {
    process.send({cmd: 'beg_req', i: i, timeout: REQUEST_TIMEOUT});
    req.on('end', function () {
        console.log('request close');
        process.send({cmd: 'end_req', i: i-1});
    });
    if (req.url == '/slow') {
        while (1) { }
    }

    res.writeHead(200);
    res.end("hello world: " + (i+1) + "\n");
    // Send message to master process
    process.send({ cmd: 'notifyRequest' });

    if (++i>=REQUESTS_PER_CHILD) {
        this.close();
    }
  });
  server.on('close', function () {
    process.send({ cmd: 'close' });
    console.log('close worker: ' + process.pid);
    process.exit(0);
  });
  server.listen(8000);
}