WebSocketが一番速いアプリケーションサーバはどれだ?:Tomcat、Jetty、Socket.IO/Node.js性能比較(2/3 ページ)
双方向通信を実現するHTML5関連技術WebSocketを実装した3つのアプリケーションサーバの実装の違い・性能などを徹底検証する。
TomcatでのWebSocketアプリケーション実装上の3つの注意点
【1】処理スレッドの解放
7.0.34までの実装では、WebSocketハンドシェイク後にクライアント側から何かしらデータを送信しない限り処理スレッドが解放されないという問題がありました。
このため、Tomcat側のコネタク設定で、“最大接続数(maxConnections)”が“最大処理スレッド数(maxThreads)”より多い場合でも、クライアント側からデータを送信しなければ、maxThreads数以上の接続ができませんでした。
7.0.35では、この問題は解消しています。
【2】Jetty-WebSocketクライアントとの相性
7.0.29ではWebSocketハンドシェイク時に、「Transfer-Encoding: chunked」を返していました。このため、Jetty-WebSocketクライアントがChunkedメッセージと勘違いして誤動作を起こしていました。
7.0.33で、この問題は修正されています。
【3】ヒープメモリの大量使用
TomcatのWebSocketモジュールでは、送信時のテキストデータ/バイナリデータの内部保持用に、java.nio.CharBuffer/java.nio.ByteBufferを使用しています。デフォルトの状態では、1接続につき、それぞれ8Kbytes確保しています。
これらのバッファは、クライアントから接続している間ヒープメモリを圧迫し続けるため注意が必要です。不要であれば、バッファサイズを小さくすることも考えた方が良いでしょう。
バッファサイズは、MessageInbound#setOutboundByteBufferSize()/setOutboundCharBufferSize()を使って変更することが可能です。
なお、内部保持用のバッファは、以下の場合に使われます。
- writeTextMessage():CharBuffer、ByteBufferともに利用
- writeTextData():CharBuffer、ByteBufferともに利用
- writeBinaryData():ByteBufferを利用
ただし、writeBinaryMessage()を使用する場合は、この内部保持用バッファは使用しませんので、送信するデータがバイナリで良い場合(もしくは、クライアント側でバイナリからテキストに変換できる場合)は、このメソッドを使用すると効率が良くなります。
また、setOutboundByteBufferSize()/setOutboundCharBufferSize()のそれぞれに1を渡すことで、ヒープ利用量を目いっぱい下げることができます。
JettyでのWebSocketアプリケーション実装上の3つの注意点
【1】無通信タイムアウト
デフォルトでは接続後に何も送信しないと、およそ5分(30万ミリ秒)でサーバ側からコネクションを切断します。
WebSocketServletの初期化パラメータ、もしくは、プログラム中でWebSocket.Connection#setMaxIdleTime()を用いてタイムアウト値を変更することで対処します。「0」を設定すると、無限待ちとなります。
【2】バッファサイズ
Jetty 8ではデフォルト通信バッファサイズが8Kbytesです。このため、大量にデータを送信する場合は、バッファサイズを拡張した方が性能が出る場合があります。web.xmlで初期化パラメータbufferSizeを設定することで、チューニングが可能です。
【3】Jetty-WebSocketクライアント
Jettyが提供しているWebSocketクライアントは、作成したWebSocketServletのテストを行う場合に便利な機能です。
ただし、1つのコネクションに対してスレッドを大量に確保しようとするため、今回の検証のように1プロセスで大量のコネクション用スレッドを起動すると、プロセス数の上限にすぐに達してしまうため、注意が必要です。このような使い方をする方が間違っているのかもしれませんが……。
Node.js/Socket.IOでのWebSocketアプリケーション実装上の注意点
Socket.IOはバイナリデータの送信に対応していません。Socket.IOを使ってバイナリデータをやりとりする場合は、データをBase64でエンコードする必要があります。
3つのテストパターンで試す
今回は、3パターンのテストパターンを考えてみました。「Apache Bench(ab)」「JMeter」などで単純に負荷を掛けて、というテストではなく、多重接続した状態での一斉受信、一斉配信した場合の性能差や、サーバから送信するデータ量によって性能差が見えるかを確認しています。
テストケース【1】同時受信パターン
10000クライアント接続完了後にサーバからメッセージを送信して、クライアント側での受信時間を計測します。
- クライアントアプリケーション
1万クライアント分のスレッドを作成して、各スレッドがWebSocketでサーバに接続します。全てのクライアントが接続を完了した後、最後に接続したクライアントからメッセージを送信します。
- サーバアプリケーション(Tomcat/Jetty用)
クライアントからの接続を受け付けると、コネクションをArrayListに格納します。クライアントからメッセージを受け取ると、ArrayListに格納されたコネクションを取り出して、メッセージを送信します。
Tomcat/Jettyともに、ブロードキャストする仕組みを持っていませんので、上記のような作りになっています。
・サーバアプリケーション(Node.js用)
クライアントからの接続を受け付けても、特に処理は行いません。クライアントからメッセージを受け取ると、送信したクライアントおよび接続中のクライアントに対してメッセージをブロードキャストします。
テストケース【2】同時送信パターン
1万クライアント接続完了後にクライアントからメッセージを送信して、サーバ側での受信時間を計測します。
- クライアントアプリケーション
1万クライアント分のスレッドを作成して、各スレッドがWebSocketでサーバに接続します。全てのクライアントが接続を完了した後、すべてのクライアントスレッドがサーバに対して一斉にメッセージを送信します。
全クライアントがメッセージの送信を完了した後、サーバに対してログ出力依頼のメッセージを送信します。
- サーバアプリケーション
クライアントからメッセージを受け取ると、Tomcat/Jetty用サーブレットではArrayListに、Node.js用ではArrayにメッセージを溜め込みます。
クライアントからログ出力依頼のメッセージを受け取ると、溜め込んだメッセージをログファイルに出力します。
テストケース【3】大量データ受信パターン
1つのクライアントに対して、サーバから大量データを送信して、クライアント側で全データ受信にかかった時間を計測します。データサイズは、1〜10Mbytes(1Mbytes刻み)の10パターンで行います。
- クライアントアプリケーション
1クライアントのみWebSocket接続します。接続後に、要求するデータサイズを送信します。
- サーバアプリケーション
クライアントから要求データサイズの入ったメッセージを受け取ると、Tomcat/Jetty用はバイナリデータを、Node.js用はテキストメッセージをクライアントに対して送信します。
次ページでは、テスト結果を見ていきます。
Copyright © ITmedia, Inc. All Rights Reserved.