Socket.ioサーバをスケールアウトする

[Socket.ioでコンソールベースのチャットを作ってみる](http://qiita.com/takehilo/items/dd64d999058cb1dd2c79)で作ったサーバをスケールアウトしたい。
Node.jsプロセスのスケールアウトといえばclusterなので、これを使って単純にスケールアウトしてみる。

```index.js
const os = require('os');
const cluster = require('cluster');
const io = require('socket.io')();

if (cluster.isMaster) {
  os.cpus().forEach(() => {
    const worker = cluster.fork();
    console.log('CLUSTER: Worker %d started', worker.id);
  });

  cluster.on('exit', () => {
    const worker = cluster.fork();
    console.log('CLUSTER: Worker %d started', worker.id);
  });
} else {
  io.listen(3000);

  io.on('connection', (socket) => {
    console.log(`connected, id: ${socket.id}`);

    socket.on('chat message', (user, message) => {
      data = `${message} from ${user}`;
      console.log(data);
      socket.broadcast.emit('chat message', data);
    });

    socket.on('disconnect', () => {
      console.log(`disconnected, id: ${socket.id}`);
    });
  });
}
```

サーバを起動する。

```
$ node index.js
CLUSTER: Worker 1 started
CLUSTER: Worker 2 started
CLUSTER: Worker 3 started
CLUSTER: Worker 4 started
```

クライアントから接続する。
※クライアントのプログラムは[こちら](http://qiita.com/takehilo/items/dd64d999058cb1dd2c79)。

```
$ node client.js UserA
Error: { Error: xhr poll error
    at XHR.Transport.onError (/Users/takehiro/Documents/git/socketio-app/node_modules/engine.io-client/lib/transport.js:64:13)
    at Request.<anonymous> (/Users/takehiro/Documents/git/socketio-app/node_modules/engine.io-client/lib/transports/polling-xhr.js:129:10)
    at Request.Emitter.emit (/Users/takehiro/Documents/git/socketio-app/node_modules/engine.io-client/node_modules/component-emitter/index.js:133:20)
    at Request.onError (/Users/takehiro/Documents/git/socketio-app/node_modules/engine.io-client/lib/transports/polling-xhr.js:307:8)
    at Timeout._onTimeout (/Users/takehiro/Documents/git/socketio-app/node_modules/engine.io-client/lib/transports/polling-xhr.js:254:18)
    at ontimeout (timers.js:365:14)
    at tryOnTimeout (timers.js:237:5)
    at Timer.listOnTimeout (timers.js:207:5) type: 'TransportError', description: 400 }
disconnected
```

XHR Pollingエラーになってしまう。
Socket.ioクライアントはコネクション確立のハンドシェイク時にいくつかのアクセスを行うが、clusterによってこれらのアクセスが毎回違うサーバプロセスに振り分けられてしまうため、コネクションが確立できないのである。

この問題を解決する方法として、[Socket.IOの公式ドキュメント](http://socket.io/docs/using-multiple-nodes/#using-node.js-cluster)では[sticky-session](https://github.com/indutny/sticky-session)モジュールを使用する方法を紹介している。

sticky-sessionモジュールを使用したサーバの実装がこちら。

```
const cluster = require('cluster');
const io = require('socket.io')();
const sticky = require('sticky-session');
const http = require('http');

const server = http.createServer((req, res) => {
  res.end('worker: ' + cluster.worker.id);
});

io.attach(server);
isWorker = sticky.listen(server, 3000);

if (isWorker) {
  io.on('connection', (socket) => {
    console.log(`connected, id: ${socket.id}`);

    socket.on('chat message', (user, message) => {
      data = `${message} from ${user}`;
      console.log(data);
      socket.broadcast.emit('chat message', data);
    });

    socket.on('disconnect', () => {
      console.log(`disconnected, id: ${socket.id}`);
    });
  });
}
```

sticky-sessionモジュールの内部でclusterが使われていて、`sticky.listen`というAPIを呼べば複数のプロセスを自動でForkしてくれる。
デフォルトではCPUの数だけForkするが、オプションで数を指定することもできる。

また、`sticky.listen`の戻り値はbooleanで、自身がMasterであれば`false`、Workerであれば`true`を返す。
これを利用し、Workerとしてこのプログラムが実行された場合はSocket.IOサーバとして機能するようにしている。

上記の実装により、XHR Pollingエラーの問題は解消される。
毎回同じプロセスに割り振られている様子は以下から確認できる。

```
$ curl localhost:3000
worker: 1

$ curl localhost:3000
worker: 1

...
```

なお、sticky-sessionは送信元IPを元に、サーバプロセスへの振り分け先を固定するようである。