(小ネタ)Node.jsのWebアプリでclusterを使いながら定期的に子プロセスを再起動させる


Node.jsでサーバサイドWebアプリを開発中、なぜかメモリリークがあるライブラリに遭遇してしまったので、ワークアラウンドとして、定期的にプロセスを再起動させて、メモリリークの問題を緩和したいと思いました。

サーバサイド

http://0.0.0.0:10080 をリスンするプロセスが4つ立ち上がり、5秒未満で子プロセスを停止させ、その後に再起動します。(実用上は、もっと長い時間でプロセスを殺すべきです。)

src/index.js
const cluster = require("cluster");
const http = require("http");

const sleep = time => new Promise(done => setTimeout(done, time));

const clusterCount = 4;
const portNumber = 10080;

if (cluster.isMaster) {
  const spawnProcess = () => {
    // プロセスを終了させるまでの時間: 0 〜 5000 msec
    const ttl = ~~(5000 * Math.random());
    const child = cluster.fork();
    let timeout;

    child.on("listening", () => {
      // 指定時間で終了(Graceful kill)させる
      console.log(`誕生! 死まで ${ttl} msec.`);
      timeout = setTimeout(() => {
        console.log(`死: ${child.id}`);
        child.kill();
      }, ttl);
    });
    child.on("disconnect", () => {
      // 別の理由で死んだ場合はkillをキャンセル
      if (timeout) {
        clearTimeout(timeout);
      }
    });
    child.on("exit", () => {
      // 子プロセスが終了したら代わりのものを1つ起動する
      spawnProcess();
    });
  };

  // 子プロセスを複数起動する
  for (let i = 0; i < clusterCount; i++) {
    spawnProcess();
  }
}
if (cluster.isWorker) {
  // Express や Koa など好きに使いましょう
  http
    .createServer(async (req, res) => {
      // リクエスト終了までやや時間がかかる設定
      await sleep(1000);
      res.writeHead(200);
      res.end("Request done\n");
    })
    .listen(portNumber);
}

起動すると、下記のようなログを吐きながら、プロセスの終了と生成を延々と繰り返します。

誕生! 死まで 2712 msec.
誕生! 死まで 3984 msec.
誕生! 死まで 4297 msec.
誕生! 死まで 1547 msec.
死: 4
誕生! 死まで 4276 msec.
死: 2
:
:

テスト

ab コマンドできちんとリクエストが中断されずにいるか、テストしてみます、

$ ab -c 20 -n 500 http://localhost:10080/
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Finished 500 requests


Server Software:        
Server Hostname:        localhost
Server Port:            10080

Document Path:          /
Document Length:        13 bytes

Concurrency Level:      20
Time taken for tests:   26.226 seconds
Complete requests:      500
Failed requests:        0
Total transferred:      44000 bytes
HTML transferred:       6500 bytes
Requests per second:    19.07 [#/sec] (mean)
Time per request:       1049.029 [ms] (mean)
Time per request:       52.451 [ms] (mean, across all concurrent requests)
Transfer rate:          1.64 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.0      0       7
Processing:  1000 1008   5.4   1007    1025
Waiting:     1000 1007   4.7   1006    1023
Total:       1000 1008   5.5   1007    1025

Percentage of the requests served within a certain time (ms)
  50%   1007
  66%   1010
  75%   1012
  80%   1013
  90%   1016
  95%   1019
  98%   1020
  99%   1022
 100%   1025 (longest request)

特に何も問題なくリクエスト処理は完了しているようです。

Complete requests:      500
Failed requests:        0

まとめ

  • cluster でマルチプロセス化できるし、定期的にプロセスを再起動して、健全性を保つことができるはず
    • こういうゴミ掃除はマルチスレッドモデルだとできなさそう