この記事では、Spring Boot アプリケーションでのリクエスト処理のさまざまなアプローチ (ThreadPool、WebFlux、コルーチン、仮想スレッド (Project Loom)) を比較します。
この記事では、Spring Boot アプリケーションで使用できるさまざまなリクエスト処理メソッドのパフォーマンスを簡単に説明し、大まかに比較します。
効率的なリクエスト処理は、高パフォーマンスのバックエンド アプリケーションの開発において重要な役割を果たします。従来、ほとんどの開発者は Spring Boot アプリケーションに埋め込まれた Tomcat を使用しており、そのデフォルトのスレッド プールはバックグラウンドでリクエストを処理するために使用されます。しかし、近年、代替方法の人気が高まっています。WebFlux は、リアクティブなリクエスト処理にイベント ループを活用しており、Kotlin コルーチンとその一時停止機能が人気を集めています。
さらに、仮想スレッドを導入する Project Loom も Java 21 でリリースされる予定です。ただし、Java 21 はまだリリースされていませんが、Java 19 から Project Loom を試すことができました。したがって、この記事では、リクエストを処理するための仮想スレッドの使用についても説明します。
さらに、JMeter を使用した高負荷テストのさまざまなリクエスト処理方法のパフォーマンス比較を実行します。
性能試験モデル
次のモデルを使用してリクエスト処理方法を比較します。
プロセスはケーキと同じくらい簡単です。
- クライアント (JMeter) は 500 個のリクエストを並行して送信します。各スレッドは、別のリクエストを繰り返し送信する前に、応答を待ちます。リクエストのタイムアウトは 10 秒です。テスト セッションは 1 分間続きます。
- 私たちがテストしている Spring Boot アプリケーションは、クライアントからリクエストを受け取り、遅いサーバーからの応答を待ちます。
- サーバーの応答が遅いと、ランダムなタイムアウトが発生します。最大応答時間は 4 秒です。平均応答時間は 2 秒です。
処理されるリクエストが多いほど、パフォーマンスの結果は向上します。
1. スレッドプール
デフォルトでは、Tomcat はスレッド プール (接続プールとも呼ばれます) を使用してリクエストを処理します。
概念は単純です。Tomcat にリクエストが届くと、リクエストを処理するためにスレッド プールからスレッドが割り当てられます。この割り当てられたスレッドは、応答が生成されてクライアントに送り返されるまで、ブロックされたままになります。
デフォルトでは、スレッド プールには最大 200 個のスレッドが含まれます。これは基本的に、一度に処理できるリクエストは 200 件のみであることを意味します。
ただし、このパラメータとその他のパラメータは構成可能です。
デフォルトのスレッドプール
Tomcat とデフォルトのスレッド プールが組み込まれた単純な Spring Boot アプリケーションのパフォーマンスを測定してみましょう。
スレッド プールには 200 個のスレッドが保持されます。リクエストごとに、サーバーは別のサーバーに対してブロッキング呼び出しを行い、平均応答時間は 2 秒です。したがって、1 秒あたり 100 リクエストのスループットが期待できます。
リクエストの総数
|
スループット、リクエスト/秒
|
応答時間、ミリ秒
|
エラー率、%
|
|||
平均
|
一番小さい
|
90%ライン
|
最大
|
|||
3356
|
91.2
|
4787
|
155
|
6645
|
7304
|
0.00
|
実際の結果は非常に近く、測定されたスループットは 1 秒あたり 91.2 リクエストであることに注目してください。
スレッドプールを増やす
アプリケーション プロパティを使用して、スレッド プール内のスレッドの最大数を 400 に増やしてみましょう。
server:
tomcat:
threads:
max: 400
もう一度テストを実行してみましょう。
リクエストの総数 | スループット、リクエスト/秒 | 応答時間、ミリ秒 | エラー率、% | |||
平均 | 一番小さい | 90%ライン | 最大 | |||
6078 | 176.7 | 2549 | 10 | 4184 | 4855 | 0.00 |
スレッド プール内のスレッド数が 2 倍になると、スループットが 3 倍になることが期待されます。
ただし、システム容量やリソースの制約に関係なくスレッド プール内のスレッドの数を増やすと、パフォーマンス、安定性、およびシステム全体の動作に悪影響を及ぼす可能性があることに注意してください。システムの特定の要件と機能に従って、スレッド プール サイズを慎重に調整し、最適化することが重要です。
2.ウェブフラックス
WebFlux は、各リクエストに専用のスレッドを割り当てる代わりに、少数のスレッドを備えたイベント ループ モデル (多くの場合、イベント ループ グループと呼ばれます) を採用します。これにより、限られた数のスレッドで多数の同時リクエストを処理できるようになります。リクエストは非同期で処理され、イベント ループはノンブロッキング I/O 操作を使用して複数のリクエストを同時に効率的に処理できます。WebFlux は、多数の長時間接続やストリーミング データの処理など、高いスケーラビリティが必要なシナリオに非常に適しています。
理想的には、WebFlux アプリケーションは完全にリアクティブな方法で作成する必要がありますが、これがそれほど簡単ではない場合もあります。ただし、WebClient を使用して低速サーバーを呼び出すだけの単純なアプリケーションがあります。
@Bean
public WebClient slowServerClient() {
return WebClient.builder()
.baseUrl("http://127.0.0.1:8000")
.build();
}
Spring WebFlux のコンテキストでは、RouterFunction はマッピングと処理をリクエストするための別のアプローチです。
@Bean
public RouterFunction<ServerResponse> routes(WebClient slowServerClient) {
return route(GET("/"), (ServerRequest req) -> ok()
.body(slowServerClient
.get()
.exchangeToFlux(resp -> resp.bodyToFlux(Object.class)),
Object.class
));
}
ただし、従来のコントローラーを使用することも可能です。
それでは、テストを実行してみましょう。
リクエストの総数 | スループット、リクエスト/秒 | 応答時間、ミリ秒 | エラー率、% | |||
平均 | 一番小さい | 90%ライン | 最大 | |||
7443 | 219.2 | 2068年 | 12 | 3699 | 4381 | 0.00 |
結果は、スレッド プールを増やす場合よりもさらに優れています。ただし、スレッド プールと WebFlux にはそれぞれ独自の長所と短所があり、選択は特定の要件、ワークロードの性質、開発チームの専門知識によって決まることに注意することが重要です。
3. コルーチンと WebFlux
Kotlin コルーチンはリクエスト処理に効果的に使用でき、より同時かつノンブロッキングな方法で代替手段を提供します。
Spring WebFlux はリクエストを処理するためのコルーチンをサポートしているので、そのようなコントローラーを作成してみましょう。
@GetMapping
suspend fun callSlowServer(): Flow<Any> {
return slowServerClient.get().awaitExchange().bodyToFlow(String::class)
}
サスペンド関数は、基になるスレッドをブロックすることなく、長時間実行またはブロック操作を実行できます。Kotlin コルーチンの基本に関する記事では、基本について詳しく説明しています。
そこで、もう一度テストを実行してみましょう。
リクエストの総数 | スループット、リクエスト/秒 | 応答時間、ミリ秒 | エラー率、% | |||
平均 | 一番小さい | 90%ライン | 最大 | |||
7481 | 220.4 | 2064年 | 5 | 3615 | 4049 | 0.00 |
おおよそ、結果はコルーチンのない WebFlux アプリケーションの場合と変わらないと結論付けることができます。
しかし、コルーチンに加えて同じ WebFlux が使用されているため、テストではコルーチンの可能性が完全には明らかにされていない可能性があります。次回は、Ktor を試してみる価値があります。
4. 仮想スレッド (Project Loom)
仮想スレッドまたはファイバーは、Project Loom によって導入された概念です。
仮想スレッドはネイティブ スレッドよりもメモリ フットプリントが大幅に小さいため、アプリケーションはシステム リソースを使い果たすことなく、より多くのスレッドを作成および管理できます。スレッドの作成と切り替えを高速化できるため、スレッド作成に伴うオーバーヘッドが軽減されます。
仮想スレッドの実行の切り替えは、Java 仮想マシン (JVM) によって内部的に処理され、次の場所で実行できます。
自発的な一時停止: 仮想スレッドは、Thread.sleep() や CompletableFuture.await( などのメソッドを使用して、その実行を明示的に一時停止できます。 )。仮想スレッド自体が中断されると、実行は一時的に中断され、JVM は別の仮想スレッドの実行に切り替えることができます。
ブロッキング操作: 仮想スレッドは、I/O の待機やロックの取得などのブロッキング操作に遭遇すると、自動的に一時停止できます。これにより、JVM は基礎となるネイティブ スレッドを使用して、実行準備ができている他の仮想スレッドを実行することで、より効率的に利用できるようになります。
仮想スレッドとキャリア スレッドのトピックに興味がある場合は、DZone - Java Virtual Threads - A Brief Introduction に関するこの素晴らしい記事を読んでください。
仮想スレッドは Java 21 で最終的にリリースされますが、Java 19 からテストできるようになりました。JVM オプションを明示的に指定する必要があるだけです。
基本的に、Tomcat スレッド プールを仮想スレッド ベースのエグゼキュータに置き換えるだけです。
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandler() {
return protocolHandler ->
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
そこで、スレッド プール エグゼキュータの代わりに仮想スレッドを使用し始めました。
テストを実行してみましょう:
リクエストの総数 | スループット、リクエスト/秒 | 応答時間、ミリ秒 | エラー率、% | |||
平均 | 一番小さい | 90%ライン | 最大 | |||
7427 | 219.3 | 2080 | 12 | 3693 | 4074 | 0.00 |
結果は実際には WebFlux の場合と同じですが、リアクティブなテクノロジはまったく使用されていません。遅いサーバーからの呼び出しに対しても、通常のブロック RestTemplate を使用します。私たちが行ったのは、スレッド プールを仮想スレッド エグゼキュータに置き換えるだけです。
結論は
テスト結果を表に集めてみましょう。
リクエストハンドラ | 30 秒以内のリクエストの合計数 |
スループット、リクエスト/秒 | 応答時間、ミリ秒 | エラー率、% | |||
平均 | 一番小さい | 90%ライン | 最大 | ||||
スレッドプール (200 スレッド) | 3356 | 91.2 | 4787 | 155 | 6645 | 7304 | 0.00 |
スレッド プールを増やす (400 スレッド) | 6078 | 176.7 | 2549 | 10 | 4184 | 4855 | 0.00 |
ウェブフラックス | 7443 | 219.2 | 2068年 | 12 | 36999 | 4381 | 0.00 |
WebFlux + コルーチン | 7481 | 220.4 | 2064年 | 5 | 3615 | 4049 | 0.00 |
仮想スレッド (Project Loom) | 7427 | 219.3 | 2080 | 12 | 3693 | 4074 | 0.00 |
この記事で実行したパフォーマンス テストは表面的なものですが、いくつかの暫定的な結論を導き出すことができます。
- スレッド プールのパフォーマンス結果は低下します。スレッド数を増やすと改善される可能性がありますが、システムの容量とリソースの制約を考慮する必要があります。それでも、スレッド プールは、特に多くのブロック操作を扱う場合には実行可能なオプションです。
- WebFlux は 非常に良い結果を示しています。ただし、そのパフォーマンスを最大限に活用することは注目に値します。コード全体はレスポンシブ スタイルで記述する必要があります。
- コルーチンと WebFlux を組み合わせると、同様のパフォーマンス結果が得られます。おそらく、コルーチンの力を活用するために特別に設計されたフレームワークである Ktor を使ってそれらを試す必要があるでしょう。
- 仮想スレッド (Project Loom)を使用しても同様の結果が得られました。ただし、コードを変更したり、リアクティブな手法を使用したりしていないことは注目に値します。唯一の変更は、スレッド プールを仮想スレッド エグゼキューターに置き換えることです。シンプルであるにもかかわらず、スレッド プールを使用する場合と比較して、パフォーマンスの結果が大幅に向上します。
したがって、Java 21 での仮想スレッドのリリースは、既存のサーバーおよびフレームワークでのリクエスト処理の方法を大幅に変更すると最初に結論付けることができます。