在 Play 中,每个请求都会产生一个 Akka actor 吗?

In Play, does every request spawn an Akka actor?

我读到 Play 是基于 Akka 构建的,所以我想知道是否对于每个传入请求,都会产生一个 actor 来提供服务。

以控制器动作为例:

def upload = Action(parse.multipartFormData) { implicit request =>
  request.body.file("picture").map { picture =>
    val client = new AmazonS3Client
    client.putObject("my-bucket", picture.filename, picture.ref.file)
  }.getOrElse {
    BadRequest("File missing")
  }
}

上传是同步进行的,我经常看到示例试图在 Future 中包装这样的代码块。我认为如果这个请求是由 Akka actor 服务的,那么就不需要这样做了。

请让我知道我是对还是错,以及您对使用阻塞服务的建议。

我认为 Play 不会为每个请求生成一个新的 actor,而是使用一个 actor 池来处理它们。如果可以为数百万个请求生成数百万个 actor,而所有这些都是为了处理一个请求,那么利用 actor 的消息队列来处理请求的目的就有点背离了。在某些时候应该有一个上限。

不过,Play 是否真的这样做并不重要。默认情况下,所有 Action 都是异步处理的。您的代码与使用 Action.async 包装在 Future 中的代码之间的唯一区别在于,它将使用一种便捷的方法来处理 Future。最后,两者都是像 Request => Future[Result].

这样的函数

I think that if this request is being served by an Akka actor, doing so is not needed.

这不是真的,部分原因是上述原因。 Play 使用可配置的线程池(演员使用)来处理请求。默认情况下,它的大小为每个核心一个线程。演员共享线程以完成工作。这意味着如果你有 4 个核心/4 个线程和 100 个参与者(随机数),如果你有 4 个阻塞上传,你将有 4 个阻塞线程。不管有多少演员,你仍然会遇到线程饥饿,而其他 96 个都没用。更糟糕的是,这意味着您的服务器将无法再处理任何请求,直到其中一个上传完成并且其中一个线程不再被阻止。

如果将代码包装在 Future 中, 使用单独的 ExecutionContext 来阻止,则可以缓解这种情况。包装在 Future 中还不够,因为代码仍在阻塞。您将不得不在某处进行阻塞,但不要在 Play 用于处理请求的默认 ExecutionContext 中执行此操作。相反,您可以配置一个专门用于处理上传。


application.conf

# Configures a pool with 10 threads per core, with a maximum of 40 total
upload-context {
  fork-join-executor {
    parallelism-factor = 10.0
    parallelism-max = 40
  }
}

用法:

implicit val uploadContext: ExecutionContext = Akka.system.dispatchers.lookup("upload-context")

def upload = Action.async(parse.multipartFormData) { implicit request =>
  request.body.file("picture").map { picture =>
    val client = new AmazonS3Client
    Future(client.putObject("my-bucket", picture.filename, picture.ref.file))(uploadContext)
  }.getOrElse {
    Future.successful(BadRequest("File missing"))
  }
}

您可以阅读有关线程池的更多信息here