WebSocket アプリの負荷分散
最近 SPDY と WebSocket がアツいですね。 再来週の SPDY & WS 勉強会 も、定員100名に対して 参加者が 247 名とかなりアツいことになっています。
その予習というわけでもないですが、最近 WebSocket を実サービスへの 導入方法を考えながら遊んでいたので、 WebSocket の負荷分散方法について 考えていることを書いておこうと思います。
ステートフルな WebSocket アプリケーション
HTTP サービスは基本的にステートレスな実装になっており、リクエストが来るたびに DBサーバーや memcached などのバックエンドから情報を取得して返していました。
この構成では Web アプリ自体は完全にステートレス化することができているので、 負荷分散機はラウンドロビン等のアプリケーションを無視した負荷分散をすることができました。
しかし、 WebSocket が登場してリアルタイム Web が普及し、それがリッチ化してくると、 ステートレス型では効率が悪くなる事があります。
たとえば複数のチャットルームを持つチャットアプリを作る場合、チャットの内容を Redis に格納し、 Redis の pubsub 機能を使って他の Web サーバーに更新を通知することができますが、 Redis は subscribe している Web サーバー全てに通知を送り、チャットログを取得するリクエストを さばかなければなりません.
MMO ゲームのようにアプリサーバーのメモリ上にチャットログを格納し、同じチャットルームにいる ユーザーが全員同じアプリサーバーに接続するようにすればどうでしょうか? アプリサーバーはバックエンドから通知を受け取ったりメッセージを取得しなくても全員に メッセージを配信できます。これならバックエンドとの通信はログを保存する1回で済みます。
正攻法でこの構成を組む場合、アプリをステートレスなフロントWebアプリとステートフルな バックエンドアプリの2段構成にすることができます。 でも、この程度ならリバースプロキシで直接リクエストの振り分けをやってしまったほうが 楽だし低コストですよね。
redis を使った動的な負荷分散
そこで、 redis に URL のパスごとに分散先を格納しておき、それを使って動的に リバースプロキシする構成を考えてみました。
効率や応答速度のことを考えたら、リバースプロキシは WebSocket のデコード&エンコードをする よりも、HTTP サーバーが Upgrade ヘッダを見つけると TCP プロキシとして動作するほうがいいはずです。
Go
最初に目をつけたのは Go です。nginx よりはコードを読み書きしやすいからです。 (node.js でも良いのですが個人的には JavaScript よりも Go の方が好きです)
Go の標準ライブラリ "net/http" にはそのものズバリな revserseproxy があります。 ただしこれは WebSocket に対応していないので、ちょっと改造して Upgrade ヘッダを見つけると TCP プロキシになる機能を追加しました。 (methane/rproxy)
あとは redigo という Redis クライアント(名前がカッコイイ!) と組み合わせればお手軽に賢いリバースプロキシの完成です。
nginx + lua
そういえば nginx も最近 WebSocket に対応したって話題になってたなーと思って調べてみると、 これも Upgrade ヘッダを見て TCP プロキシとしてふるまうようです。 さらに調べてみたら Redis + Lua を使って動的な負荷分散するサンプルもいくつかみつかりました。
ただし、 nginx のサブリクエストは基本的に HTTP 用であまり memcached や redis に格納した 生の文字列を扱うのに向いていないので、 lua-resty-redis という pure Lua のライブラリを使って書きなおしてみました。
これもちゃんと nginx の非同期機能を使ってくれるので Redis に問い合わせしている間 ブロックしたりはしませんし、コネクションプールも可能です。
せっかく作ったので Go 版を紹介したものの, nginx が使えるならもう nginx でいいですね。 でも、毎回 Redis に問い合わせるのではなく expire 付きの LRU キャッシュをインメモリでやるとか、 Socket.IO のセッションを解析して自分で Redis に格納するとか複雑なことをするなら Go の強みが生きてくるかもしれません。
サンプル
お待たせしました。サンプルはこちらになります。
KLab/websocket-reverseproxy-demo
このサンプルでは、 Tornado を使ったオンメモリで動くチャットアプリを2つのポートで立ち上げ、 Redis にチャットルームごとのバックエンドを格納した上で、 nginx と go によるリバースプロクシの動作を確認しています。
Go も nginx も開発版を使っているので少しハードルが高いかもしれませんが、 SPDY & WS 勉強会の 参加者はぜひ予習として試してみてください。
トラックバックURL
この記事へのコメント
このマスクはフレーム毎にランダムな4バイトでフレーム内のデータを XOR します。
なので、一応プレーンテキストを避ける事はできるのですが、とても簡単な難読化でしかないので、セキュアではありません。
特殊な proxy に邪魔されないためにも、 WebSocket を使う場合は TLS を利用することが推奨されます。