AWS SDK for Java v2のバックグラウンドで動作するスレッドプール

今回はAWS SDK for Java v2 がバックグラウンドでどのようにスレッドプールを生成、使用しているか調べてみました。
2021.11.28

はじめに

AWS SDK for Java v2では非同期APIが提供され、NettyによるノンブロッキングI/Oが実現されています。これにより少ないスレッド数で多くのリクエストを処理することができます。しかしSDKを使う場合には同一のJavaプロセスの中でアプリケーションが本来行いたい処理をExecutorServiceなどを使って実行する場合がほとんどでしょう。Javaプロセスを実行する環境のリソースは有限なのでこれらの処理がスレッドプールやその他のExecutorServiceを生成、管理しているかを知るのはパフォーマンスの観点から非常に重要です。今回はAWS SDK for Java v2 がバックグラウンドでどのようにスレッドプールを生成、使用しているか調べてみました。

全体像

まずバックエンドであるNettyまで含めた概略図を示します。

大まかに言うとSDKが使用するスレッドプールは2つに分けられます。

  • (Nettyの)NIOEventLoopGroupのスレッドプール
  • SdkAsyncHttpClient のスレッドプール

NIOEventLoopGroupのスレッドプール

NIOEventLoopGroupはNettyにおけるEventLoopGroupの実装の一つです。一般にEventLoopGroupは任意の数のEventLoopを保持して複数のChannelを処理します。EventLoopは1つのスレッドと紐づいてI/Oイベント(リード、ライト、接続、切断など)を処理します。

AWS SDKが使用しているNIOEventLoopGroupでは固定数のEventLoopを保持しています。SDKではSDKEventLoopGroup.BuilderでNIOEventLoopGroupを生成しています。またこのビルダーでは別のEventLoopGroupを設定することもできます。

EventLoopによるI/Oイベント処理は効率化のために独自のスケジューリングが行われています。ここでは触れませんが詳細は「Netty in Action」に詳しいです。

SdkAsyncHttpClient のスレッドプール

SDKのドキュメント「Asynchronous programming」に以下のように記載があります。

The AWS SDK for Java 2.x uses Netty, an asynchronous event-driven network application framework, to handle I/O threads. The AWS SDK for Java 2.x creates an ExecutorService behind Netty, to complete the futures returned from the HTTP client request through to the Netty client. This abstraction reduces the risk of an application breaking the async process if developers choose to stop or sleep threads. By default, 50 Threads are generated for each asynchronous client, and managed in a queue within the ExecutorService.

ここではExecutorServiceとしか記載がないですが具体的にはThreadPoolExecutor が生成されます(デフォルトの挙動)。

ドキュメントにもあるとおりこのExecutorServiceは使用側で指定できます。

S3AsyncClient clientThread = S3AsyncClient.builder()
  .asyncConfiguration(
    b -> b.advancedOption(SdkAdvancedAsyncClientOption
      .FUTURE_COMPLETION_EXECUTOR,
      Executors.newFixedThreadPool(10)
    )
  )
  .build();

ところでこのExecutorServiceはSDK内のコードで抽象化されたHTTPクライアントの実装に対してリクエストのコールバックの実行用に指定されています。

コールバック用のExecutorServiceを分離することで、コールバック処理がブロックしてもバックエンド側(Netty)のスレッドはブロックしないように配慮されていますが、このスレッドプールのサイズが小さかったり頻繁にブロックするとスループットが落ちるので注意が必要なことがわかります。

まとめ

AWS SDKでは、少ないスレッド数でも高いパフォーマンスを実現するNettyに加えてコールバック処理をFUTURE_COMPLETION_EXECUTORでオフロードすることでデフォルトでも性能問題が起きにくい設計になっていることがわかりました。

しかし以下の場合にはFUTURE_COMPLETION_EXECUTORを設定するのが必要だと思います。

  • アプリケーションが性質の異なるワークロードで構成される場合
    • 頻繁にブロッキングする処理があるは他の処理とクライアントを共有しない
  • 多数のクライアントを生成する場合
    • この場合はExecutionServiceを共有したほうが全体としてみた時に使用リソース量やパフォーマンスを最適化できる可能性がある