Socket.IO(v1.3.5)+Redis+pm2+NginxでWebSocketサーバをスケールさせる

# サンプルコード
新バージョン:[Socket.IO(v1.3.5) + pm2(forkモード)](https://github.com/geekduck/socketio-scale-sample/tree/v1.3.5)
旧バージョン:[Socket.IO(v0.9) + pm2(clusterモード)](https://github.com/geekduck/socketio-scale-sample/tree/v1.3.5)


# 前置き
Socket.IOのv1.0でRedisを使ったスケールの仕方が変わり、
pm2のclusterモード(Node.jsのclusterモジュール)を使っていたv0.9用のスケールの仕方がそのままでは通用しなくなりました。

v0.9用のスケールの仕方に関する参考情報
[Socket.IO, Redisを使用し各ゲーム間でプッシュ通知するシステム](https://www.cyberagent.co.jp/techinfo/techreport/report/id=8858)
[Node.js + Socket.IO + pm2でデーモン化とクラスタリング](http://tomowatanabe.hatenablog.com/entry/2014/01/31/124740)
[Express / Socket.IO をスケールアウトしてみよう](https://gist.github.com/nulltask/89e6f36e194c951697a0)

# 目的
最新のSocket.IO(v1.3.5)と[pm2](https://github.com/Unitech/PM2)を使ってWebSocketサーバをスケールさせる方法を調べました。

本家の[ドキュメント](http://socket.io/docs/using-multiple-nodes/#)を見ると、
  1. Nginxのip_hashを使う
  2. [sticky-session](https://github.com/indutny/sticky-session)のようなモジュールを使ってサーバサイドコードレベルでstickyロードバランスさせる
のどちらかのアプローチがありそうです。

Socket.IO v0.9とv1.0以降との違いは、v0.9ではリクエストがどのサーバプロセスで処理されてもSocket.IOがうまく扱ってくれたのに対し、
v1.0以降ではNginxかサーバサイドコードでリクエストを同じサーバプロセスで処理させる必要があります。

今回は1.のNginxを使ってロードバランスさせる方法を解説します。
これは既にSocket.IO v0.9+pm2(clusterモード)でスケールさせていたコード(サンプルコードの旧バージョンみたいなやつ)があり、
サーバサイドのコードにあまり手を入れずにSocket.IO v1.3.5に対応させたかったからです。

# 構成
* Node.js v0.12.5
* Socket.IO v1.3.5
* socket.io-redis v0.1.4
* PM2 v0.14.2
* Redis v3.0.2
* Nginx v1.8.0

すべての設定は手元のMacでlocalhost前提で設定しています。
Nginx,Node,Redisが動くマシンが異なる場合などは環境に合わせて適宜変更してください。

# PM2のモードはclusterかforkか?

本家のドキュメントにある通り、Node.jsのclusterモジュールを使う場合はsticky-sessionなどを使うようにサーバサイドのコードを変更する必要があります。
従って残念ながらpm2のclusterモードは今のところ使えません。
どのワーカープロセスにリクエストが処理されるかわからないためです。

pm2のforkモードで別々のportを割り当てたWebSocketサーバを起動し、Nginxでロードバランスさせます。
pm2のforkモードでCPU数に応じて動的にサーバのプロセスを起動する方法はこちらの投稿を参照してください。
[pm2のforkモードで動的にCPUと同じ数のhttpサーバを起動する](http://qiita.com/geek_duck/items/8348ef5f230c5ce49bc2)


# server.jsとpm2の設定ファイル
以下のようにサーバサイドコードとpm2の設定ファイルを書いて ``` $ pm2 start process.json ``` と実行するとCPUの数だけport8080〜808xに割り当てられたWebSocketサーバが起動します。
 
```js:server.js
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379}));

var port = 8080 + parseInt(process.env.NODE_APP_INSTANCE || 0); // CPU数と同じ個数のプロセスが起動する

app.get('/', function(req, res) {
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', function(socket) {
  socket.on('chat message', function(msg) {
    io.emit('chat message', "port:" + port + " user:" + socket.id + " msg:" + msg);
  });
});

http.listen(port, function() {
  console.log('listening on *:' + port);
});

```

```json:process.json
{
  "name": "socketio-scale-sample",
  "script": "server.js",
  "exec_mode": "fork_mode",
  "instances": 0
}

```

# Nginxでロードバランス

本家の[ドキュメント](http://socket.io/docs/using-multiple-nodes/#nginx-configuration)と同じように設定します。

```nginx:conf/conf.d/default
upstream io_nodes {
  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 80;
  server_name localhost;
  location / {
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_http_version 1.1;
    proxy_pass http://io_nodes;
  }
  # 〜省略〜
}
```

# スケールされてるか試す

Nginxとpm2を起動してhttp://localhost/ にアクセスすればWebSocketを使ったチャットアプリにつながります。

何かコメントを書くと「port:リクエストを処理したWebSocketサーバのポート user:Socket.IOのID msg:チャットメーリングリスト」の形式でメッセージが表示されます。
アプリを起動しているマシンとは別のIPアドレスを持つPC,タブレット,スマホなどからアクセスすれば、portが異なるサーバプロセスで処理されていることがわかります。![socketio.png](https://qiita-image-store.s3.amazonaws.com/0/16039/957c7605-3c5f-71d5-e358-70674f048bb6.png)

同じマシン上の別ブラウザや別タブでアクセスしても、IPが同じため常に同じWebSocketサーバで処理されることに注意してください。

# さらなるスケール

もしさらにWebSocketサーバを増やす必要に迫られたら、
WebSocket用のサーバマシンを追加し、
以下のようにNginxの設定を変えれば良いと思います。

```nginx:conf/conf.d/default
upstream io_nodes {
  ip_hash;
  # WebSocketサーバ1用の設定
  server server1:8080;
  server server1:8081;
  server server1:8082;
  server server1:8083;

  # WebSocketサーバ2用の設定
  server server2:8080;
  server server2:8081;
  server server2:8082;
  server server2:8083;
  # ・・・
  # WebSocketサーバN用の設定
  server serverN:8080;
  server serverN:8081;
  server serverN:8082;
  server serverN:8083;
}
```

io_nodesの設定ファイルの行数がスゴイことになりそうですね・・・。
このあたりはもっと考える必要があるかもしれません。

# Socket.IOv0.9 + pm2(clusterモード)との変更点

pm2のclusterモードで起動できなくなったため、pm2の[Graceful reload](https://github.com/Unitech/PM2/blob/master/ADVANCED_README.md#graceful-reload)は使えません。
pm2のGraceful reloadに頼るような運用をしていた場合は、pm2のrestartで問題がないか調査・確認が必要だと思います。

# まとめ
Socket.IO(v1.3.5) + pm2でスケールするWebSocketサーバを構築するとこんな感じになると思います。

Socket.IO(v1.3.5) + pm2を使ってスケールさせる情報は少ないのでこれがベストかはわかりませんがちょっとでも参考になれば幸いです。
突っ込みどころがあれば遠慮なくお願いします！