Block Rockin’ Codes

back with another one of those block rockin' codes

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 をきちんと通せるものがなかなか少ない現状だと思うんですが、なにか良い方法を知っていたら教えて頂きたいです。


また、この辺自分も理解が足らないところがあるので、何か気づいた点があったら教えて頂けると幸いです。