前回SalesforceハッカソンにWebSocketのクイズアプリを出してきたよ~という話をしたわけだが今のところ身内以外からはほとんどアクセスされてないっぽい。
まぁほとんど宣伝してないからそれは別に構わないんだけど、審査されている形跡もないのは大丈夫なのか。。。(^^;
さて、それはさておきWebSocketアプリを作ったら是非試してみたいと思っていたことのひとつにC10K問題の検証と言うのがあります。
C10Kとはクライアント1万台問題の略で平たく言うと「WebSocketってクライアントとずっとソケット繋ぎっぱにするわけだよね。そんなのクライアントの数がちょっと増えたらあっという間に破綻するんじゃね?」という問題のことです。
★検証シナリオ
今回作ったアプリにはルーム毎にチャットの機能があるのでそれを利用することにします。
具体的な目標数値としてはとりあえず以下のように設定しました。
- ルームを100個用意する
- 各部屋に3秒に1人ずつ、100人(100クライアント)入室する(最大時同時接続1万クライアント)
- 各クライアントは5秒に1回チャットメッセージを投げる(合計100万メッセージ)
とりあえず1万クライアント。全員を同じルームにいれちゃうとチャットのブロードキャストがえらいことになってしまうので、ルームだけは分けることにします。
3秒とか5秒とか段階的に負荷を増やしているのはWebSocket接続はだいたい1接続が1人の人間に対応するので、人間の操作としてそこまでの連続リクエストはこないだろうと思われるからです。(先にDevSumiで行ったデモは特殊な例です。(^^;)
この数字をもう少し細かく見ていくと以下のような負荷になります。
- 最大時秒間2000メッセージ(1万人が5秒に1回メッセージ送信するから)
- その時のブロードキャストメッセージが20万件(1メッセージはルーム内の100人にブロードキャストされるから)
- 1クライアントが1秒間に受信するメッセージは20件(ルーム内の100人が5秒に1回メッセージ送信するから)
。。。やりすぎ?。。。
しかし、普段負荷テストをする時はJMeterでぶんぶんリクエスト投げてるわけだから、3秒に1人追加とか5秒に1回のメッセージとかもの凄く手加減してる気分なんだが。。。。(--
それに1クライアントあたり秒間20件のダウンロードがあると言っても、チャットのメッセージなんか1000個あってもそこらのHTMLページのサイズよりも小さいだろうから、通信のコスト自体はどう考えてもhttpアプリのJMeterテストよりも低いはず。
そもそもWebSocketアプリの負荷テストのセオリーがわからないし、テストツールも自作するんだからとりあえずやっとけやっとけ。
Herokuはこの程度の負荷ではおかしくならない!(多分)
★とりあえず1dynoでテスト
いきなり高負荷のテストをしようにもどの位のDynoを用意すれば良いのかまったく見当がつかないのでまずは1dynoの臨界点を見極めることにします。
手始めに
- 1ルームに100人入室、100メッセージ送信
というテストをしてみたところ、何の問題もなく正常終了しました。
まぁNon-Blocking I/Oを使っているんだからこれくらいはやってもらわないと、という感じです。(^^;
ちなみにこの時点でテスト環境で使っているRedisはRedisCloudの無償版です。無料枠でもこの程度のリクエストは捌けるってことですね。
ここから人数とメッセージ数はそのままでルームを増やしていきます。
- 2ルームに100人入室、100メッセージ送信
まだ何のエラーも起こりません。
メモリ使用量も200MB未満なのでかなり優秀です。
Play(というよりもNettyとAkkaのActor)はかなり筋が良いんだろうと感じました。
- 3ルームに100人入室、100メッセージ送信
接続できないクライアントがでてきました。
が。。。サーバー側にエラーログが出ていないところを見るとどうやら限界を迎えたのはクライアント側っぽい。
テストクライアントはjava_websocketを使ったPlayアプリケーションとして作っているんですが、このライブラリが内部的に使っているのは通常のSocketクラスなので、この辺がNon-Blocking I/OとNormal I/Oの差でしょうか。またjava_websocketはSocketの監視のために接続毎にスレッドを作っているのでその辺もメモリ使っちゃって臨界が早い原因になっていると思います。
ここから先はテストクライアントもHeroku上で複数dynoを立ててのテストとなります。テストクライアントをHerokuにあげて実行したらこれもクリアしました。
本当に優秀。。。ていうか予想ではそろそろエラーになるはずなのになんで動いてるの?って感じ。。。(^^;;;
- 4ルームに100人入室、100メッセージ送信
ここにきてようやく2種類のエラーが発生しました。
まずそろそろ来ると思っていたRedisの最大接続数到達エラーがでました。
無料枠のRedisの接続数はたったの10個しかありません。RedisClientはコネクションプールから取るようにしていますが同時接続数が増えると当然並列で使われる件数が増えます。また、それ以上に影響が大きいと思われるのはルームの数だけRedisのSubscriberが必要になる点です。4つのルームがアクティブなら4つのコネクションがそれぞれのルームに占有されます。つまり同時接続数は増えているにも関わらず使えるコネクション数は減っているわけでむしろよくここまで持ったと思います。
本番環境ではRedisGreenを使ってますが、この先のテストでは接続数無制限のRedisCloud(2.5GB)を使うことにします。
またHerokuのRouterがH11を返すようになりました。このエラーの意味はDevCenterのドキュメント、Request queueingのあたりに書いてあります。
HerokuのRouterは1dynoに対して50同時接続しか行わずそれ以降はキューに入ります。そのキューがあふれた場合にこのエラーが発生するわけです。
しかし実際には400近くの接続が1dynoで行われています。これが何でかというとルーター自体が複数台あるからです。結果からの推測では1アプリで少なくとも8台ルーター使ってる計算になります。
なので、H11が発生した場合でもリトライすると別のルーターに繋がってあっさりと接続できたりします。(はっきりと裏を取ったわけではありませんが高負荷時にブラウザからアクセスした際にそれらしき現象を確認しました。)
この動作はWebSocketに限らずhttpアクセスの場合も同じはずです。Httpクライアントからのアクセスがエラーとなった場合に直後にリトライするのって意味あるんだろうか?と思ってましたが、少なくともHerokuでは意味があると言えます。(まぁH11が発生しているなら素直にdynoを増やせっていう話ではありますけど。)
★1dynoの限界
ここまでの結果からH11が発生する400前後が限界となります。
ただしサーバー側のメモリ使用量を見ると300MB前後なので、ルーターさえリクエストを回してくれればもっと多くのリクエストを1dynoで捌けるだろうとは思います。
ちなみに2X dynoの場合でもこのルーターの制限値は変わらないようで、これはもうちょっとなんとかならんのか?あるいはWebSocket有効のルーターでは制限値を引き上げられないか?ということをサポートに問い合わせ中です。
どうやら現在ちょうどルーター周りの改修を行っているらしく、うまくいけば近々対応されるかもしれません。(みたいな返事が来ているが実際のところはどうなのか謎。。。)
この結果を受けて1万クライアントを捌くには25dynoくらいだろうと辺りを付けて実際試してもいるんですが続きはまた今度。
これ書きながら追試したいことやサーバーサイドを直したいところも出てきているので。(Herokuに問い合わせ中のモノもあるし)
ちなみに現在のところ、完全にノーエラーで接続できていると確認できたクライアント数は5600台位です。
傾向としては1度接続が確立したクライアントの動作は安定しておりパフォーマンスもほとんど悪くならないようです。(何故ならソケット繋ぎっぱだから。)
負荷がかかってくると接続時にエラーになることが多いようで、そこをなんとかすれば7,8000まではすぐに行くんじゃないかという気もしてます。(1回のテストで2GB以上のログが出るんで雑な検証しかしてませんが。。。(^^;;;)
後篇がいつになるか(あるいは本当に書かれるのか?)は未定です。(^^;;;