Play Framework 2.5请求随机超时

时间:2017-09-04 12:22:41

标签: postgresql amazon-web-services playframework slick boxfuse

症状

经过一段时间的运行后,我们的后端将停止为大多数端点提供响应。它会开始表现得像黑洞那样。一旦进入这种状态,如果我们不采取任何行动,它将留在那里。

更新

我们可以使用我们在后端处于非响应状态时进行的数据库转储来重现此行为。

基础架构设置

我们在AWS上运行Play 2.5,在具有RDS上的PostgreSQL数据库的负载均衡器后面的EC2实例上运行。我们使用slick-pg作为数据库连接器。

我们所知道的

到目前为止我们已经找到了一些事情。

关于HTTP请求

我们的日志和调试向我们显示请求正在通过过滤器。此外,我们看到对于身份验证(我们使用Silhoutte),应用程序能够执行数据库查询以接收该请求的标识。但是,控制器动作永远不会被调用。

后端正在响应HEAD次请求。进一步的日志记录向我们表明,似乎控制器使用注入服务(我们使用googles guice),其方法不再被调用。没有注入服务的控制器似乎工作正常。

关于EC2实例

不幸的是,我们无法从那个获得太多信息。我们正在使用boxfuse,它为我们提供了一个不可变的基础设施。我们即将将其更改为基于docker的部署,并且可能很快就能提供更多信息。不过,我们有New Relic设置来监控我们的服务器。我们在那里找不到任何可疑的东西。内存和CPU使用情况看起来很好。

但是,无论如何,这个设置为我们提供了每个部署的新EC2实例。即使在重新部署之后,该问题至少在大多数时间内仍然存在。 最终可以通过重新部署解决此问题。

更奇怪的是,我们可以在AWS上运行本地连接到数据库的后端,一切都可以在那里正常工作。

所以我们很难说问题出在哪里。似乎数据库不能与任何EC2实例一起使用(最终它将与新的实例一起使用),但是使用我们的本地机器。

关于数据库

db是此设置中唯一的有状态实体,因此我们认为该问题应与其相关。

由于我们有生产和暂存环境,因此当后者不再工作时,我们可以将生产数据库转储到暂存中。我们发现这确实立即解决了这个问题。 不幸的是,我们无法从某个损坏的数据库中获取快照以将其转储到暂存环境中,看看是否会立即将其破坏。当后端不是时,我们有一个db的快照回应了。当我们将其转储到我们的暂存环境时,后端将立即停止响应。

根据AWS控制台,数据库的连接数大约为20,这是正常的。

TL; DR

  • 我们的后端最终开始表现为某个端点的黑洞
  • 请求未达到控制器操作
  • EC2中的新实例可能会解决此问题,但不一定
  • 本地使用相同的数据库一切正常
  • 将一个工作数据库转储到它中解决了问题
  • EC2实例的CPU和内存使用情况以及与DB的连接数看起来完全正常
  • 我们可以使用我们在后端没有响应时进行的数据库转储重现行为(请参阅更新2)
  • 使用新的灵活的线程池设置,我们将在重新启动数据库后重新启动我的ec2实例,从而获得ThreadPoolExecutor异常。 (见更新3)

更新1

回应marcospereira

以此为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)
  }

}

indexadmin工作正常。但taskchecker将显示奇怪的行为。

更新2

我们现在能够重现这个问题!我们发现上次我们的后端没有响应时我们进行了数据库转储。当我们将其转储到我们的临时数据库时,后端将立即停止响应。

我们开始使用Thread.getAllStackTraces.keySet.size在我们的一个过滤器中记录线程数,发现有50到60个线程正在运行。

更新3

作为@AxelFontaine suggested,我们为数据库启用了MultiAZ部署故障转移。我们通过故障转移重新启动了数据库。在重新启动之前,期间和之后,后端没有响应。

重新启动后,我们注意到与db的连接数保持为0.此外,我们不再获得任何身份验证日志(在我们这样做之前,身份验证步骤甚至可以生成db请求并获得响应)。

重新启动EC2实例后,我们现在正在

play.api.UnexpectedException: Unexpected exception[RejectedExecutionException: Task slick.backend.DatabaseComponent$DatabaseDef$$anon$2@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

建议here

更新4

在我们得到更新3中描述的异常后,后端现在再次正常运行。我们没有为此做任何事情。这是我们第一次在没有我们参与的情况下从这个州恢复过来。

2 个答案:

答案 0 :(得分: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
      }
    }
  }
}

这样你就可以在这里添加超时,指标(如果数据库关闭,还有断路器)。

答案 1 :(得分:2)

经过一些调查后,我们发现我们的一个工作是在我们的数据库中产生死锁。我们遇到的问题是我们使用的光滑版本中的一个已知错误,并报告on github

所以问题是我们同时在太多线程的.transactionally .map内运行了DBIOAction的数据库事务。