Node.js + Socket.IO + pm2でデーモン化とクラスタリング

Socket.IOをpm2でクラスタリングするには、ちょっと工夫が必要だったのでメモ

forever

Node.jsのデーモン化といえばforeverです。しかしクラスタリングしようとすると、Node.jsのコードをクラスタリング対応で書かないといけないのでやや面倒だったりします。今回、foreverよりも高機能なpm2を使ってクラスタリングを試してみました。

pm2

pm2はforeverと同じようにNode.jsをデーモン化するツールですが、モニタ機能やクラスタリング機能などかなり高機能になっています。

pm2のインストール

pm2はグローバルでインストールします。

$ npm install -g pm2

対応するNode.jsのバージョンは古いと動かないかもしれません。今回はv0.10.20(on Mac)で動作確認しました。動かすNode.jsアプリはこちらのSocket.IOのサンプルアプリです。このコードだとSocket.IOのバージョンが古いようなので、一旦npm uninstall socket.ioして、再度npm install socket.ioで再インストールしました。

pm2を使ってクラスタリング

$ pm2 start app.js -i max

として起動するとプロセスがCPUコア数分だけ起動し、クラスタリングされます。簡単ですね。起動リストを表示させるには

$ pm2 list

f:id:tomo_watanabe:20140131120059p:plain

プロセスが4つ起動されているのがわかります。この時にブラウザでアクセスして確認します。

f:id:tomo_watanabe:20140131120326p:plain

片方がxhr-pollingになってたりしますね。これリロードするとwebsocketになったり、xhr-pollingになったりなんとも不安定になります。

ログを表示するには

$ pm2 logs

f:id:tomo_watanabe:20140131120712p:plain

ところどころに警告がでています。

warn  - client not handshaken client should reconnect

調べてみるとSocket.IOの接続に起因するものでSocket.IO or WebSocket を AmazonELB でバランスする検証にありました。

つまり、 Socket.IO は接続確立までに 2 回リクエストを投げるということです。 そしてこの最初の要求と、 WS の接続要求は、「同じインスタンス」にアクセスする必要があります。 二つの接続がバランスされて別々のサーバに行くと、ハンドシェイクがエラーになり、リトライが発生します。 たまたま二回同じサーバにいけば、接続が確立できるので、バランスするサーバが増えるほど成功確率が減 り、確立までの時間が長くなります。

redisの導入

解決策としてredisをセッションキーストアとして利用する、ということのようです。今更だけどSocket.ioについてまとめてみる

redisのPub/Subを利用して、各プロセス間でセッション共有をするということです。

このサイトの記事のとおりなのですが、ひと通りメモ

redisのインストールと起動

$ brew install redis
$ redis-server /usr/local/etc/redis.conf &
[1] 3234
$ [3234] 31 Jan 11:29:03.112 * Max number of open files set to 10032
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 2.8.1 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in stand alone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 3234
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

[3234] 31 Jan 11:29:03.113 # Server started, Redis version 2.8.1
[3234] 31 Jan 11:29:03.113 * The server is now ready to accept connections on port 6379

redisのpub/sub機能

redisにはpublisher/subscriberという機能があって、いずれかのプロセスが受けた通知をredisに設定することで、他のプロセスに通知を行い共有できるということです。この機能を使うことでクラスタリングされたプロセス間でセッションの共有を行えます。

サーバアプリの書き換え

app.jsを以下のように書き換えます

var app = express()
    , server = http.createServer(app)
    , redis = require('socket.io/lib/stores/redis')  // socket.ioにredisがある
    , redisConf = { host: '127.0.0.1', port: 6379 }
    , io = require('socket.io').listen(server);

// storeタイプをredisに変更
io.set('store', new redis({
    redisPub: redisConf,
    redisSub: redisConf,
    redisClient: redisConf
}));

pm2でクラスタリング

再びpm2でクラスタリングを試してみます。

$ pm2 start app.js -i max

この時のアクセスログを見ると警告が無くなり、かつ正常にクラスタリングされているようです\(^o^)/

f:id:tomo_watanabe:20140131120626p:plain