Understanding Play thread pools
Play框架是一个自下而上的异步web框架。使用iteratees异步处理streams。Play使用比传统web框架线程更少的线程池,play-core的IO从来不会阻塞。
因此,如果你计划去写一个阻塞的IO代码,或是可能会做一个CPU密集型的工作,你需要确切的知道哪一个线程承受着很大的工作量,然后你需要做一些相应的调整。使用阻塞IO没有考虑到这可能会导致Play框架的性能非常低,比如,你可能会看到每秒只有少数的请求会被处理,而CPU的使用率只有5%。相比之下, 典型开发硬件 (如 macbook pro) 的基准显示, 在正确调整后, 每秒钟可以处理数以百计甚至上千个请求的工作负载。
Knowing when you are blocking
最常见的地方就是Play应用在与数据库的交流中会阻塞。不幸的是,没有任何主流的数据为JVM提供异步的数据库驱动。所以对于大多数的数据库,你只能选择使用阻塞的IO。值得注意的例外是ReactiveMongo
,这是一个MongoDB的驱动使用Play的Iteratee库与MongoDB交流。
其他可能会阻塞你代码的场景包括:
- 使用REST/WebService API通过第三方的客户端,即没有使用Play的asynchronous WS API。
- 一些仅仅提供同步API的消息传递技术去发送一些消息。
- 当你直接打开一些files或是sockets。
- 一些需要很长时间去执行的CPU密集型操作。
通常来说如果你使用的API返回的是Futures
,他就不会阻塞,而且它的就会阻塞。
注意:你看到这里可能会因此将你的阻塞代码包裹在
Futures
中。这不会使它就成为非阻塞的,这仅仅只会让这个阻塞发生在其他的线程中。你任然需要保证线程池有足够的线程数去处理阻塞。
相比之下,下面的IO类型是不会阻塞的:
- The Play WS API
- Asynchronous database drivers such as ReactiveMongo
- Sending/receiving messages to/from Akka actors
Play`s thread pools
Play使用了一些数量的不同线程用于不同的目的:
- Netty boss/worker thread pools - 通过使用Netty的方式处理Netty IO。应用自身的代码不会由这个线程池的线程处理。
- Play default thread pool - 这个是你在Play框架中所有的应用代码执行的线程池。它是一个Akka的dispatcher,在应用中通过
ActorSystem
使用它。通过配置Akka去配置它,下面的内容会讨论它。
注意:在Play 2.4之后这些线程池都被被合并到了Play default thread pool.
Using the default thread pool
Play框架中的所有actions都使用这个default thread pool
.当做了某些异步操作,比如,调用map
或是flatMap
在future,你可能需要去提供一个明确的execution context用于执行这个function。一个execution context基本上是另一个名字ThreadPool
.
在大多数情况下,适当的使用execution context是通过Play default thread pool,可以通过play.api.libs.concurrent.Execution.Implicits._
,他可以通过importing到你的Scala source file:
import play.api.libs.concurrent.Execution.Implicits._
def someAsyncAction = Action.async {
wsClient.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)
}
}
Play的线程池直接连接到Application`s ActorSystem
并且使用默认的dispatcher。
Configuring the default thread pool
这个默认的线程池可以被配置,使用标准的Akka配置在application.conf
的akka
命名空间下。这里有一个默认的Play的thread pool配置:
akka {
actor {
default-dispatcher {
fork-join-executor {
# Settings this to 1 instead of 3 seems to improve performance.
parallelism-factor = 1.0
# @richdougherty: Not sure why this is set below the Akka
# default.
parallelism-max = 24
# Setting this to LIFO changes the fork-join-executor
# to use a stack discipline for task scheduling. This usually
# improves throughput at the cost of possibly increasing
# latency and risking task starvation (which should be rare).
task-peeking-mode = LIFO
}
}
}
}
这个配置指明Akka创建1个线程/每个可用的处理器,线程池中的最大线程数为24.
当然你也可以尝试Akka的默认配置:
akka {
actor {
default-dispatcher {
# This will be used if you have set "executor = "fork-join-executor""
fork-join-executor {
# Min number of threads to cap factor-based parallelism number to
parallelism-min = 8
# The parallelism factor is used to determine thread pool size using the
# following formula: ceil(available processors * factor). Resulting size
# is then bounded by the parallelism-min and parallelism-max values.
parallelism-factor = 3.0
# Max number of threads to cap factor-based parallelism number to
parallelism-max = 64
# Setting to "FIFO" to use queue like peeking mode which "poll" or "LIFO" to use stack
# like peeking mode which "pop".
task-peeking-mode = "FIFO"
}
}
}
}
完整的配置选项你可以在这里看到here
Using other thread pools
在某些环境下,你可能希望调度work到其他的线程。比如像CPU密集运算,IO,或是数据库访问。你必须首先创建一个ThreadPool
,在Scala中很简单:
val myExecutionContext: ExecutionContext = akkaSystem.dispatchers.lookup("my-context")
我们使用Akka去创建ExecutionContext
,或者你可能想使用java的executors创建更容易的创建你自己的ExecutionContext
,又或是Scala的fork join thread pool。去配置Akka的execution context,你可以添加下面的配置到你的application.conf
:
my-context {
fork-join-executor {
parallelism-factor = 20.0
parallelism-max = 200
}
}
在Scala中使用execution context.你可能想要更简单的使用Future对象函数:
Future {
// Some blocking or expensive code here
}(myExecutionContext)
或者你可以明确的使用它:
implicit val ec = myExecutionContext
Future {
// Some blocking or expensive code here
}
Class loaders and thread locals
Class loaders和thread locals需要特殊的处理在多线程环境。
Application class loader
Play应用的thread context class loader可能并不能总是能够加载application classes。你可以明确的使用application class loader 去加载 classes.
Java
Class myClass = app.classloader().loadClass(myClassName);
Scala
val myClass = app.classloader.loadClass(myClassName)
显示的加载classes在运行Play为开发模式下比production模式要重要得多。这是因为Play的开发模式使用多个class loaders,以支持引用的自动重载。Some of Play’s threads might be bound to a class loader that only knows about a subset of your application’s classes.
在一些情况下你可能不能明确的使用application classloader。