[Todo]Understanding Play thread pools

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.confakka命名空间下。这里有一个默认的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。

你可能感兴趣的:([Todo]Understanding Play thread pools)