Socket.IO or WebSocket を AmazonELB でバランスする検証
追記
- 12/2/29
- 検証コードと環境は後にしてとりあえず結果だけ書く
- 12/3/5
- Socket.IO の RedisStore を使えばスケール可能なことがわかったので追加
- 12/3/11
- 検証コード追加
caution
この検証は 東京Node学園 4時限目 - connpass でやった結果です。しかしその時の環境やソースが手元に無いので今再現ソースと環境を作っています。
2/28 現在分かってる結論だけ先に出しておきます。ソースは後で追って掲載します。その時点でもし結論が変わったりした場合は追記します。
また、この検証内容については一切責任は取りませんので、プロダクション等で使う場合はきちんと検証して下さい。
特に ELB の仕様が変わったら結果が変わると思います。結果が変わったことに気がついた方は教えて頂けると助かります。
code
検証コードを公開しました。 https://github.com/Jxck/elb-websocket-test
- sio.js は普通の socket.io サーバです。本文に書いたように、これだと ELB でバランスすると、 reconnect が多発します。
- sio-redis.js が session を redis に登録する例です。これなら、ELB で最初の接続と次の接続が別のサーバに行っても動きます。
- balancer.js は ELB を使わずに自分でバランスする場合のサンプルです。同じく sio-redis.js ならうまくいきます。
- ws.js は生の websocket の通信のつもりで書いたんですが、なぜか繋がらず。。断念。
ちなみに、 socket.io のサーバは /socket.io.html をデフォルトでレスポンスするので、
ELB のハートビートはそこに送ると便利です。
intro
複数の AWS サーバのインスタンス上に Socket.IO を立てて、ELB でバランスしたい時、
現状の ELB の仕様では、「できない」ということがわかりました。
これは Socket.IO を使わない生の WebSocket であれば設定によってはできます。
詳細を書いておきます。
ELB
簡単にまとめると、 ELB は TCP と HTTP の2つのモード(とここでは呼びます)があります。
また、 HTTP を見てバランスする場合は、 Sticky Session というオプションがあり、
Cookie を ELB が発行するか、自分の作ったアプリが発行するか選べます。
- TCP モード
- HTTP モード
- ELB セッション
- APP セッション
WebSocket サーバをバランスしたい
WebSocket の接続は、 HTTP の upgrade が通るかどうかが大きな境目です。
ELB の場合は、 TCP モードの場合は HTTP ヘッダは見ていないので、通るようです。
ただし、 HTTP モードの場合は、なぜか ELB が HTTP ヘッダを書き換えてしまっていました。
- upgrade ヘッダが削除されてる
- connection=websocket が connection=keepalive に書き換えられてる
ということで HTTP モードではバランス以前に WebSocket 接続が確立できません。
かならず TCP モードにして下さい。
Socket.IO をバランスしたい
生の WS では機能が足らないなどの場合に、Socket.IO を用いることも多いと思います。
しかし Socket.IO は WS と別に、最初に HTTP リクエストで Socket.IO サーバからいくつかのデータを受け取り、
その後再び upgrade リクエストを発行し、 WS のコネクションを確立します。
つまり、 Socket.IO は接続確立までに 2 回リクエストを投げるということです。
そしてこの最初の要求と、 WS の接続要求は、「同じインスタンス」にアクセスする必要があります。
二つの接続がバランスされて別々のサーバに行くと、ハンドシェイクがエラーになり、リトライが発生します。
たまたま二回同じサーバにいけば、接続が確立できるので、バランスするサーバが増えるほど成功確率が減り、確立までの時間が長くなります。
確実に同じクライアントをサーバに振るためには、 Sticky Session が利用できます。これは ELB が Cookie を見て同じサーバに降ってくれるものです。
Cookie は HTTP ヘッダだし HTTP モードでしか使えないのでこの時点で TCP モードでは無理ということになります。
HTTP モードで Sticky Session を有効にする際二つのモードがあります。
で、いずれでも Socket.IO のハンドシェイクは一発で成功するのですが、先ほどのように WebSocket はヘッダが書き換えられるので、フォールバックして XHR になります。
しかも ELB の仕様で HTTP モードは 60 秒に一回接続が切れるらしいです。Socket.IO はリコネクトできるんですがステートフルセッションにとってこれは致命的ですね。
[追記]
Socket.IO の SessionStore を Redis にすると、ハンドシェイクも共有されて接続可能なことがわかりました。
この RedisStore という機能は Socket.IOv0.8.4 のソースを読んでわかった次期新機能先取り - Block Rockin’ Codes でも紹介しましたが、
現在実装が見た感じほぼ終わってますが、まだ Stable 扱いではないアンドキュメントな機能です。
簡単に言うと、Redis サーバを立て Socket.IO の SessionStorage としてそこを指定します。
すると、上述した最初のアクセスと二回目のアクセス先のサーバが違ったとしても、ハンドシェイクが成功します。
(ソースはおって出しますすいません。。)
結果
生 WebSocket | Socket.IO | |
---|---|---|
TCP mode | 接続が確立するし、バランスする | 2回のアクセスが偶然同じインスタンスに行けば接続が確立する。 台数が増えればそれだけ、リコネクトの回数が増えて、接続確立までの時間が増える。 |
HTTP mode | ヘッダが書き換えられて、接続が確立しない。 | Sticky Session を有効にすれば一発で接続が確立する。 しかし、WebSocketは無理でフォールバックしてXHRになります。 また 60 秒に一回切断する。 |
考察
生の WS で足りるケースならいいかもしれないけど、 Socket.IO のバランスは現実的ではないようです。
ELB は設定項目が少ないし、プログラマブルな部分が無いので、 HTTP モードでヘッダを書き換える挙動が治らないと、 ELB を使ったバランスは無理そうですね。
どうしても Socket.IO の前にバランサが必要なら、
- WebSocket を通す
- Sticky Session
を実現できるバランサを自分で立てる必要があります。
今のところ Node-HTTP-Proxy など、Node でできたものがいくつか有ります。
そもそも WebSocket をきちんと通せるものがなかなか少ない現状だと思うんですが、なにか良い方法を知っていたら教えて頂きたいです。
また、この辺自分も理解が足らないところがあるので、何か気づいた点があったら教えて頂けると幸いです。