§Playのスレッドプールの概要
Play Frameworkは、根本から非同期ウェブフレームワークです。Playのスレッドプールは、従来のウェブフレームワークよりも少ないスレッドを使用するように調整されています。これは、play-coreのIOが決してブロックしないためです。
そのため、ブロッキングIOコードまたは潜在的に多くのCPU集約的な作業を行う可能性のあるコードを記述する場合は、どのスレッドプールがそのワークロードを処理しているかを正確に把握し、それに応じて調整する必要があります。これを考慮せずにブロッキングIOを行うと、Play Frameworkのパフォーマンスが非常に低下する可能性があります。たとえば、CPU使用率が5%の状態でも、処理されるリクエストが毎秒わずか数件しかなくなる可能性があります。対照的に、一般的な開発ハードウェア(例:MacBook Pro)でのベンチマークでは、Playが正しく調整されていれば、毎秒数百、場合によっては数千ものリクエストを簡単に処理できることが示されています。
§ブロッキングが発生しているかどうかの確認
一般的なPlayアプリケーションでブロッキングが発生する最も一般的な場所は、データベースとの通信です。残念ながら、主要なデータベースのどれもJVM用の非同期データベースドライバを提供していないため、ほとんどのデータベースでは、ブロッキングIOを使用するしかありません。この例外として注目すべきは、ReactiveMongoであり、これはPlayのIterateeライブラリを使用してMongoDBと通信するMongoDBのドライバです。
コードがブロックする可能性のあるその他のケースには、以下が含まれます。
- サードパーティのクライアントライブラリ(つまり、Playの非同期WS APIを使用しない)を介したREST/WebService APIの使用
- 一部のメッセージングテクノロジは、メッセージを送信するための同期APIのみを提供します
- ファイルやソケットを直接開く場合
- 実行に時間がかかるという事実によってブロックするCPU集約的な操作
一般的に、使用しているAPIが`Future`を返す場合、非ブロッキングです。そうでない場合は、ブロッキングです。
したがって、ブロッキングコードをFutureでラップしようとすると、非ブロッキングにはなりません。これは、ブロッキングが別のスレッドで発生することを意味するだけです。使用しているスレッドプールに、ブロッキングを処理できるだけの十分なスレッドがあることを確認する必要があります。https://playframework.com/download#examplesのPlayの例テンプレートを参照して、ブロッキングAPI用にアプリケーションを構成する方法を確認してください。
対照的に、次のタイプのIOはブロックしません。
- Play WS API
- ReactiveMongoなどの非同期データベースドライバ
- Pekko actorへの/からのメッセージの送受信
§Playのスレッドプール
Playは、さまざまな目的で多数の異なるスレッドプールを使用します。
-
**内部スレッドプール** - これらは、IOの処理のためにサーバーエンジンによって内部的に使用されます。アプリケーションのコードは、これらのスレッドプール内のスレッドによって実行されるべきではありません。PlayはデフォルトでPekko HTTPサーバーバックエンドで構成されているため、バックエンドを変更するには、`application.conf`の設定を使用する必要があります。または、PlayにはNettyサーバーバックエンドも付属しており、有効にすると、`application.conf`から設定できる設定もあります。
-
**Playのデフォルトスレッドプール** - これは、Play Frameworkのすべてのアプリケーションコードが実行されるスレッドプールです。これはPekkoディスパッチャであり、アプリケーション`ActorSystem`によって使用されます。これは以下で説明するPekkoを構成することで設定できます。
§デフォルトスレッドプールの使用
Play Frameworkのすべてのアクションは、デフォルトスレッドプールを使用します。非同期操作(たとえば、futureに対する`map`または`flatMap`の呼び出し)を実行する際には、指定された関数をその中で実行するための暗黙的な実行コンテキストを提供する必要がある場合があります。実行コンテキストは、基本的に`ThreadPool`の別の名前です。
ほとんどの場合、使用する適切な実行コンテキストは**Playのデフォルトスレッドプール**です。これは`@Inject()(implicit ec: ExecutionContext)`を介してアクセスできます。これはScalaソースファイルに注入して使用できます。
class Samples @Inject() (components: ControllerComponents)(implicit ec: ExecutionContext)
extends AbstractController(components) {
def someAsyncAction = Action.async {
someCalculation()
.map { result => Ok(s"The answer is $result") }
.recover {
case e: TimeoutException =>
InternalServerError("Calculation timed out!")
}
}
def someCalculation(): Future[Int] = {
Future.successful(42)
}
}
または、Javaコードで`CompletionStage`と`ClassLoaderExecutionContext`を使用します。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import play.libs.concurrent.ClassLoaderExecutionContext;
import play.mvc.*;
public class MyController extends Controller {
private ClassLoaderExecutionContext clExecutionContext;
@Inject
public MyController(ClassLoaderExecutionContext ec) {
this.clExecutionContext = ec;
}
public CompletionStage<Result> index() {
// Use a different task with explicit EC
return calculateResponse()
.thenApplyAsync(
answer -> {
return ok("answer was " + answer).flashing("info", "Response updated!");
},
clExecutionContext.current());
}
private static CompletionStage<String> calculateResponse() {
return CompletableFuture.completedFuture("42");
}
}
この実行コンテキストは、アプリケーションの`ActorSystem`に直接接続し、Pekkoのデフォルトディスパッチャを使用します。
§デフォルトスレッドプールの設定
デフォルトスレッドプールは、`pekko`名前空間の下にある`application.conf`で標準のPekko設定を使用して設定できます。
デフォルトディスパッチャを設定する場合、別のディスパッチャを使用する場合、または使用する新しいディスパッチャを定義する場合は、詳細については、Pekkoのリファレンスドキュメントのディスパッチャの種類セクションを参照してください。
使用可能な完全な設定オプションは、設定セクションにあります。
§その他のスレッドプールの使用
特定の状況では、他のスレッドプールに作業をディスパッチしたい場合があります。これには、CPU集約的な作業や、データベースアクセスなどのIO作業が含まれる場合があります。これを行うには、最初に`ThreadPool`を作成する必要があります。これはScalaで簡単に実行できます。
val myExecutionContext: ExecutionContext = actorSystem.dispatchers.lookup("my-context")
この場合、Pekkoを使用して`ExecutionContext`を作成していますが、Java executorやScala fork joinスレッドプールなどを使用して独自の`ExecutionContext`を簡単に作成することもできます。Playは、独自のコンテキストを作成するために使用できる`play.libs.concurrent.CustomExecutionContext`と`play.api.libs.concurrent.CustomExecutionContext`を提供します。詳細については、ScalaAsyncまたはJavaAsyncを参照してください。
このPekko実行コンテキストを設定するには、次の設定を`application.conf`に追加します。
my-context {
fork-join-executor {
parallelism-factor = 20.0
parallelism-max = 200
}
}
この実行コンテキストをScalaで使用する場合は、scalaの`Future`コンパニオンオブジェクト関数を使用するだけです。
Future {
// Some blocking or expensive code here
}(myExecutionContext)
または、暗黙的に使用することもできます。
implicit val ec = myExecutionContext
Future {
// Some blocking or expensive code here
}
さらに、ブロッキングAPI用にアプリケーションを構成する方法の例については、https://playframework.com/download#examplesの例テンプレートを参照してください。
§クラスローダー
クラスローダーは、Playプログラムなどのマルチスレッド環境では特別な処理が必要です。
§アプリケーションクラスローダー
Playアプリケーションでは、スレッドコンテキストクラスローダーが常にアプリケーションクラスをロードできるとは限りません。クラスをロードするには、アプリケーションクラスローダーを明示的に使用する必要があります。
- Java
-
Class myClass = app.classloader().loadClass(myClassName);
- Scala
-
val myClass = app.classloader.loadClass(myClassName)
クラスのロードを明示的に行うことは、本番モードではなく開発モード(`run`を使用)でPlayを実行する場合に最も重要です。これは、Playの開発モードが複数のクラスローダーを使用するため、アプリケーションの自動リロードをサポートできるためです。Playのスレッドの一部は、アプリケーションのクラスのサブセットしか認識していないクラスローダーにバインドされている可能性があります。
場合によっては、アプリケーションクラスローダーを明示的に使用できない場合があります。これは、サードパーティライブラリを使用する場合に当てはまることがあります。この場合は、サードパーティのコードを呼び出す前に、スレッドコンテキストクラスローダーを明示的に設定する必要がある場合があります。設定する場合は、サードパーティのコードの呼び出しが終了したら、コンテキストクラスローダーを以前の値に戻すことを忘れないでください。
§スレッドの切り替え
しかし、クラスローダーの問題点は、制御が別のスレッドに切り替わるとすぐに、元のクラスローダーへのアクセスを失うことです。そのため、`thenApplyAsync` を使用して、または `Future` に関連付けられた `CompletionStage` が完了した後の時点で `thenApply` を使用して `CompletionStage` をマップし、その後元のクラスローダーにアクセスしようとすると、おそらく機能しません。この問題に対処するために、Play は ClassLoaderExecutionContext
を提供します。これにより、現在のクラスローダーを `Executor` にキャプチャできます。その後、`CompletionStage` の `*Async` メソッド(`thenApplyAsync()` など)に渡すことができ、エグゼキュータがコールバックを実行すると、クラスローダーがスコープ内に残るようになります。
ClassLoaderExecutionContext
を使用するには、コンポーネントにそれをインジェクトし、`CompletionStage` と対話するたびに現在のコンテキストを渡します。例:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import play.libs.concurrent.ClassLoaderExecutionContext;
import play.mvc.*;
public class MyController extends Controller {
private ClassLoaderExecutionContext clExecutionContext;
@Inject
public MyController(ClassLoaderExecutionContext ec) {
this.clExecutionContext = ec;
}
public CompletionStage<Result> index() {
// Use a different task with explicit EC
return calculateResponse()
.thenApplyAsync(
answer -> {
return ok("answer was " + answer).flashing("info", "Response updated!");
},
clExecutionContext.current());
}
private static CompletionStage<String> calculateResponse() {
return CompletableFuture.completedFuture("42");
}
}
カスタムエグゼキュータがある場合は、それを `ClassLoaderExecutionContext` のコンストラクタに渡すだけで、`ClassLoaderExecutionContext` でラップできます。
§ベストプラクティス
アプリケーション内の作業を異なるスレッドプール間で最適に分割する方法は、アプリケーションが実行する作業の種類と、並列に実行できる作業の量をどの程度制御したいかによって大きく異なります。この問題に対する万能な解決策はなく、最適な決定は、アプリケーションのブロッキングIO要件とそのスレッドプールへの影響を理解することから得られます。アプリケーションの負荷テストを実行して、構成を調整および検証することが役立つ場合があります。
注記:ブロッキング環境では、ワークスティーリングは不可能であるため、`thread-pool-executor` は `fork-join` よりも優れており、`fixed-pool-size` サイズを使用し、基になるリソースの最大サイズに設定する必要があります。
JDBC はブロッキングであるという事実を考えると、スレッドプールは、スレッドプールがデータベースアクセス専用に使用されていると仮定して、データベースプールで使用可能な接続の数にサイズ設定できます。少ないスレッドでは、使用可能な接続数が消費されません。使用可能な接続数よりも多くのスレッドは、接続の競合を考えると無駄になる可能性があります。
以下では、Play Framework で使用したい一般的なプロファイルの概要を示します。
§純粋な非同期
この場合、アプリケーションではブロッキングIOを実行していません。ブロッキングがないため、プロセッサごとに1つのスレッドというデフォルトの構成はユースケースに完全に適合するため、追加の構成は必要ありません。Play のデフォルトのエグゼキューションコンテキストは、すべての場合に使用できます。
§高度に同期
このプロファイルは、Javaサーブレットコンテナなどの従来の同期IOベースのWebフレームワークのプロファイルと一致します。ブロッキングIOを処理するために大きなスレッドプールを使用します。これは、ほとんどのアクションがデータベースの同期IO呼び出し(データベースへのアクセスなど)を実行しており、さまざまな種類の作業の同時実行を制御したくない場合、または必要がないアプリケーションに役立ちます。このプロファイルは、ブロッキングIOを処理するための最も簡単な方法です。
このプロファイルでは、すべての場所でデフォルトのエグゼキューションコンテキストを使用しますが、プール内のスレッド数を非常に多くするように構成します。デフォルトのスレッドプールは、Playリクエストとデータベースリクエストの両方のサービスに使用されるため、固定プールサイズは、データベース接続プールの最大サイズ、コア数、およびハウスキーピング用の追加の2つを合計した値にする必要があります。
pekko {
actor {
default-dispatcher {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = 55 # db conn pool (50) + number of cores (4) + housekeeping (1)
}
}
}
}
このプロファイルは、Javaアプリケーションで他のスレッドに作業をディスパッチするのが難しいため、同期IOを行うJavaアプリケーションに推奨されます。
さらに、ブロッキングAPI用にアプリケーションを構成する方法の例については、https://playframework.com/download#examplesの例テンプレートを参照してください。
§多くの特定のスレッドプール
このプロファイルは、多くの同期IOを実行したいが、アプリケーションが一度に実行する操作の種類と量を正確に制御したい場合に役立ちます。このプロファイルでは、デフォルトのエグゼキューションコンテキストで非ブロッキング操作のみを実行し、ブロッキング操作をそれらの特定の操作用の異なるエグゼキューションコンテキストにディスパッチします。
この場合、次のように、さまざまな種類の操作に対して多数の異なるエグゼキューションコンテキストを作成できます。
object Contexts {
implicit val simpleDbLookups: ExecutionContext = actorSystem.dispatchers.lookup("contexts.simple-db-lookups")
implicit val expensiveDbLookups: ExecutionContext =
actorSystem.dispatchers.lookup("contexts.expensive-db-lookups")
implicit val dbWriteOperations: ExecutionContext =
actorSystem.dispatchers.lookup("contexts.db-write-operations")
implicit val expensiveCpuOperations: ExecutionContext =
actorSystem.dispatchers.lookup("contexts.expensive-cpu-operations")
}
これらは次のように構成される場合があります。
contexts {
simple-db-lookups {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = 20
}
}
expensive-db-lookups {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = 20
}
}
db-write-operations {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = 10
}
}
expensive-cpu-operations {
fork-join-executor {
parallelism-max = 2
}
}
}
次にコードで、`Future` を作成し、`Future` が実行している作業の種類に関連する `ExecutionContext` を渡します。
注記:構成の名前空間は、`app.actorSystem.dispatchers.lookup` に渡されるディスパッチャIDと一致する限り、自由に選択できます。`CustomExecutionContext` クラスは、これを自動的に実行します。
§少数の特定のスレッドプール
これは、多くの特定のスレッドプールと高度に同期されたプロファイルの中間です。ほとんどの単純なIOをデフォルトのエグゼキューションコンテキストで行い、そこでスレッド数をかなり高く設定しますが(100など)、特定のコントテキストに特定の高価な操作をディスパッチし、一度に実行される数を制限します。
§スレッドプールのデバッグ
ディスパッチャには多くの設定が可能であり、特にデフォルトのディスパッチャをオーバーライドする場合、どの設定が適用され、デフォルトの設定が何であるかを把握するのが困難な場合があります。`pekko.log-config-on-start` 構成オプションは、アプリケーションの読み込み時に適用された構成全体を表示します。
pekko.log-config-on-start = on
出力を見るにはPekkoのログレベルをデバッグレベルに設定する必要があるため、`logback.xml`に以下を追加する必要があります。
<logger name="org.apache.pekko" level="DEBUG" />
ログ出力されたHOCON出力が表示されたら、それを「example.conf」ファイルにコピーアンドペーストし、IntelliJ IDEAで表示できます(HOCON構文をサポートしています)。Pekkoのディスパッチャとマージされた変更が表示されます。そのため、`thread-pool-executor`をオーバーライドすると、マージされたものが表示されます。
{
# Elided HOCON...
"actor" : {
"default-dispatcher" : {
# application.conf @ file:/Users/wsargent/work/catapi/target/universal/stage/conf/application.conf: 19
"executor" : "thread-pool-executor"
}
}
}
Playには、開発モードと本番モードで異なる構成設定があることにも注意してください。スレッドプールの設定が正しいことを確認するには、本番構成でPlayを実行する必要があります。
このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを自由に送ってください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティとの会話を開始してください。