症状
经过一段时间的正常运行后,我们的后端将停止对其大多数端点做出响应。对于那些人来说,它只会开始表现得像个黑洞。一旦处于这种状态,如果我们不采取任何行动,它就会一直呆在那里。
更新
当后端处于无响应状态时,我们可以使用我们制作的数据库转储来重现此行为。
基础设施设置
我们在 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 中描述的异常后,后端现在再次运行良好。我们没有为此做任何事情。这是后端第一次在没有我们参与的情况下从这种状态中恢复。