Socket.ioサーバをスケールアウトする(Redisを使った複数プロセス間でのブロードキャスト)


前回のSocket.ioサーバをスケールアウトするでは、sticky-sessionモジュールを使用してSocket.ioサーバを複数プロセスで運用する方法について書いた。

sticky-sessionを使用することによってクライアントがサーバとコネクションを張れない問題は解決できたが、まだ課題は残っている。
それは、2つのクライアントが別々のSocket.ioサーバプロセスに接続した場合に、この2つのクライアント間でメッセージを送受信することができないという問題。
例えば、クライアントAがサーバXに接続し、クライアントBがサーバYに接続したとすると、現段階ではサーバXがクライアントAから受け取ったメッセージを、サーバYに知らせるすべがない。

この問題を解決するために、Socket.ioではsocket.io-redisという拡張Adapterを用意している。メッセージのルーティングを担うモジュールのことをSocket.ioではAdapterと呼んでいて、socket.io-redisはデフォルトのAdapterであるsocket.io-adapterを置き換える。

socket.io-redisを使用したサーバの実装は以下の通り。
socket.io-redisをインポートして、Adapterとしてセットするだけ。

index.js
const cluster = require('cluster');
const io = require('socket.io')();
const sticky = require('sticky-session');
const http = require('http');
const redis = require('socket.io-redis');  // 追加

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

io.adapter(redis({host: '127.0.0.1', port: 6379}));  // 追加
io.attach(server);
isWorker = sticky.listen(server, 3000);

if (isWorker) {
  io.on('connection', (socket) => {
    console.log(`worker: ${cluster.worker.id}, 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}`);
    });
  });
}

2つの別々のホストからこのサーバに接続する。
※クライアントのプログラムはこちら

# クライアントA
$ node client.js A
connected, id: S1H5pJcm8Kun5lOoAAAA
# クライアントB
$ node client.js B
connected, id: 7HLiEmHioqbLuINtAAAA
# サーバ
$ node index.js
worker: 2, connected, id: S1H5pJcm8Kun5lOoAAAA
worker: 4, connected, id: 7HLiEmHioqbLuINtAAAA

サーバのログから、クライアントAとBは別々のSocket.ioサーバプロセスに接続されたことがわかる。
前回までの実装では、クライアントAが送信したメッセージをサーバブロードキャストしても、クライアントBがメッセージを受取ることはできなかったが、Redisを介した実装ではクライアントBがメッセージを受取ることができる。

# クライアントA
$ node client.js A
connected, id: S1H5pJcm8Kun5lOoAAAA

Hello  # クライアントAが送信したメッセージ

Hi from B  # クライアントBが送信したメッセージ
# クライアントB
$ node client.js B
connected, id: 7HLiEmHioqbLuINtAAAA

Hello from A  # クライアントAが送信したメッセージ

Hi  # クライアントBが送信したメッセージ

裏側で何が起きているか

Redisをモニターしていると、裏側でどんなやり取りがあるのかがわかる。
socket.io-redisは、RedisのPUB/SUBを利用している。

まず、socket.io-redisを実装したサーバを起動すると、サーバはsocket.io#/#というチャンネルをSUBSCRIBEする(使用するネームスペースによって変わる)。
4行出力されているのは、Socket.ioサーバワーカーが4つ起動しているから。

$ redis-cli monitor
OK

"subscribe" "socket.io#/#" "socket.io-request#/#" "socket.io-response#/#"
"subscribe" "socket.io#/#" "socket.io-request#/#" "socket.io-response#/#"
"subscribe" "socket.io#/#" "socket.io-request#/#" "socket.io-response#/#"
"subscribe" "socket.io#/#" "socket.io-request#/#" "socket.io-response#/#"

次に、クライアントAがサーバXに接続してメッセージを送信すると、サーバはメッセージをRedisに対してPUBLISHする。

"publish" "socket.io#/#" "\x93\xa66n0BWC\x83\xa4type\x02\xa4data\x92\xacchat message\xacHello from A\xa3nsp\xa1/\x83\xa6except\x91\xb4S1H5pJcm8Kun5lOoAAAA\xa5rooms\xc4\xa5flags\x81\xa9broadcast\xc3"

socket.io#/#チャンネルに対してPUBLISHしているので、このチャンネルをSUBSCRIBEしているサーバYがメッセージの通知を受取り、それをクライアントBに送信することができるという仕組み。