Node.js
Express

node + express で Graceful Shutdown 実装でハマったので共有

More than 1 year has passed since last update.

Node.js + Express 4 で graceful 対応でハマったので共有。

■なにが起こったのか

Ctrl+C で安全に終了するように実装したが、なぜか終了しない。

■結論

接続を切ってから終了する機能追加することで実現。

var http = require('http');
var server = http.createServer(app);

function wireUpServer(/*httpServer*/ server) {
    var connections = {};
    server.on('connection', function(conn) {
        var key = conn.remoteAddress + ':' + conn.remotePort;
        connections[key] = conn;
        conn.on('close', function() {
            delete connections[key];
        });
    });

    server.destroy = function(cb) {
        server.close(cb);
        for (var key in connections)
            connections[key].destroy();
    };
}
wireUpServer(server);

(上記ソースは no way to shutdown a server cleanly より)

としておいて、

const gracefulShutdown = function() {
    server.destroy(() => {
        process.exit();
    });
};
process.on('SIGTERM', gracefulShutdown); // for kill
process.on('SIGINT', gracefulShutdown); // for Ctrl+C

とすることで実現できる。

なお、この場合の graceful とは下記である:

  • Shutdown 開始したら新しい閲覧は受け付けない。
  • まさにアクセス中のユーザがいたら強制切断する。
  • DBなどは(接続がなくなるので)安全に終われるはず。

■下記だと何が問題なのか。

通常なら下記の方法でできそうである。

const gracefulShutdown = function() {
    server.close(function () {
        process.exit();
    });
};
process.on('SIGTERM', gracefulShutdown); // for kill
process.on('SIGINT', gracefulShutdown); // for Ctrl+C

しかし、下記に遭遇する:

  • Chrome で閲覧している場合、タブを落とすだけでなく、ブラウザ自体を落とすまでコネクションが繋がり続ける(内部的なconnection数がブラウザ自体を落とすまで減らない)。30秒ほど待てば終了するのだが、Chrome をリロードし続ける限りずっと繋がる(とはいえその間、新規のアクセスは防がれる)。 ※no way to shutdown a server cleanly によれば SocketIO 側のバグのよう。
  • curl でのアクセスなら期待通りの動き(レスポンスと同時に切断扱い)。

使えなくはないが、Chrome でテストしてて、終了のたびに30秒待たされたりとかやりづらいし、本番でアクティブにアクセスしてるユーザがいるとずっと終了できないので、この方法は使えない。

■何が起こっているのか

server.close(function () {
    process.exit();
});

で渡したコールバックが実行されるのは、すべてのコネクションが終了したときである。
ちなみに今どれくらいのコネクションが繋がっているのかは下記を仕込むと解る。

繋がっているコネクション数を知る方法
setInterval(() => {
    console.log(`++++++++++++++++++++++++ ${server._connections}`);
}, 500);