Pub/Subを使ってWebSocketサーバをスケールアウトさせる(オンプレ版)

スケーラブルなWebサービスのよくある設計はWebサーバの前段にロードバランサーを設置し後段にWebサーバを複数台配置するというものである。通常のWebサービスはステートレスなHTTP(もしくはHTTPS)通信のためこの方式で問題なく稼働する。
しかし、ステートフルなWebSocket通信においては以下の課題が存在する。

  1. ロードバランサーがWebSocket通信を判別しセッションを保持しているバックエンドサーバに転送できなければならない。
  2. 自分以外のWebSocketサーバが管理しているクライアントにはWebSocket通信を行うことができない。

そこで本稿では以下の方法でこの2つの課題の解決を試みる。

  1. WebSocket対応のロードバランサーを使う。
  2. Pub/Subを使う。全バックエンドサーバはsubscribeしておき、情報を送信したいサーバはpublishする。

この方法をオンプレミスで実現する方法を作ってみたので備忘録で残しておく。ソースコードはこちら
別記事でクラウド(AWS)で実装したときの記事も書く予定。

アプリの概要

今回は簡単なチャットアプリを作る。アプリの動作イメージは以下の通り。

  1. クライアントはWebブラウザを使ってアプリページを表示する。
  2. WebSocketでアプリと接続する。
  3. チャット入力欄に文章を入力し「Submit」ボタンを押すと、WebSocketを使ってアプリにメッセージを送信する。
  4. アプリはチャット画面を開いている(WebSocket接続を確立している)クライアントに向けて一斉にWebSocketでメッセージを送信。
  5. メッセージを受信したクライアントはメッセージ表示エリアにメッセージを表示する。
チャットアプリの動作イメージ

システムアーキテクチャ

ロードバランサーにはHAProxyを利用する。特別な設定をしなくてもWebSocketのセッションを認識してルーティングしてくれるみたい。(ちゃんと設定しようとするとこちらの記事にあるようにタイムアウトを個別に設定する必要がありそう。本稿ではデフォルト設定) また、SSLオフロードによりロードバランサーへのアクセスにはHTTPS/WebSocket over TLSを使用し、後段のサーバとはHTTP/WebSocketで通信する。
Pub/SubにはRedisを利用する。
アプリサーバにはWebSocketにも対応したTornadoを採用した。Webサーバプロセスはクライアントからメッセージを受信するとPub/Subにpublishする。また、Pub/Subをsubscribeするプロセスも動かしておいてsubscribeの結果をHTTP経由で自サーバのWebサーバプロセスに伝える。

システムアーキテクチャ図

インフラ設計

同一サブネットにロードバランサー、アプリケーションサーバ2台、Pub/Subサーバの合計4台のサーバを配置する。今回の検証ではサーバはVirtualBoxのVMを利用し、vagrantで管理している。ゲストOSはCentOS。

インフラ設計

実装

ロードバランサー (load-balancer)

HAProxyをインストール

yum -y install haproxy

/etc/haproxy/haproxy.cfg を編集

frontend ssl_proxy
    mode http
    bind *:443 ssl crt /etc/haproxy/server.pem
    server  app1 192.168.33.201:80 check
    server  app2 192.168.33.202:80 check

サーバ証明書 /etc/haproxy/server.pem を作成

普通は秘密鍵と証明書は別ファイルにすることが多いが、HAProxyでは一つのファイルにまとめる必要がある。
以下の例では秘密鍵(server.key)と自己証明書(server.crt)を作り、この2つのファイルをくっつけたserver.pemを作っている。

openssl genrsa 2048 > server.key
openssl req -new -key server.key > server.csr
openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt
cat server.key > /etc/haproxy/server.pem
cat server.crt >> /etc/haproxy/server.pem

サーバプロセス起動

systemctl start haproxy
systemctl enable haproxy

Pub/Subサーバ (pub-sub)

Redisをインストール

yum -y install epel-release
yum -y install redis

/etc/redis.conf を編集

bind 192.168.33.203

サーバプロセス起動

systemctl start redis
systemctl enable redis

アプリケーションサーバ (app1, app2)

まずはソースコードをダウンロード

git pull https://github.com/tetsis/simple-chat.git
cd simple-chat

Pythonを直接実行する場合

諸々インストール

yum install -y https://centos7.iuscommunity.org/ius-release.rpm
yum install python36u python36u-libs python36u-devel python36u-pip
pip3.6 install -r requirements.txt

サーバプロセス起動

python3.6 src/app.py | python3.6 src/subscribe.py

コンテナで動かす場合

諸々インストール

yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum -y install docker-ce docker-ce-cli containerd.io
systemctl start docker
systemctl enable docker
curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

サーバプロセス起動

docker-compose build --no-cache
docker-compose up -d

動作確認

2つのブラウザでhttps://192.168.33.200にアクセスするとチャット画面が表示される。(192.168.33.200はロードバランサーのIPアドレス)

片方のブラウザからメッセージを送信すると、両方のボックスにメッセージが表示される。(下図では左のブラウザで「Hello」と入力して「送信」ボタンを押すと、もう一つのブラウザにも「Hello」と表示される)