§Play のスレッドプールを理解する
Play framework is, from the bottom up, an asynchronous web framework. Streams are handled asynchronously using iteratees. Thread pools in Play are tuned to use fewer threads than in traditional web frameworks, since IO in play-core never blocks.
このため、ブロッキング IO や、潜在的に多くの CPU を集約して実行可能なコードを書きたいと考えた場合に、どのスレッドプールがその処理を実行しているかを知り、それに応じて調整する必要があります。これを考慮に入れずにブロッキング IO を行うと、Play framework のパフォーマンスは貧弱になり得ます。例えば、1 秒あたりほんの少数のリクエストを扱う場合にも、 CPU 使用率が 5% に貼り付くのを目にするかもしれません。それに比べて、 MacBook Pro のような典型的な開発ハードウェア上のベンチマークでは、汗を流して正確に調整しなくても、 Play が毎秒何百、何千リクエストの仕事量を扱うことができることを示しました。
§いつブロッキングされるかを知る
データベースと通信との通信は Play アプリケーションがブロックされる典型的な例です。不幸なことに、メジャーなデータベースでも JVM での非同期なドライバーを提供しているものはありませんし、ほとんどのデータベースではブロック IO を使うことを選ぶことになります。MongoDB 用のドライバーである ReactiveMongo は、Play の Iteratee ライブラリを使う注目すべき例外です。
その他のブロックするかもしれないコードは以下のようになっています。
- (Play の非同期 WS API を使わずに) サードパーティのクライアントライブラリで REST/Web サービス API を使う
- メッセージを送る際に、同期 API のみ提供されているメッセージ技術を使っている
- ファイル、ソケットを直接自分で Open した場合
- 実行に長い時間がかかるためブロックする CPU 集約的なオペレーション
一般的に、使用している API が future を返す場合はノンブロッキングですが、そうでなければブロッキングです。
このため、Future でブロッキングコードをラップするという誘惑に駆られるかもしれないことに注意してください。こうすることではノンブロッキングになりませんし、ブロッキングが他のスレッド内で起こるかもしれません。使用しているスレッドプールがブロッキングを扱うための十分なスレッドを持っていることを確かめる必要があります。
対照的に、以下のタイプの IO はブロックしません:
- Play WS API
- ReactiveMongo のような非同期データベースドライバー
- Akka アクターへメッセージを送ったり、Akka アクターからのメッセージを受け取る
§Play のスレッドプール
Play は複数の異なる目的のためのスレッドプールを使っています。
- Netty boss/worker thread pools - These are used internally by Netty for handling Netty IO. An applications code should never be executed by a thread in these thread pools.
- Play Internal Thread Pool - This is used internally by Play. No application code should ever be executed by a thread in this thread pool, and no blocking should ever be done in this thread pool. Its size can be configured by setting
internal-threadpool-size
inapplication.conf
, and it defaults to the number of available processors. - Play default thread pool - This is the default thread pool in which all application code in Play Framework is executed. It is an Akka dispatcher, and can be configured by configuring Akka, described below. By default, it has one thread per processor.
- Akka thread pool - This is used by the Play Akka plugin, and can be configured the same way that you would configure Akka.
§デフォルトスレッドプールを使用する
Play Framework での全アクションはデフォルトスレッドプールを使用します。例えば、ある非同期動作を行う場合、future での map
あるいは flatMap
を呼ぶ場合に、与えられた機能内での実行するために暗黙の実行コンテキストを提供する必要があるかもしれません。実行コンテキストは基本的にスレッドプール用の別名です。
ほとんどの状況で、使用する適切な実行コンテキストは Play デフォルトスレッドプールになるでしょう。
Scala ソースファイルへ import することによって使用することができます:
import play.api.libs.concurrent.Execution.Implicits._
def someAsyncAction = Action.async {
import play.api.Play.current
WS.url("http://www.playframework.com").get().map { response =>
// This code block is executed in the imported default execution context
// which happens to be the same thread pool in which the outer block of
// code in this action will be executed.
Results.Ok("The response code was " + response.status)
}
}
§デフォルトスレッドプールを設定する
デフォルトスレッドプールは application.conf
内の play
名前空間で標準 Akka 設定を使用して設定することができます。デフォルト設定は以下の通りです:
play {
akka {
akka.loggers = ["akka.event.Logging$DefaultLogger", "akka.event.slf4j.Slf4jLogger"]
loglevel = WARNING
actor {
default-dispatcher = {
fork-join-executor {
parallelism-factor = 1.0
parallelism-max = 24
}
}
}
}
}
この設定はプール内で 24 スレッドを最大として、有効なプロセッサーごとにスレッドを一つ作成することを Akka に指示します。すべての設定可能なオプションは ここ で見ることができます。
この設定が Play Akka plugin が使用する設定と分離していることに注意してください。Play Akka プラグインは、(play {} で囲まれていない) ルートネームスペース中の akka の設定によって、別々に設定されます。
§他のスレッドプールを使う
ある状況では、他のスレッドプールに仕事を割り当てたくなることがあります。このような状況には、データベースアクセスのような CPU 負荷の高い作業や IO が含まれるかもしれません。この処理を行うために、最初にスレッドプールを作成するべきです。これは、 Scala 内で簡単に行うことができます:
object Contexts {
implicit val myExecutionContext: ExecutionContext = Akka.system.dispatchers.lookup("my-context")
}
この場合、実行コンテキストを作成するために Akka を使います。しかし Java executor を使って自分の実行コンテキストを簡単に作りたくなることがあるかもしれません。 application.conf
で以下のように設定することで追加することができます。
my-context {
fork-join-executor {
parallelism-factor = 20.0
parallelism-max = 200
}
}
Scala 内でのこの実行コンテキストを使用することで、 scala Future
コンパニオンオブジェクトの関数で簡単に使うことができます。
Future {
// Some blocking or expensive code here
}(Contexts.myExecutionContext)
または、暗黙的に使うこともできます。
import Contexts.myExecutionContext
Future {
// Some blocking or expensive code here
}
§Class loaders and thread locals
Class loaders and thread locals need special handling in a multithreaded environment such as a Play program.
§Application class loader
In a Play application the thread context class loader may not always be able to load application classes. You should explicitly use the application class loader to load classes.
- Java
-
Class myClass = Play.application().classloader().loadClass(myClassName);
- Scala
-
val myClass = Play.current.classloader.loadClass(myClassName)
Being explicit about loading classes is most important when running Play in development mode (using run
) rather than production mode. That’s because Play’s development mode uses multiple class loaders so that it can support automatic application reloading. Some of Play’s threads might be bound to a class loader that only knows about a subset of your application’s classes.
In some cases you may not be able to explicitly use the application classloader. This is sometimes the case when using third party libraries. In this case you may need to set the thread context class loader explicitly before you call the third party code. If you do, remember to restore the context class loader back to its previous value once you’ve finished calling the third party code.
§Java thread locals
Java code in Play uses a thread local to find out about contextual information such as the current HTTP request. Scala code doesn’t need to use thread locals because it can use implicit parameters to pass context instead. Threads locals are used in Java so that Java code can access contextual information without needing to pass context parameters everywhere.
Java thread locals, along with the correct context class loader, are propagated automatically by ExecutionContextExecutor
objects provided through the HttpExecution
class. (An ExecutionContextExecutor
is both a Scala ExecutionContext
and a Java Executor
.) These special ExecutionContextExecutor
objects are automatically created and used by in Java actions and Java Promise
methods. The default objects wrap the default user thread pool. If you want to do your own threading then you should use the HttpExecution
class’s helper methods to get an ExecutionContextExecutor
object yourself.
In the example below, a user thread pool is wrapped to create a new ExecutionContext
that propagates thread locals correctly.
import play.libs.HttpExecution;
import scala.concurrent.ExecutionContext;
- Java 8
-
public static Promise<Result> index2() { // Wrap an existing thread pool, using the context from the current thread ExecutionContext myEc = HttpExecution.fromThread(myThreadPool); return Promise.promise(() -> intensiveComputation(), myEc) .map((Integer i) -> ok("Got result: " + i), myEc); }
- Java
-
public static Promise<Result> index2() { // Wrap an existing thread pool, using the context from the current thread ExecutionContext myEc = HttpExecution.fromThread(myThreadPool); Promise<Integer> promiseOfInt = Promise.promise( new Function0<Integer>() { public Integer apply() { return intensiveComputation(); } }, myEc ); return promiseOfInt.map( new Function<Integer, Result>() { public Result apply(Integer i) { return ok("Got result: " + i); } }, myEc ); }
§ベストプラクティス
アプリケーションにおける作業を異なるスレッドプール間でどのように割り振るべきかは、アプリケーションが実行している作業の種類、およびどれだけの作業を平行して行えるよう制御したいのかという要望に大きく依存します。全てのソリューションに合うただひとつの設定値はありませんので、アプリケーションのブロッキング IO 要件と、それらのスレッドプール上における意味を理解することで、最良の決定を行うことができます。設定値の調整および検証にはアプリケーションの負荷テストが役立つでしょう。
JDBC がスレッドをブロックするという事実を踏まえ、スレッドプールがデータベースアクセスのためだけに使われると仮定すると、スレッドプールの大きさは利用できる db プールへのコネクションのサイズに設定することができます。これより少ない量のスレッドでは、利用可能なコネクションの数を使い切ることはないでしょう。利用可能なコネクションの数よりも多いスレッドは、コネクションの競合により無駄になる可能性があります。
以下で、 Play Flamework で使用できるいくつかの一般的なプロファイルの概要を説明します。
§ピュアな非同期化
この場合、アプリケーションではブロッキング IO を行いません。決してブロッキングを行わないので、プロセッサーごとにひとつのスレッドを割り当てるデフォルトの設定がこのユースケースにぴったりですし、追加の設定を行う必要はありません。Play のデフォルト実行コンテキストがあらゆる状況で使われます。
§高度な同期化
このプロファイルは Java servlet container のような従来の同期 IO ベースの web フレームワークにマッチします。ブロッキング IO を扱うために大きなスレッドプールを使います。ほとんどのアクションがデータベースアクセスを行う際に、同期 IO を行うようなアプリケーションで有効です。また、他のタイプの作業での平行性に対するコントロールを望まないし必要としない場合にも有効です。このプロファイルはブロッキング IO を扱う場合にもっとも簡単です。
このプロファイルでは、どこでも単純にデフォルト実行コンテキストを使いますが、以下のように、そのプールに非常に多くのスレッドを持つように設定する必要があります。
play {
akka {
akka.loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = WARNING
actor {
default-dispatcher = {
fork-join-executor {
parallelism-min = 300
parallelism-max = 300
}
}
}
}
}
このプロファイルは同期 IO を行う Java アプリケーションで推奨されます。Java では他のスレッドに作業を割り当てるのがより難しいためです。
§多くの特定のスレッドプール
このプロファイルはたくさんの同期 IO を、アプリケーションがどのタイプの作業を直ちに実行するか正確にコントロールしながら実行したい場合のためのものです。このプロファイルでは、デフォルト実行コンテキストでノンブロッキングに作業を行い、特別な作業を異なる実行コンテキストでブロッキングオペレーションに割り当てます。
この場合、以下のような異なるタイプのオペレーションに対して複数の異なる実行コンテキストを作る必要があります。
object Contexts {
implicit val simpleDbLookups: ExecutionContext = Akka.system.dispatchers.lookup("contexts.simple-db-lookups")
implicit val expensiveDbLookups: ExecutionContext = Akka.system.dispatchers.lookup("contexts.expensive-db-lookups")
implicit val dbWriteOperations: ExecutionContext = Akka.system.dispatchers.lookup("contexts.db-write-operations")
implicit val expensiveCpuOperations: ExecutionContext = Akka.system.dispatchers.lookup("contexts.expensive-cpu-operations")
}
これらは以下のように設定します。
contexts {
simple-db-lookups {
fork-join-executor {
parallelism-factor = 10.0
}
}
expensive-db-lookups {
fork-join-executor {
parallelism-max = 4
}
}
db-write-operations {
fork-join-executor {
parallelism-factor = 2.0
}
}
expensive-cpu-operations {
fork-join-executor {
parallelism-max = 2
}
}
}
この後、コードにて future を作成し、future が実行していた作業と関係のある実行コンテキストを引き渡します。
Note: The configuration namespace can be chosen freely, as long as it matches the dispatcher ID passed to
Akka.system.dispatchers.lookup
.
§わずかな特定のスレッドプール
これは、多くの特定のスレッドプールと高度に同期化されたプロファイルの組み合わせです。デフォルト実行コンテキスト中でほとんどの単純な IO を行い、(100 くらいの) 合理的な複数のスレッドを設定しますが、その後、一度に行われる数を制限することのできる特定のコンテキストに負荷の高いオペレーションを割り振ります。
Next: Configuring logging
このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。