4

症状

经过一段时间的正常运行后,我们的后端将停止对其大多数端点做出响应。对于那些人来说,它只会开始表现得像个黑洞。一旦处于这种状态,如果我们不采取任何行动,它就会一直呆在那里。

更新

当后端处于无响应状态时,我们可以使用我们制作的数据库转储来重现此行为。

基础设施设置

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

我们所知道的

到目前为止,我们发现了一些事情。

关于 HTTP 请求

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

后端正在响应HEAD请求。进一步的日志记录显示,似乎使用注入服务的控制器(我们为此使用谷歌的 guice)是那些不再调用方法的控制器。没有注入服务的控制器似乎工作正常。

关于 EC2 实例

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

尽管如此,这个设置还是在每次部署时为我们提供了一个新的 EC2 实例。即使在重新部署之后,问题至少在大多数情况下仍然存在。最终可以通过重新部署来解决这个问题。

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

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

关于数据库

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

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

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

TL;博士

  • 对于某些端点,我们的后端最终开始表现得像一个黑洞
  • 请求未达到控制器操作
  • EC2 中的新实例可能会解决此问题,但不一定
  • 在本地使用相同的数据库,一切正常
  • 将工作数据库转储到其中可以解决问题
  • EC2 实例的 CPU 和内存使用率以及与数据库的连接数看起来完全正常
  • 当后端不再响应时,我们可以使用我们制作的数据库转储来重现该行为(请参阅更新 2)
  • 使用新的 slick 线程池设置,我们将在重新启动数据库后从 slick 获取 ThreadPoolExecutor 异常,然后重新启动我们的 ec2 实例。(见更新 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)
  }

}

index工作admin正常。不过,这taskchecker将显示出奇怪的行为。

更新 2

我们现在可以重现此问题!我们发现上次后端不再响应时我们进行了数据库转储。当我们将其转储到暂存数据库中时,后端将立即停止响应。

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

更新 3

正如@AxelFontaine建议的那样,我们为数据库启用了多可用区部署故障转移。我们通过故障转移重新启动了数据库。在重新启动之前、期间和之后,后端没有响应。

重新启动后,我们注意到与 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

按照这里的建议

更新 4

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

4

2 回答 2

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
      }
    }
  }
}

因此,您可以在此处添加超时、指标(如果数据库已关闭,则为断路器)。

于 2017-09-04T14:43:20.213 回答
2

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

所以问题是我们同时在太多线程上运行.transactionally数据库事务.mapDBIOAction

于 2017-09-11T09:35:42.000 回答