§Play のスレッドプールを理解する
Play framework は、下から上まで、非同期な web フレームワークです。ストリームは Iteratee を使って非同期に扱われます。play-core 内の IO はブロックされないので、伝統的な web フレームワークと比較して、スレッドプールは低目に調整されています。
このため、ブロッキング 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 のボス/ワーカー スレッドプール - Netty IO を扱うために Netty 内で使われています。アプリケーションのコードはこれらのスレッドプール内でスレッドによって実行されることはありません。
- Iteratee スレッドプール - Iteratee ライブラリ内で使われます。サイズは
application.conf
のiteratee-threadpool-size
で設定します。初期値は利用可能なプロセッサー数になります。 - Play 内部スレッドプール - Play 内部で使われます。アプリケーションコードは、このスレッドプール中のスレッドによって実行されるべきではありませんし、このスレッドプールの中でブロッキングは行われるべきではありません。サイズは
application.conf
のinternal-threadpool-size
で設定します。初期値は利用可能なプロセッサー数になります。 - Play デフォルトスレッドプール - Play Framework で実行される、iteratee を除くすべてのアプリケーションコードでのデフォルトスレッドプールです。 Akka ディスパッチャーであり、 Akka の設定で設定することができます。後述します。
- Akka スレッドプール - Play Akka プラグインで使われており、 Akka を設定するのと同じように設定できます。
§デフォルトスレッドプールを使用する
Play Framework での全アクションはデフォルトスレッドプールを使用します。例えば、ある非同期動作を行う場合、future での map
あるいは flatMap
を呼ぶ場合に、与えられた機能内での実行するために暗黙の実行コンテキストを提供する必要があるかもしれません。実行コンテキストは基本的にスレッドプール用の別名です。
Java promise API を使うときに、 Play のデフォルト実行コンテキストはいつも使われます。ほとんどのオペレーションについては、どの実行コンテキストが使用されるかは選択できません。
ほとんどの状況で、使用する適切な実行コンテキストは Play デフォルトスレッドプールになるでしょう。
Scala ソースファイルへ import することによって使用することができます:
import play.api.libs.concurrent.Execution.Implicits._
def someAsyncAction = Action {
Async {
WS.get("http://www.example.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.
Ok("The response code was " + response.status)
}
}
}
§デフォルトスレッドプールを設定する
デフォルトスレッドプールは application.conf
内の play
名前空間で標準 Akka 設定を使用して設定することができます。デフォルト設定は以下の通りです:
play {
akka {
event-handlers = ["akka.event.Logging$DefaultLogger", "akka.event.slf4j.Slf4jEventHandler"]
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
}
§ベストプラクティス
アプリケーションにおける作業を異なるスレッドプール間でどのように割り振るべきかは、アプリケーションが実行している作業の種類、およびどれだけの作業を平行して行えるよう制御したいのかという要望に大きく依存します。全てのソリューションに合うただひとつの設定値はありませんので、アプリケーションのブロッキング IO 要件と、それらのスレッドプール上における意味を理解することで、最良の決定を行うことができます。設定値の調整および検証にはアプリケーションの負荷テストが役立つでしょう。
以下で、 Play Flamework で使用できるいくつかの一般的なプロファイルの概要を説明します。
§ピュアな非同期化
この場合、アプリケーションではブロッキング IO を行いません。決してブロッキングを行わないので、プロセッサーごとにひとつのスレッドを割り当てるデフォルトの設定がこのユースケースにぴったりですし、追加の設定を行う必要はありません。Play のデフォルト実行コンテキストがあらゆる状況で使われます。
§高度な同期化
このプロファイルは Java servlet container のような従来の同期 IO ベースの web フレームワークにマッチします。ブロッキング IO を扱うために大きなスレッドプールを使います。ほとんどのアクションがデータベースアクセスを行う際に、同期 IO を行うようなアプリケーションで有効です。また、他のタイプの作業での平行性に対するコントロールを望まないし必要としない場合にも有効です。このプロファイルはブロッキング IO を扱う場合にもっとも簡単です。
このプロファイルでは、どこでも単純にデフォルト実行コンテキストを使いますが、以下のように、そのプールに非常に多くのスレッドを持つように設定する必要があります。
play {
akka {
event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
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 が実行していた作業と関係のある実行コンテキストを引き渡します。
§わずかな特定のスレッドプール
これは、多くの特定のスレッドプールと高度に同期化されたプロファイルの組み合わせです。デフォルト実行コンテキスト中でほとんどの単純な IO を行い、(100 くらいの) 合理的な複数のスレッドを設定しますが、その後、一度に行われる数を制限することのできる特定のコンテキストに負荷の高いオペレーションを割り振ります。