The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

頭痛の種にならない超パフォーマンス

このブログ記事の目的は、RESTEasy Reactive についての混乱を解消し、よくある質問に答えることです。

謝辞

このブログ記事は、 Clement Escoffier氏と Stéphane Épardaud氏の専門的なアドバイスなしには実現できませんでした。

命令型とリアクティブ型: エレベーターピッチ

RESTEasy Reactive が重要な理由と、RESTEasy Classic と異なる点を理解するために、ここ で最初に紹介した非常に重要なメッセージを再び考察してみましょう。

一般的に、Java のWeb アプリケーションでは、ブロッキング IO 操作と組み合わせた命令型プログラミングを使用します。これは、コードを推論するのが簡単なので、非常に人気があります。物事は順次実行されます。アプリケーションがリクエストを受け取ると、フレームワークはこのリクエストをワーカースレッドに関連付けます。リクエスト処理がデータベースや他のリモートサービスと対話する必要があるときは、ブロッキング IO に依存します。スレッドは応答を待ってブロックされ、通信を同期化します。このモデルでは、1 つのリクエストは別のスレッドで実行されるので、別のリクエストの影響を受けません。1 つのスレッドが待機している場合でも、異なるスレッド上で実行されている他のリクエストが大幅に遅くなることはありません。

しかし、このモデルでは、同時実行リクエストごとに1つのスレッドが必要となり、達成可能な同時実行性に制限が生じます。一方、リアクティブ実行モデルでは、非同期開発モデルとノンブロッキング IO を採用しています。このモデルでは、複数のリクエストを同じスレッドで処理することができます。(リモートサービスをリクエストしたり、データベースと対話したりするために) リクエストの処理が進まなくなった場合は、ノンブロッキング IO を使用します。スレッドをブロックする代わりに、操作をスケジュールし、操作の完了後に呼び出される継続を渡します [1]。これによりスレッドはすぐに解放され、別のリクエストに対応するために使用することができます。IO 操作の結果が利用可能になると、リクエストの処理が再開され、その実行が継続されます。

このモデルでは、単一の IO スレッドを使用して複数のリクエストを処理することができます。3 つの大きなメリットがあります。

  • まず、別のスレッドにジャンプする必要がないので、レスポンス時間が短くなります。

  • 第 2 に、スレッドの使用量が減るため、メモリーの消費量を減らすことができます。

  • 第 3 に、並行処理はスレッド数に制限されなくなりました。

リアクティブモデルはハードウェアリソースをより効率的に使用しますが、重大な落とし穴が潜んでいます。もしリクエストの処理がブロックされ始めると、他のリクエストが処理できなくなるため、本当にすぐに事態が悪化してしまいます。これを避けるためには、非同期でノンブロッキングなコードの書き方、操作のスケジュールの立て方、連続処理の書き方、アクションの連鎖の仕方などを学ぶ必要があります。基本的には、非同期処理を構造化し、ノンブロッキングIOを使う方法が必要です。これは間違いなく、パラダイムシフトであることは間違いありません。Quarkusでは、このシフトをできるだけ簡単にしたいと考えているので、RESTEasy Reactiveでは、エンドポイントがブロッキングかノンブロッキングかを選択することができます (アプリケーションはブロッキングとノンブロッキングのメソッドを自由に組み合わせて使用することができます)。インフラストラクチャはリアクティブですが、あなたのコードはリアクティブ型にも命令型にもなり得ます。これが、リアクティブ型と命令型の統一の意味です。

RESTEasy Reactive な視点から見た場合の意味

RESTEasy Reactive は、デフォルトでは IO スレッド(イベントループスレッドとして知られている)上の各 HTTP リクエストを処理します [2]

以下のイメージでは、ハイレベルの様子を示しています。

RR non blocking

これにより、最大のスループットを達成することがきます。ただし、エンドポイントメソッドの実装がタイムリーに完了しなければならないことも意味しています。そうでなければ、スレッドの使用が長くなり過ぎてしまい [3]、他のリクエストがキューイングされ、スループットの低下につながります。

命令型コードを使用するメソッド本体が問題になるのは、実行に長い時間がかかるときだけ - ブロッキング IO 処理がほぼ全てのケース - であることを理解することが重要です。

そのため、メソッドの本体が何らかのブロッキング IO 操作 (あるいは完了までに時間を要する CPU バインド操作) を実行する場合、リクエストはワーカースレッドにオフロードされる必要があります。RESTEasy Reactiveでは、@Blocking アノテーションを使用して宣言的に行われます。Quarkus は、IO スレッドでブロッキング IO を使用しようとした場合にも警告します。しかし、メソッド本体がノンブロッキング IO (または非常に速く完了するCPUバインド操作) を実行する場合、RESTEasy Reactive は、IO スレッド上でリクエスト全体を提供し続けることができます。

RESTEasy Reactive はリアクティブAPIの使用に限定されているのでしょうか?

もちろん違います。

RESTEasy Reactive は、ノンブロッキング IO とイベントループスレッドからのリクエストを処理するために一から構築されていますが(そのため、ワーカープールスレッドの不要な使用を避けることができます)、ブロッキング IO と、(Hibernate のような) ブロッキング APIを提供するあらゆるコードをイベントループをブロッキングせずに簡単に動作させることができます。

エンドポイントのメソッドやクラスに @Blocking を追加するだけです。これだけです。@Blocking を使用すると、通常のディスパッチの仕組み:ワーカースレッドがメソッドの実行に使用される方式に戻れます。

ハイレベルでは以下のようになります。

RR blocking

RESTEasy ReactiveはHibernate Reactiveを必要としますか?

前の質問の答えからわかる通り、答えは「ノー」です。

RESTEasy Reactive が Hibernate と一緒に使用されるシナリオでは、 @Blocking アノテーションを Hibernate と相互作用するエンドポイントメソッドに配置する必要があります。

RESTEasy Reactive が Hibernate Reactive とともに使用されるシナリオでは、Hibernate Reactive と相互作用するエンドポイントメソッドに @Blocking アノテーションは必要ありません。

@Blocking を使用することによるパフォーマンスへの影響について

エンドポイントメソッドがノンブロッキング (つまり、HTTP リクエストがイベントループスレッドから完全に提供される) の場合には、絶対的に最高のスループットが達成されますが、 @Blocking を使用していても優れたパフォーマンスを達成することができます。

私たちのベンチマークでは、@Blocking を使用することで最大スループットが約 30%footnote 低下することがわかります [4]

しかし、RESTEasy Reactive で @Blocking を使用したエンドポイントメソッドでは、RESTEasy Classic を使用した同じメソッドよりも約 50% 高いスループットを達成しています。

なぜ @Blocking を使用した RESTEasy Reactive は RESTEasy Classic よりもパフォーマンスが良いのですか?

RESTEasy Reactive は、RESTEasy Classic と比較して、以下のような特徴を持っています。

  • IO に関連するすべてを Eclipse Vert.x と緊密に統合します。Vert.x は IO 操作のために非常によく最適化されています。そのため、RESTEasy Reactive はそれとの緊密な統合により、その恩恵を受けることができます。RESTEasy Classic on Quarkus も同様に Vert.x を使用しています。ただし、その場合、統合はそれほど深くないため、Vert.x のパワーを十分に活用することができません。

  • 多くの作業をビルド時に移行しています。RESTEasy Reactive は、Quarkus のニーズに応えるためにゼロから構築されています。そのため、Quarkus との統合が可能な限り緊密に行われており、おそらく最も多くをビルド時に作業を行うエクステンションとなっています。これにより、各リクエストを処理するための最適なデータパイプラインを作成し、実行時の操作をインラインで行うバイトコードを生成することで JIT コンパイラを支援し、実行時の (メソッドの呼び出しと型の決定のための) リフレクションを排除し、メモリーの割り当てを削減します。

  • ThreadLocals の使用を避け、代わりに必要な情報をすべて含むコンテキストオブジェクトを利用します。ThreadLocals はフレームワークのさまざまな部分でデータを利用できるようにする便利な方法です。ただし、その頻繁な使用にはコストがかかるため、RESTEasyReactive では完全に回避されています。

  • 必要なすべてのインジェクションに最適な方法でArcを活用します。RESTEasy Classic は、さまざまなインジェクション操作を実行する抽象化レイヤーを提供しますが、Arcは同じ機能をより優れたパフォーマンスで提供するため、Quarkus のニーズには全く不要です。

RESTEasy Classic with Mutiny との比較はどうでしょうか?

RESTEasy Classic を使用しているときに Quarkus では、quarkus-resteasy-mutiny エクステンションを介して Mutiny 戻り値の型 (UniとMulti) を使用することができることや、これが RESTasy Reactive の使用と、どのように異なるかと思うかもしれません。

RESTEasy Classic について最も理解しておくべきこととして、RESTEasy Classic はイベントループの概念を一切使用しないため、*常に*ワーカースレッド上でリクエストを処理することがあります。

これを最もよく表しているのは、以下の画像です。

CR

そのため、RESTEasy Classic を使用する場合、UniMulti のようなリアクティブ型を返しても、最初のリクエストはワーカースレッドで処理され、ライブラリーの呼び出しはノンブロッキング IO になることがあります。それでも IO の待機中にブロックされると、RESTEasyClassic がワーカースレッドを再利用する方法はありません。

このように、RESTEasy Classic でリアクティブな戻り値型を使用することで得られる利益は、ランタイムの利益ではなく、構文的な利益です。リアクティブ型を使用していても、基礎となるハードウェアが、より効率的に使用されることはありません。

RESTEasy Reactive を使用して Mutiny 型を返す場合、すべての処理は IO スレッド上で行われます (エンドポイントが @Blocking でアノテーションされている場合を除く)。RESTEasy Reactive で Mutiny を使用するための外部エクステンションは不要です。

最大のパフォーマンスを実現するためには、新しい RESTEasy Reactive API を使用する必要がありますか?

RESTEasy Reactive のドキュメントを読むと、すぐにリクエストフィルター (@ServerRequestFilter)、レスポンスフィルター (@ServerResponseFilter)、例外マッパー (@ServerExceptionMapper) を記述するための新しい API に出くわします。これらの使用法が標準の JAX-RS API (ContainerRequestFilter, ContainerResponseFilter, ExceptionMapper) と比べてパフォーマンスに影響を与えるかどうかを疑問に思うかもしれません。

後者のケースで @Context を使用する場合、新しい API は古い API を使用するよりもわずかなパフォーマンスの優位性を与えます。ただし、その優位性は取るに足らない程度で、可能な限りのパフォーマンスを限界まで引き出すのでない限り、心配する必要はありません。どちらの API を使ってフィルターを書く場合でも注意すべきことは、javax.ws.rs.container.ResourceInfo の代わりに org.jboss.resteasy.reactive.server.SimpleResourceInfo を使うことです。

新しい API によってパフォーマンスが顕著に向上する特別なケースとして、MessageBodyReaderMessageBodyWriter クラスがあります。HTTP リクエストの読み込みと HTTP レスポンスの書き込みの際に、 ServerMessageBodyReaderServerMessageBodyWriter を使用することで、RESTEasy Reactive はリクエストを提供するためのデータパスを最適化することができます。

リアクティブルートは?

Quarkusはすでに IO スレッドから HTTP リクエストを処理する方法を提供していました。Reactive Routes (リアクティブルート) は、HTTP API を実装するための宣言モデルを提供します。各ルートは、IO スレッド (デフォルト) またはワーカースレッド (@Blocking アノテーションを使用) で呼び出すことができます。この記事で強調されているように、Reactive Routes は非常に優れたスループットとパフォーマンスを提供します。リアクティブルートは RESTEasy Reactive と比較してどうでしょうか?

Reactive Routes についての主な不満の 1 つは、開発モデルに関するものでした。RESTEasy で使用したものとは大きく異なります。しかし、Reactive Routes を使用することで、Quarkus 上にエンドツーエンドのリアクティブモデルを使用することで得られるパフォーマンスと効率性のメリットを検証することができました。RESTEasy Reactive は「次世代」と考えることができます。

まとめ

RESTEasy Reactive は次世代の HTTP フレームワークです。リアクティブ型 (ノンブロッキング IO、非同期 API) と命令型 (@Blocking アノテーションを使用) を統合しています。ユーザーエクスペリエンスを制限することなく、生のパフォーマンスを向上させます。その命令型/リアクティブ型の二面性により、高度に並列化された HTTP API から、より伝統的なトランザクション型の CRUD アプリケーションまで、あらゆるユースケースに適合するようになります。

RESTEasy Reactive は、近い将来 Quarkus のデフォルト HTTP レイヤーになると思われます。また、開発者にうれしい新機能を導入しながら、可能な限り最高のパフォーマンスを実現することに完全にコミットしています。

この短いブログ記事が、RESTEasy Reactive が特別である理由と、RESTEasy Reactive について抱いていた誤解を解き明かすためのヒントになればと思います。


1. この記事ではOSとノンブロッキング IO ライブラリがどのようにそういったモデルを実現しているかの詳細には踏み込みません。内部的に、select、epoll、ICMPといったカーネルメカニズムがIO処理をスピードとリソース利用の観点で非常に効率的にしています
2. RESTEasy Reactiveの実行モデルについて詳細は ここ にあります
3. "長過ぎる" は目標とする並列度次第です。頻繁に使用されるエンドポイントでは 1ms も長過ぎると考える場合もありますし、あまり使われないエンドポイントでは 100ms でも許容可能かもしれません。
4. これは基本的にワーカースレッドにリクエストを振り分けるためにかかるコストです