RedisのPub/Subで異なるコンテナ間のWebSocketを同期する

RedisのPub/Subで異なるコンテナ間のWebSocketを同期する

先日参加したCyberAgentの短期インターンでredisのPub/Subについて学ぶ機会があったので実際に動かして遊んでみました。

redis

redisとは?

redisとはキャッシュやセッション管理でよく用いられるオンメモリ(=高速!)のNoSQLデータストアです。KVS(Key Value Store)と言い切っていいのかはちょっとよくわからないのでお茶を濁しておきますw

Pub/Sub

Pub/Subはredisの中でもマイナーな機能で知らない人も多いと思います。(実際に僕は今回初めて知りました)

簡単に説明すると、リアルタイムでデータの送受信を行うことができるデータストアです。Pub(=Publicsh)することで、Sub(=Subscribe)しているクライアントにデータを流すことができます。

Pub/SubとWebSocket

やっていることはWebSocketに近いです。違いとしては、WebSocket単体では同じサーバーに接続している人にしかデータを流すことができませんが、WebSocketとPub/Subを組み合わせることで、複数のサーバーにまたがってWebSocketのデータを共有することが可能になります。

WebSocketサーバーをWebSocketで接続しても同じことが実装できるでしょうが、これを専門にしているredisを使った方がパフォーマンスは上がると思います。また、コード的にもやっていることがシンプルになります。

なぜこのようなことを考えないといけないかというと、ロードバランシングしたときに誰がどこに接続するかを管理する必要がでてきたり、再接続の際に同じサーバーにいくように設計しないといけないからです。(個人開発だとそんなこと考えないですよね、、w)

WebSocketのnodeサーバーを実装する

redisを介したサーバーを作る前にまずはWebSocketを実装したnodeのサーバーを立ててみましょう。公式のサンプルを参考に、

var express = require('express')
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);

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', msg);
    });
});

app.use("/static", express.static(__dirname + '/static'));

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

こんな感じで実装できます。一部静的ファイルをホスティングしたりしています。よくある構成かと。

フロントの方は、

import React from "react";
import ReactDOM from "react-dom";
import io from "socket.io-client"

class App extends React.Component {

    constructor() {
        super()
        this.state = {
            msg: [],
            value: ""
        }
        this.socket = io()
    }

    componentDidMount() {
        this.socket.on("chat message", msg => this.setState({msg: [...this.state.msg, msg]}))
    }

    handleChange(e) {
        this.setState({value: e.target.value})
    }

    handleSubmit() {
        this.socket.emit("chat message", this.state.value)
        this.setState({value: ""})
    }

    msgList() {
        return this.state.msg.map((m, i) => <li key={i}>{m}</li>)
    }

    render() {
        return (
            <div>
                <input type="text" value={this.state.value} onChange={e => this.handleChange(e)} />
                <button onClick={() => this.handleSubmit()}>送信</button>
                <ul>{this.msgList()}</ul>
            </div>
        );
    }
}

ReactDOM.render(<App />, document.getElementById("root"));

こんな感じでreactの実装にしてみました。wsの接続と表示だけなのでコンポーネント分割などはせずに、雑にcomponentDidMountでwsのコネクションを張ってますw

いやぁ、socket.ioめっちゃ便利だなと。。

reactをよしなにbuildして、サーバーを起動するとwsで複数のタブ間で同期できている様子が確認できると思います。

複数のサーバー間でWebSocketを同期する

さて、本題のredisで複数サーバーを同期します。設計としては、docker-composeで2台のWebSocketサーバー(websocket1:3000, websocket2:3001)を立てます。さらに、redisも立ててそこに、http://redis:6379でアクセスできるようにlinkを張ります。
で、このredisを介してデータの同期ができるようになります。

dockerコンテナ間の通信とかに関してはこちらを参照してください。

docker/docker-composeにおけるコンテナ間通信を実装する

WebSocketのDockerfile

要件としては、nodeが動く・expressが動く・静的ファイル&サーバーAppが配置されている、の3つです。

from node

WORKDIR /var/www/app

RUN apt-get update
RUN npm install express socket.io socket.io-redis

COPY ./server .

CMD node server.js

また、server.jsを以下のように書き換えます。

var express = require('express')
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var redis = require('socket.io-redis');

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

io.adapter(redis({ host: 'redis', port: 6379 }));

io.on('connection', function (socket) {
    socket.on('chat message', function (msg) {
        io.emit('chat message', msg);
    });
});

app.use("/static", express.static(__dirname + '/static'));

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

それ用のライブラリが充実しているので爆速で実装することができてしまいます。

あとは、起動設定をdocker-compose.ymlに記述するだけです。

version: '2'
services:
  websocket1:
    build: .
    ports:
      - "3000:3000"
    links:
      - "redis:redis"
  websocket2:
    build: .
    ports:
      - "3001:3000"
    links:
      - "redis:redis"
  redis:
    image: redis

こんな感じ。
これを、

docker-compose up -d

で起動すると、、、

こんな感じで異なるhost間でもwsを同期することができました。

反省点

  • container内とホストOSでデータの同期をしていないから開発には向いてない(動かすことが目的やったし別にいいんだが)
  • サーバー内のredisのホストがハードコーディング(これも動かすことが目的だったから&上が解決できたら別に問題ない)
  • データの永続化(せっかくストレージ使ってたのにっていう)
  • フロントのデータ管理

勉強がてら上記のリファクタリングもありかなって思ってる次第でございます。。

コードはこちら
redis-pubsub(github)