Play Framework 2.5 请求随机超时
Play Framework 2.5 Requests timing out randomly
症状
经过一段时间的 运行ning 就好了,我们的后端将停止为其大部分端点提供响应。对于那些人来说,它只会开始表现得像一个黑洞。一旦进入这个状态,如果我们不采取任何行动,它就会一直呆在那里。
更新
我们可以使用后端处于非响应状态时创建的数据库转储来重现此行为。
基础设施设置
我们正在 运行在 AWS 上的 EC2 实例上使用 RDS 上的 PostgreSQL 数据库在负载均衡器后面运行 Play 2.5。我们正在使用 slick-pg 作为我们的数据库连接器。
我们所知道的
到目前为止,我们已经弄清楚了一些事情。
关于 HTTP 请求
我们的日志和调试显示请求正在通过过滤器。此外,我们看到对于身份验证(我们为此使用 Silhoutte)应用程序能够执行数据库查询以接收该请求的身份。但是,永远不会调用控制器操作。
后端正在响应 HEAD
请求。进一步的日志记录向我们表明,似乎使用注入服务的控制器(我们为此使用 googles guice)是那些不再调用其方法的控制器。没有注入服务的控制器似乎工作正常。
关于 EC2 实例
不幸的是,我们无法从那个人那里获得太多信息。我们使用的是 boxfuse,它为我们提供了一个不可变的、可通过 ssh 访问的基础设施。我们即将将其更改为基于 docker 的部署,并且可能很快会提供更多信息。不过,我们有 New Relic 设置来监控我们的服务器。我们在那里找不到任何可疑的东西。内存和 CPU 用法看起来不错。
不过,无论如何,此设置都会在每次部署时为我们提供一个新的 EC2 实例。即使在重新部署之后,这个问题至少在大多数时候仍然存在。 最终可以通过重新部署来解决这个问题。
更奇怪的是,我们可以 运行 后端本地连接到 AWS 上的数据库,一切都会在那里正常工作。
所以我们很难说问题出在哪里。似乎数据库不能与任何 EC2 实例一起使用(直到它最终将与一个新实例一起使用)但与我们的本地机器一起使用。
关于数据库
数据库是此设置中唯一的有状态实体,因此我们认为问题应该与它有关。
因为我们有一个生产环境和一个暂存环境,所以当后者不再工作时,我们可以将生产数据库转储到暂存中。我们发现这确实立即解决了问题。 不幸的是,我们无法从不知何故损坏的数据库中获取快照以将其转储到暂存环境中并查看这是否会立即破坏它。当后端不再响应。当我们将其转储到暂存环境时,后端将立即停止响应。
根据AWS控制台,数据库的连接数在20左右,属于正常。
TL;DR
- 我们的后端最终开始表现得像它的某些端点的黑洞
- 请求未到达控制器操作
- EC2 中的新实例可能会解决此问题,但不一定
- 本地使用相同的数据库一切正常
- 将工作数据库转储到其中可以解决问题
- CPU 和 EC2 实例的内存使用情况以及与数据库的连接数看起来完全没问题
- 我们可以使用我们在后端不再响应时创建的数据库转储来重现该行为(请参阅更新 2)
- 使用新的 slick 线程池设置,在重新启动 db 并随后重新启动 ec2 实例后,我们将从 slick 获得 ThreadPoolExecutor 异常。 (见更新 3)
更新 1
回应:
以这个为例 ApplicationController.scala
:
package controllers
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import akka.actor.ActorRef
import com.google.inject.Inject
import com.google.inject.name.Named
import com.mohiva.play.silhouette.api.Silhouette
import play.api.i18n.{ I18nSupport, MessagesApi }
import play.api.mvc.Action
import play.api.mvc.Controller
import jobs.jobproviders.BatchJobChecker.UpdateBasedOnResourceAvailability
import utils.auth.JobProviderEnv
/**
* The basic application controller.
*
* @param messagesApi The Play messages API.
* @param webJarAssets The webjar assets implementation.
*/
class ApplicationController @Inject() (
val messagesApi: MessagesApi,
silhouette: Silhouette[JobProviderEnv],
implicit val webJarAssets: WebJarAssets,
@Named("batch-job-checker") batchJobChecker: ActorRef
)
extends Controller with I18nSupport {
def index = Action.async { implicit request =>
Future.successful(Ok)
}
def admin = Action.async { implicit request =>
Future.successful(Ok(views.html.admin.index.render))
}
def taskChecker = silhouette.SecuredAction.async {
batchJobChecker ! UpdateBasedOnResourceAvailability
Future.successful(Ok)
}
}
index
和 admin
工作正常。不过,taskchecker
会显示出奇怪的行为。
更新 2
我们现在可以重现这个问题了!我们发现我们上次后端不再响应时进行了数据库转储。当我们将其转储到暂存数据库中时,后端将立即停止响应。
我们现在开始使用 Thread.getAllStackTraces.keySet.size
在我们的一个过滤器中记录线程数,发现有 50 到 60 个线程 运行ning.
更新 3
作为@AxelFontaine ,我们为数据库启用了 MultiAZ 部署故障转移。我们通过故障转移重新启动了数据库。在重新启动之前、期间和之后,后端没有响应。
重新启动后,我们注意到与数据库的连接数保持为 0。此外,我们没有再获得任何身份验证日志(在我们这样做之前,身份验证步骤甚至可以发出数据库请求并获得响应).
重启 EC2 实例后,我们现在得到
play.api.UnexpectedException: Unexpected exception[RejectedExecutionException: Task slick.backend.DatabaseComponent$DatabaseDef$$anon@76d6ac53 rejected from java.util.concurrent.ThreadPoolExecutor@6ea1d0ce[Running, pool size = 4, active threads = 4, queued tasks = 5, completed tasks = 157]]
(我们之前没有得到)
对于我们的请求以及需要访问数据库的后台作业。我们巧妙的设置现在包括
numThreads = 4
queueSize = 5
maxConnections = 10
connectionTimeout = 5000
validationTimeout = 5000
按照建议
更新 4
在我们得到更新 3 中描述的异常后,后端现在 运行 再次正常。我们没有为此做任何事情。这是后端第一次在没有我们参与的情况下从这种状态恢复。
乍一看像是线程管理问题。如果您使用 Slick 3.1,Slick 将为数据库操作提供自己的执行上下文,但您确实希望管理队列大小,使其映射到与数据库大致相同的大小:
myapp = {
database = {
driver = org.h2.Driver
url = "jdbc:h2:./test"
user = "sa"
password = ""
// The number of threads determines how many things you can *run* in parallel
// the number of connections determines you many things you can *keep in memory* at the same time
// on the database server.
// numThreads = (core_count (hyperthreading included))
numThreads = 4
// queueSize = ((core_count * 2) + effective_spindle_count)
// on a MBP 13, this is 2 cores * 2 (hyperthreading not included) + 1 hard disk
queueSize = 5
// https://groups.google.com/forum/#!topic/scalaquery/Ob0R28o45eM
// make larger than numThreads + queueSize
maxConnections = 10
connectionTimeout = 5000
validationTimeout = 5000
}
}
此外,您可能希望使用自定义 ActionBuilder,并注入 Futures 组件并添加
import play.api.libs.concurrent.Futures._
完成后,您可以添加 future.withTimeout(500 毫秒)并超时,以便返回错误响应。 Play 示例中有一个自定义 ActionBuilder 的示例:
https://github.com/playframework/play-scala-rest-api-example/blob/2.5.x/app/v1/post/PostAction.scala
class PostAction @Inject()(messagesApi: MessagesApi)(
implicit ec: ExecutionContext)
extends ActionBuilder[PostRequest]
with HttpVerbs {
type PostRequestBlock[A] = PostRequest[A] => Future[Result]
private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass)
override def invokeBlock[A](request: Request[A],
block: PostRequestBlock[A]): Future[Result] = {
if (logger.isTraceEnabled()) {
logger.trace(s"invokeBlock: request = $request")
}
val messages = messagesApi.preferred(request)
val future = block(new PostRequest(request, messages))
future.map { result =>
request.method match {
case GET | HEAD =>
result.withHeaders("Cache-Control" -> s"max-age: 100")
case other =>
result
}
}
}
}
因此您可以在此处添加超时、指标(或断路器,如果数据库已关闭)。
经过更多调查后,我们发现我们的一项作业在我们的数据库中产生了死锁。我们 运行 遇到的问题是我们使用的 slick 版本中的一个已知错误,据报道 on github。
所以问题是我们在 DBIOAction
的 .map
中同时在太多线程上进行 运行 与 .transactionally
的数据库事务。
症状
经过一段时间的 运行ning 就好了,我们的后端将停止为其大部分端点提供响应。对于那些人来说,它只会开始表现得像一个黑洞。一旦进入这个状态,如果我们不采取任何行动,它就会一直呆在那里。
更新
我们可以使用后端处于非响应状态时创建的数据库转储来重现此行为。
基础设施设置
我们正在 运行在 AWS 上的 EC2 实例上使用 RDS 上的 PostgreSQL 数据库在负载均衡器后面运行 Play 2.5。我们正在使用 slick-pg 作为我们的数据库连接器。
我们所知道的
到目前为止,我们已经弄清楚了一些事情。
关于 HTTP 请求
我们的日志和调试显示请求正在通过过滤器。此外,我们看到对于身份验证(我们为此使用 Silhoutte)应用程序能够执行数据库查询以接收该请求的身份。但是,永远不会调用控制器操作。
后端正在响应 HEAD
请求。进一步的日志记录向我们表明,似乎使用注入服务的控制器(我们为此使用 googles guice)是那些不再调用其方法的控制器。没有注入服务的控制器似乎工作正常。
关于 EC2 实例
不幸的是,我们无法从那个人那里获得太多信息。我们使用的是 boxfuse,它为我们提供了一个不可变的、可通过 ssh 访问的基础设施。我们即将将其更改为基于 docker 的部署,并且可能很快会提供更多信息。不过,我们有 New Relic 设置来监控我们的服务器。我们在那里找不到任何可疑的东西。内存和 CPU 用法看起来不错。
不过,无论如何,此设置都会在每次部署时为我们提供一个新的 EC2 实例。即使在重新部署之后,这个问题至少在大多数时候仍然存在。 最终可以通过重新部署来解决这个问题。
更奇怪的是,我们可以 运行 后端本地连接到 AWS 上的数据库,一切都会在那里正常工作。
所以我们很难说问题出在哪里。似乎数据库不能与任何 EC2 实例一起使用(直到它最终将与一个新实例一起使用)但与我们的本地机器一起使用。
关于数据库
数据库是此设置中唯一的有状态实体,因此我们认为问题应该与它有关。
因为我们有一个生产环境和一个暂存环境,所以当后者不再工作时,我们可以将生产数据库转储到暂存中。我们发现这确实立即解决了问题。 不幸的是,我们无法从不知何故损坏的数据库中获取快照以将其转储到暂存环境中并查看这是否会立即破坏它。当后端不再响应。当我们将其转储到暂存环境时,后端将立即停止响应。
根据AWS控制台,数据库的连接数在20左右,属于正常。
TL;DR
- 我们的后端最终开始表现得像它的某些端点的黑洞
- 请求未到达控制器操作
- EC2 中的新实例可能会解决此问题,但不一定
- 本地使用相同的数据库一切正常
- 将工作数据库转储到其中可以解决问题
- CPU 和 EC2 实例的内存使用情况以及与数据库的连接数看起来完全没问题
- 我们可以使用我们在后端不再响应时创建的数据库转储来重现该行为(请参阅更新 2)
- 使用新的 slick 线程池设置,在重新启动 db 并随后重新启动 ec2 实例后,我们将从 slick 获得 ThreadPoolExecutor 异常。 (见更新 3)
更新 1
回应
以这个为例 ApplicationController.scala
:
package controllers
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import akka.actor.ActorRef
import com.google.inject.Inject
import com.google.inject.name.Named
import com.mohiva.play.silhouette.api.Silhouette
import play.api.i18n.{ I18nSupport, MessagesApi }
import play.api.mvc.Action
import play.api.mvc.Controller
import jobs.jobproviders.BatchJobChecker.UpdateBasedOnResourceAvailability
import utils.auth.JobProviderEnv
/**
* The basic application controller.
*
* @param messagesApi The Play messages API.
* @param webJarAssets The webjar assets implementation.
*/
class ApplicationController @Inject() (
val messagesApi: MessagesApi,
silhouette: Silhouette[JobProviderEnv],
implicit val webJarAssets: WebJarAssets,
@Named("batch-job-checker") batchJobChecker: ActorRef
)
extends Controller with I18nSupport {
def index = Action.async { implicit request =>
Future.successful(Ok)
}
def admin = Action.async { implicit request =>
Future.successful(Ok(views.html.admin.index.render))
}
def taskChecker = silhouette.SecuredAction.async {
batchJobChecker ! UpdateBasedOnResourceAvailability
Future.successful(Ok)
}
}
index
和 admin
工作正常。不过,taskchecker
会显示出奇怪的行为。
更新 2
我们现在可以重现这个问题了!我们发现我们上次后端不再响应时进行了数据库转储。当我们将其转储到暂存数据库中时,后端将立即停止响应。
我们现在开始使用 Thread.getAllStackTraces.keySet.size
在我们的一个过滤器中记录线程数,发现有 50 到 60 个线程 运行ning.
更新 3
作为@AxelFontaine
重新启动后,我们注意到与数据库的连接数保持为 0。此外,我们没有再获得任何身份验证日志(在我们这样做之前,身份验证步骤甚至可以发出数据库请求并获得响应).
重启 EC2 实例后,我们现在得到
play.api.UnexpectedException: Unexpected exception[RejectedExecutionException: Task slick.backend.DatabaseComponent$DatabaseDef$$anon@76d6ac53 rejected from java.util.concurrent.ThreadPoolExecutor@6ea1d0ce[Running, pool size = 4, active threads = 4, queued tasks = 5, completed tasks = 157]]
(我们之前没有得到)
对于我们的请求以及需要访问数据库的后台作业。我们巧妙的设置现在包括
numThreads = 4
queueSize = 5
maxConnections = 10
connectionTimeout = 5000
validationTimeout = 5000
按照建议
更新 4
在我们得到更新 3 中描述的异常后,后端现在 运行 再次正常。我们没有为此做任何事情。这是后端第一次在没有我们参与的情况下从这种状态恢复。
乍一看像是线程管理问题。如果您使用 Slick 3.1,Slick 将为数据库操作提供自己的执行上下文,但您确实希望管理队列大小,使其映射到与数据库大致相同的大小:
myapp = {
database = {
driver = org.h2.Driver
url = "jdbc:h2:./test"
user = "sa"
password = ""
// The number of threads determines how many things you can *run* in parallel
// the number of connections determines you many things you can *keep in memory* at the same time
// on the database server.
// numThreads = (core_count (hyperthreading included))
numThreads = 4
// queueSize = ((core_count * 2) + effective_spindle_count)
// on a MBP 13, this is 2 cores * 2 (hyperthreading not included) + 1 hard disk
queueSize = 5
// https://groups.google.com/forum/#!topic/scalaquery/Ob0R28o45eM
// make larger than numThreads + queueSize
maxConnections = 10
connectionTimeout = 5000
validationTimeout = 5000
}
}
此外,您可能希望使用自定义 ActionBuilder,并注入 Futures 组件并添加
import play.api.libs.concurrent.Futures._
完成后,您可以添加 future.withTimeout(500 毫秒)并超时,以便返回错误响应。 Play 示例中有一个自定义 ActionBuilder 的示例:
https://github.com/playframework/play-scala-rest-api-example/blob/2.5.x/app/v1/post/PostAction.scala
class PostAction @Inject()(messagesApi: MessagesApi)(
implicit ec: ExecutionContext)
extends ActionBuilder[PostRequest]
with HttpVerbs {
type PostRequestBlock[A] = PostRequest[A] => Future[Result]
private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass)
override def invokeBlock[A](request: Request[A],
block: PostRequestBlock[A]): Future[Result] = {
if (logger.isTraceEnabled()) {
logger.trace(s"invokeBlock: request = $request")
}
val messages = messagesApi.preferred(request)
val future = block(new PostRequest(request, messages))
future.map { result =>
request.method match {
case GET | HEAD =>
result.withHeaders("Cache-Control" -> s"max-age: 100")
case other =>
result
}
}
}
}
因此您可以在此处添加超时、指标(或断路器,如果数据库已关闭)。
经过更多调查后,我们发现我们的一项作业在我们的数据库中产生了死锁。我们 运行 遇到的问题是我们使用的 slick 版本中的一个已知错误,据报道 on github。
所以问题是我们在 DBIOAction
的 .map
中同时在太多线程上进行 运行 与 .transactionally
的数据库事务。