Documentation

You are viewing the documentation for the 2.1.5 release in the 2.1.x series of releases. The latest stable release series is 2.4.x.

§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 ライブラリを使う注目すべき例外です。

その他のブロックするかもしれないコードは以下のようになっています。

一般的に、使用している API が future を返す場合はノンブロッキングですが、そうでなければブロッキングです。

このため、Future でブロッキングコードをラップするという誘惑に駆られるかもしれないことに注意してください。こうすることではノンブロッキングになりませんし、ブロッキングが他のスレッド内で起こるかもしれません。使用しているスレッドプールがブロッキングを扱うための十分なスレッドを持っていることを確かめる必要があります。

対照的に、以下のタイプの IO はブロックしません:

§Play のスレッドプール

Play は複数の異なる目的のためのスレッドプールを使っています。

§デフォルトスレッドプールを使用する

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 くらいの) 合理的な複数のスレッドを設定しますが、その後、一度に行われる数を制限することのできる特定のコンテキストに負荷の高いオペレーションを割り振ります。