最近のプロジェクトで、Dropwizard で書かれた老朽化したモノリシック Java Web サービスを最新化しました。このサービスは、AWS Lambda 関数を通じて多くのサードパーティ (3P) の依存関係を処理していましたが、アーキテクチャの同期性とブロック性の性質により、パフォーマンスに遅れが生じていました。このセットアップの P99 遅延は 20 秒で、サーバーレス機能が完了するのを待機している間、要求スレッドがブロックされました。このブロックによりスレッド プールが飽和状態になり、トラフィックのピーク時にリクエストが頻繁に失敗するようになりました。
問題の核心は、Lambda 関数への各リクエストが Java サービスのリクエスト スレッドを占有していることでした。これらの 3P 機能は完了するまでにかなりの時間がかかることが多いため、それらを処理するスレッドはブロックされたままとなり、リソースを消費し、スケーラビリティを制限します。このブロック動作がコードでどのように見えるかを示す例を次に示します。
// Blocking code example public String callLambdaService(String payload) { String response = externalLambdaService.invoke(payload); return response; }
この例では、callLambdaService メソッドは externalLambdaService.invoke() が応答を返すまで待機します。その間、他のタスクはスレッドを利用できません。
これらのボトルネックに対処するために、非同期でノンブロッキングなメソッドを使用してサービスを再構築しました。この変更には、Lambda 関数を呼び出して org.asynchttpclient ライブラリの AsyncHttpClient を使用する HTTP クライアントの使用が含まれており、内部で EventLoopGroup を使用してリクエストを非同期に処理します。
AsyncHttpClient を使用すると、プールからスレッドを消費せずにブロック操作をオフロードできました。更新されたノンブロッキング呼び出しの例を次に示します。
// Non-blocking code example public CompletableFuture<String> callLambdaServiceAsync(String payload) { return CompletableFuture.supplyAsync(() -> { return asyncHttpClient.invoke(payload); }); }
個々の呼び出しをノンブロッキングにすることに加えて、CompletableFuture を使用して複数の依存関係呼び出しをチェーンしました。 thenCombine や thenApply などのメソッドを使用すると、複数のソースからデータを非同期にフェッチして結合できるため、スループットが大幅に向上します。
CompletableFuture<String> future1 = callLambdaServiceAsync(payload1); CompletableFuture<String> future2 = callLambdaServiceAsync(payload2); CompletableFuture<String> combinedResult = future1.thenCombine(future2, (result1, result2) -> { return processResults(result1, result2); });
実装中に、Java のデフォルトの AsyncResponse オブジェクトには型安全性がなく、任意の Java オブジェクトが渡されてしまうことに気づきました。これに対処するために、ジェネリックを使用して SafeAsyncResponse クラスを作成しました。これにより、指定された応答タイプのみが返されるようになり、保守性が向上し、実行時エラーのリスクが軽減されます。このクラスは、応答が複数回書き込まれた場合にもエラーをログに記録します。
// Blocking code example public String callLambdaService(String payload) { String response = externalLambdaService.invoke(payload); return response; }
// Non-blocking code example public CompletableFuture<String> callLambdaServiceAsync(String payload) { return CompletableFuture.supplyAsync(() -> { return asyncHttpClient.invoke(payload); }); }
これらの変更の有効性を検証するために、仮想スレッドを使用して負荷テストを作成し、単一マシンでの最大スループットをシミュレートしました。さまざまなレベルのサーバーレス関数実行時間 (1 ~ 20 秒の範囲) を生成したところ、新しい非同期ノンブロッキング実装により、実行時間が短い場合はスループットが 8 倍、実行時間が長い場合は約 4 倍向上することがわかりました。
これらの負荷テストを設定する際、スループットを最大化するためにクライアント レベルの接続制限を調整するようにしました。これは、非同期システムのボトルネックを回避するために不可欠です。
これらのストレス テストを実行しているときに、カスタム HTTP クライアントに隠れたバグを発見しました。クライアントは、接続タイムアウトが Integer.MAX_VALUE に設定されたセマフォを使用しました。これは、クライアントが利用可能な接続を使い果たすと、スレッドを無期限にブロックすることを意味します。このバグを解決することは、高負荷シナリオでの潜在的なデッドロックを防ぐために非常に重要でした。
なぜ単純に仮想スレッドに切り替えなかったのか不思議に思う人もいるかもしれません。これにより、リソースを大幅に消費せずにスレッドがブロックできるようになり、非同期コードの必要性が軽減されます。ただし、仮想スレッドには現在の制限があります。仮想スレッドは同期操作中に固定されます。これは、仮想スレッドが同期ブロックに入るとアンマウントできず、操作が完了するまで OS リソースがブロックされる可能性があることを意味します。
例:
CompletableFuture<String> future1 = callLambdaServiceAsync(payload1); CompletableFuture<String> future2 = callLambdaServiceAsync(payload2); CompletableFuture<String> combinedResult = future1.thenCombine(future2, (result1, result2) -> { return processResults(result1, result2); });
このコードでは、利用可能なデータがないために読み取りがブロックされた場合、仮想スレッドは OS スレッドに固定され、アンマウントされずに OS スレッドもブロックされます。
幸いなことに、JEP 491 が目前に迫っているため、Java 開発者は仮想スレッドの動作の改善を期待できます。これにより、プラットフォーム スレッドを使い果たすことなく、同期されたコード内のブロック操作をより効率的に処理できるようになります。
サービスを非同期ノンブロッキング アーキテクチャにリファクタリングすることで、大幅なパフォーマンスの向上を達成しました。 AsyncHttpClient を実装し、型安全性のために SafeAsyncResponse を導入し、負荷テストを実施することで、Java サービスを最適化し、スループットを大幅に向上させることができました。このプロジェクトは、モノリシック アプリケーションを最新化するための貴重な演習であり、スケーラビリティのための適切な非同期プラクティスの重要性を明らかにしました。
Java が進化するにつれて、将来的には仮想スレッドをより効果的に活用できるようになるかもしれませんが、現時点では、待ち時間の長いサードパーティ依存のサービスにおけるパフォーマンスの最適化には、非同期およびノンブロッキング アーキテクチャが不可欠なアプローチであり続けます。
以上が非同期およびノンブロッキング アーキテクチャによるパフォーマンス向上のための Java モノリスの最新化の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。