SocketIO を使ったサーバーを複数のプロセスで起動させる

[SocketIO](http://socket.io/) で HTTP Long Polling を使う場合、[ひとつのコネクションが複数のプロセスにまたがることを許容しない](http://socket.io/docs/using-multiple-nodes/)。

Node の [`cluster` モジュール](https://nodejs.org/api/cluster.html)は [`sticky-session`](https://www.npmjs.com/package/sticky-session) を使うと同じ IP アドレスからの接続を固定のサーバープロセスでクライアントからの要求を処理できる ([スティッキーセッション](http://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/elb-sticky-sessions.html))。だが [PM2 の `cluster` モードは `sticky-session` に対応していない](https://github.com/Unitech/pm2/issues/389)。そのため、SocketIO を使っている場合は PM2 の `cluster` モードが使えない。

`cluster` モードを使わないとひとつのプロセスですべての処理をさせることになり、サーバー資源を余らせてしまう。なのでどうにかして対処したい。

## `fork` モードを使う

PM2 の `fork` モードを使うと複数のプロセスを起動させられる。

PM2 は `fork` モードでプロセスを起動させると環境変数「`NODE_APP_INSTANCE`」(`process.env.NODE_APP_INSTANCE`) にインスタンス ID を入れる。インスタンス ID は `0` からプロセスの数に応じて増える。そのため、基準となるポート番号に環境変数「`NODE_APP_INSTANCE`」の値を加えることにより、複数のポート番号でサーバープロセスを起動できる。

```js:server.js
// ...

var app = express();
var server = http.createServer(app);
var io = socketIO(server);

// ...

var port = 8080 + Number(process.env.NODE_APP_INSTANCE || 0);
server.listen(port);
```

## H2O からリバースプロキシする

[H2O](https://h2o.examp1e.net/) の [Proxy Directives](https://h2o.examp1e.net/configure/proxy_directives.html) は同一ホスト名で異なるポート番号のサーバーにリバースプロキシできない。

そのため、Nginx を介さなければならない。

Nginx の `upstream` ディレクティブに羅列させ、`ip_hash` ディレクティブを使うと `cluster` モジュールと `sticky-session` を使ったときと同様に同じ IP アドレスからの接続を固定のサーバープロセスで処理させられる。

```nginx:/etc/nginx/conf.d/example-app.conf
upstream example-app {
  ip_hash;
  server 127.0.0.1:8080;
  server 127.0.0.1:8081;
  server 127.0.0.1:8082;
  server 127.0.0.1:8083;
}

server {
  listen 10000;

  location / {
    proxy_pass http://example-app;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
```

あとは Nginx に対して H2O からリバースプロキシするだけで良い。

```yaml:/etc/h2o/h2o.conf
hosts:
  "example.com:443":
    listen:
      port: 443
      ssl:
        certificate-file: /etc/letsencrypt/live/example.com/fullchain.pem
        key-file: /etc/letsencrypt/live/example.com/privkey.pem
    paths:
      /:
        proxy.reverse.url: http://127.0.0.1:10000/
        proxy.timeout.io: 10000
        proxy.websocket: ON
```

## 構成図

```plain
                                      +-------------------+
                                      |                   |
                                      | Redis (port 6379) |
                                      |                   |
                                      +---------+---------+
                                                |
          +------------------------+------------+-----------+------------------------+
          |                        |                        |                        |
+---------+---------+    +---------+---------+    +---------+---------+    +---------+---------+
|                   |    |                   |    |                   |    |                   |
| Node (port 8080)  |    | Node (port 8081)  |    | Node (port 8082)  |    | Node (port 8083)  |
|                   |    |                   |    |                   |    |                   |
+---------+---------+    +---------+---------+    +---------+---------+    +---------+---------+
          |                        |                        |                        |
          +------------------------+------------+-----------+------------------------+
                                                |
                                      +---------+---------+
                                      |                   |
                                      | Nginx (port 10000)|
                                      |                   |
                                      +---------+---------+
                                                |
                                      +---------+---------+
                                      |                   |
                                      |   H2O (port 443)  |
                                      |                   |
                                      +-------------------+
```

わたしは HTTP/2 でサーバープッシュが使いたかったのと、brotil を使いたかったため H2O にポート 443 番を返してもらっている。

ALPN に対応している OpenSSL 1.0.2 以降で Nginx をビルドできる環境で、かつサーバープッシュが不要であるのならば、H2O ではなく Nginx にポート 443 番を返してもらえば良いだろう。そのほうが構成もシンプルになり、管理の手間も減る。