在基于 Akka 的应用程序上处理 AskTimeoutException
Handleing AskTimeoutException on Akka based application
我有以下基于 HTTP 的应用程序,它将每个请求路由到一个 Akka Actor,它使用一长串 Akka Actor 来处理请求。
path("process-request") {
post {
val startedAtAsNano = System.nanoTime()
NonFunctionalMetrics.requestsCounter.inc()
NonFunctionalMetrics.requestsGauge.inc()
entity(as[Request]) { request =>
onComplete(distributor ? [Response](replyTo => Request(request, replyTo))) {
case Success(response) =>
NonFunctionalMetrics.requestsGauge.dec()
NonFunctionalMetrics.responseHistogram.labels(HttpResponseStatus.OK.getCode.toString).observeAsMicroseconds(startedAtAsNano, System.nanoTime())
complete(response)
case Failure(ex) =>
NonFunctionalMetrics.requestsGauge.dec()
NonFunctionalMetrics.responseHistogram.labels(HttpResponseStatus.INTERNAL_SERVER_ERROR.getCode.toString).observeAsMicroseconds(startedAtAsNano, System.nanoTime())
logger.warn(s"A general error occurred for request: $request, ex: ${ex.getMessage}")
complete(InternalServerError, s"A general error occurred: ${ex.getMessage}")
}
}
}
}
如您所见,我正在向 distributor
发送 ask
回复请求。
问题是在非常高的 RPS 下,有时 distributor
会失败并出现以下异常:
2022-04-16 00:36:26.498 WARN c.d.p.b.http.AkkaHttpServer - A general error occurred for request: Request(None,0,None,Some(EntitiesDataRequest(10606082,0,-1,818052,false))) with ex: Ask timed out on [Actor[akka://MyApp/user/response-aggregator-pool#1374579366]] after [5000 ms]. Message of type [com.dv.phoenix.common.pool.WorkerPool$Request]. A typical reason for `AskTimeoutException` is that the recipient actor didn't send a reply.
这是一个典型的非信息异常,正常处理时间约为 700
微,5
秒,它必须卡在管道的某个地方,因为它不能那么长。
我想监控这个,我考虑添加 Kamon
集成,它提供 Akka Actors
模块与邮箱等
我尝试添加以下配置,但它对我不起作用:
https://kamon.io/docs/latest/instrumentation/akka/ask-pattern-timeout-warning/(没有显示任何效果)
是否有其他建议来了解高 RPS 系统上此问题的原因?
谢谢!
Kamon 工具可用于查找您如何达到要求。如果您有很多地方询问可能会超时,它会很有用,但否则它不太可能告诉您问题所在。
这是因为询问超时几乎总是其他问题的征兆(唯一的例外是如果许多询问可能在流中完成(例如在 mapAsync
或 ask
阶段)但不是;这不适用于此代码)。假设超时不是由(例如)数据库关闭引起的,所以您没有收到回复或集群失败(这两者都相当明显,因此我的假设),超时的原因(任何超时,通常) 通常在队列中有太多元素(“饱和”)。
但是哪个队列?我们将从 distributor
开始,它是一个 actor 处理来自其邮箱(队列)的消息 one-at-a-time。当你说正常的处理时间是 700 微秒时,这是衡量分发者处理一个请求所花费的时间(即它可以处理下一个请求之前的时间)吗?如果是这样,并且 distributor
占用了 700 微秒,但每 600 微秒就有一次请求,这可能会发生:
- 时间 0:请求 0 进来,处理开始于
distributor
(邮箱深度 0)
- 600 微:请求 1 进来,在
distributor
的邮箱中排队(邮箱深度 1)
- 700 微:请求 0 完成(700 微延迟),请求 1 的处理开始(邮箱深度 0)
- 1200 微:请求 2 进入,排队(邮箱深度 1)
- 1400 微:请求 1 完成(800 微延迟),请求 2 的处理开始(邮箱深度 0)
- 1800 微:请求 3 进入,排队(邮箱深度 1)
- 2100 微:请求 2 完成(900 微延迟),请求 3 的处理开始(邮箱深度 0)
- 2400 微:请求 4 进入,排队(邮箱深度 1)
- 2800 微:请求 3 完成(1000 微延迟),请求 4 的处理开始(邮箱深度 0)
- 3000 微:请求 5 进入,排队(邮箱深度 1)
- 3500 微:请求 4 完成(1100 微延迟),请求 5 的处理开始(邮箱深度 0)
- 3600 微:请求 6 进入,排队(邮箱深度 1)
- 4200 微:请求 7 进入,排队,请求 5 完成(1200 微延迟),请求 6 的处理开始(邮箱深度 1)
- 4800 微:请求 8 进入,排队(邮箱深度 2)
- 4900 微:请求 6 完成(1300 微延迟),请求 7 的处理开始(邮箱深度 1)
- 5400 微:请求 9 进入,排队(邮箱深度 2)
等等:延迟和深度无限增加。最终,深度使得请求在邮箱中花费 5 秒(甚至更多)。
Kamon 能够跟踪演员邮箱中的消息数量(建议仅对特定演员执行此操作)。在这种情况下跟踪 distributor
的邮箱深度将显示它无限增长以确认这种情况正在发生。
如果distributor
的邮箱是太深的队列,首先考虑请求N如何影响请求N+1。Actor的one-at-a-time处理模型只是严格要求的当对请求的响应可能受到紧接在它之前的请求的影响时。如果一个请求只涉及系统整体状态的某个部分,那么该请求可以与不涉及该部分任何部分的请求并行处理。如果整个状态有不同的部分,以至于没有请求涉及 2 个或更多部分,那么可以将状态的每个部分的责任卸载给特定的参与者,并且分发者只查看每个请求足够长的时间来确定哪个将请求转发给的参与者(请注意,这通常不需要分发者提出请求:它传递请求并由它传递给(或该参与者的指定人员...)的参与者负责回复)。这基本上就是 Cluster Sharding 在幕后所做的,同样值得注意的是,这样做可能会增加低负载下的延迟(因为你正在做更多的工作),但会增加峰值吞吐量,最多可达状态部分的数量。
如果这不是解决分发者邮箱饱和的可行方法(即没有划分状态的好方法),那么您至少可以通过包含“respond-by" 请求消息中的字段(例如,对于 5 秒的询问超时,您可能需要在构造询问后 4900 毫秒内做出响应)。当分发器开始处理一条消息并且 respond-by 时间过去后,它会转到下一个请求:这样做有效地意味着当邮箱开始饱和时,消息处理率会增加。
当然是pos可能您的分发者的邮箱不是正在饱和的队列,或者即使是,也不是因为参与者花费了太多时间来处理消息。分发者(或响应所需的其他参与者)可能没有处理消息。
Actors 运行 inside a dispatcher 能够拥有一定数量的 actor(或 Future
回调或其他任务,每个都可以被视为等同于生成的 actor用于处理单个消息)在给定时间处理消息。如果在各自邮箱中有消息的 actor 数量多于可以处理消息的数量,则这些 actor 将在队列中进行调度(请注意,即使您碰巧有一个调度程序会产生同样多的调度程序,这也适用线程,因为它需要处理一条消息:由于 CPU 内核的数量有限,OS 内核调度程序的队列将充当调度程序队列的角色)。 Kamon 可以跟踪这个队列的深度。根据我的经验,检测线程饥饿(基本上是任务提交和任务开始执行之间的时间是否超过某个阈值)是否正在发生更有价值。 Lightbend 与 Akka 一起使用的商业工具包(免责声明:我受雇于 Lightbend)提供了用于检测是否正在发生饥饿并提供其他诊断信息的最小开销的工具。
如果观察到线程饥饿,并且排除了垃圾收集暂停或 CPU 节流(例如由于容器中的 运行ning)之类的事情,则线程饥饿的主要原因是参与者(或 actor-like 事物)处理一条消息的时间太长,要么是因为他们正在执行阻塞 I/O,要么是在处理单个消息时做的太多。如果阻塞 I/O 是罪魁祸首,请尝试将 I/O 移动到线程池中的 actor 或 futures 运行ning,线程池的线程数远远超过 CPU 内核的数量(一些为此甚至提倡使用无界线程池)。如果是在处理单个消息时进行过多计算的情况,请在处理过程中寻找有意义的位置,以捕获消息中剩余计算所需的状态并将该消息发送给自己(这基本上是等效的到协程屈服)。
我有以下基于 HTTP 的应用程序,它将每个请求路由到一个 Akka Actor,它使用一长串 Akka Actor 来处理请求。
path("process-request") {
post {
val startedAtAsNano = System.nanoTime()
NonFunctionalMetrics.requestsCounter.inc()
NonFunctionalMetrics.requestsGauge.inc()
entity(as[Request]) { request =>
onComplete(distributor ? [Response](replyTo => Request(request, replyTo))) {
case Success(response) =>
NonFunctionalMetrics.requestsGauge.dec()
NonFunctionalMetrics.responseHistogram.labels(HttpResponseStatus.OK.getCode.toString).observeAsMicroseconds(startedAtAsNano, System.nanoTime())
complete(response)
case Failure(ex) =>
NonFunctionalMetrics.requestsGauge.dec()
NonFunctionalMetrics.responseHistogram.labels(HttpResponseStatus.INTERNAL_SERVER_ERROR.getCode.toString).observeAsMicroseconds(startedAtAsNano, System.nanoTime())
logger.warn(s"A general error occurred for request: $request, ex: ${ex.getMessage}")
complete(InternalServerError, s"A general error occurred: ${ex.getMessage}")
}
}
}
}
如您所见,我正在向 distributor
发送 ask
回复请求。
问题是在非常高的 RPS 下,有时 distributor
会失败并出现以下异常:
2022-04-16 00:36:26.498 WARN c.d.p.b.http.AkkaHttpServer - A general error occurred for request: Request(None,0,None,Some(EntitiesDataRequest(10606082,0,-1,818052,false))) with ex: Ask timed out on [Actor[akka://MyApp/user/response-aggregator-pool#1374579366]] after [5000 ms]. Message of type [com.dv.phoenix.common.pool.WorkerPool$Request]. A typical reason for `AskTimeoutException` is that the recipient actor didn't send a reply.
这是一个典型的非信息异常,正常处理时间约为 700
微,5
秒,它必须卡在管道的某个地方,因为它不能那么长。
我想监控这个,我考虑添加 Kamon
集成,它提供 Akka Actors
模块与邮箱等
我尝试添加以下配置,但它对我不起作用: https://kamon.io/docs/latest/instrumentation/akka/ask-pattern-timeout-warning/(没有显示任何效果)
是否有其他建议来了解高 RPS 系统上此问题的原因?
谢谢!
Kamon 工具可用于查找您如何达到要求。如果您有很多地方询问可能会超时,它会很有用,但否则它不太可能告诉您问题所在。
这是因为询问超时几乎总是其他问题的征兆(唯一的例外是如果许多询问可能在流中完成(例如在 mapAsync
或 ask
阶段)但不是;这不适用于此代码)。假设超时不是由(例如)数据库关闭引起的,所以您没有收到回复或集群失败(这两者都相当明显,因此我的假设),超时的原因(任何超时,通常) 通常在队列中有太多元素(“饱和”)。
但是哪个队列?我们将从 distributor
开始,它是一个 actor 处理来自其邮箱(队列)的消息 one-at-a-time。当你说正常的处理时间是 700 微秒时,这是衡量分发者处理一个请求所花费的时间(即它可以处理下一个请求之前的时间)吗?如果是这样,并且 distributor
占用了 700 微秒,但每 600 微秒就有一次请求,这可能会发生:
- 时间 0:请求 0 进来,处理开始于
distributor
(邮箱深度 0) - 600 微:请求 1 进来,在
distributor
的邮箱中排队(邮箱深度 1) - 700 微:请求 0 完成(700 微延迟),请求 1 的处理开始(邮箱深度 0)
- 1200 微:请求 2 进入,排队(邮箱深度 1)
- 1400 微:请求 1 完成(800 微延迟),请求 2 的处理开始(邮箱深度 0)
- 1800 微:请求 3 进入,排队(邮箱深度 1)
- 2100 微:请求 2 完成(900 微延迟),请求 3 的处理开始(邮箱深度 0)
- 2400 微:请求 4 进入,排队(邮箱深度 1)
- 2800 微:请求 3 完成(1000 微延迟),请求 4 的处理开始(邮箱深度 0)
- 3000 微:请求 5 进入,排队(邮箱深度 1)
- 3500 微:请求 4 完成(1100 微延迟),请求 5 的处理开始(邮箱深度 0)
- 3600 微:请求 6 进入,排队(邮箱深度 1)
- 4200 微:请求 7 进入,排队,请求 5 完成(1200 微延迟),请求 6 的处理开始(邮箱深度 1)
- 4800 微:请求 8 进入,排队(邮箱深度 2)
- 4900 微:请求 6 完成(1300 微延迟),请求 7 的处理开始(邮箱深度 1)
- 5400 微:请求 9 进入,排队(邮箱深度 2)
等等:延迟和深度无限增加。最终,深度使得请求在邮箱中花费 5 秒(甚至更多)。
Kamon 能够跟踪演员邮箱中的消息数量(建议仅对特定演员执行此操作)。在这种情况下跟踪 distributor
的邮箱深度将显示它无限增长以确认这种情况正在发生。
如果distributor
的邮箱是太深的队列,首先考虑请求N如何影响请求N+1。Actor的one-at-a-time处理模型只是严格要求的当对请求的响应可能受到紧接在它之前的请求的影响时。如果一个请求只涉及系统整体状态的某个部分,那么该请求可以与不涉及该部分任何部分的请求并行处理。如果整个状态有不同的部分,以至于没有请求涉及 2 个或更多部分,那么可以将状态的每个部分的责任卸载给特定的参与者,并且分发者只查看每个请求足够长的时间来确定哪个将请求转发给的参与者(请注意,这通常不需要分发者提出请求:它传递请求并由它传递给(或该参与者的指定人员...)的参与者负责回复)。这基本上就是 Cluster Sharding 在幕后所做的,同样值得注意的是,这样做可能会增加低负载下的延迟(因为你正在做更多的工作),但会增加峰值吞吐量,最多可达状态部分的数量。
如果这不是解决分发者邮箱饱和的可行方法(即没有划分状态的好方法),那么您至少可以通过包含“respond-by" 请求消息中的字段(例如,对于 5 秒的询问超时,您可能需要在构造询问后 4900 毫秒内做出响应)。当分发器开始处理一条消息并且 respond-by 时间过去后,它会转到下一个请求:这样做有效地意味着当邮箱开始饱和时,消息处理率会增加。
当然是pos可能您的分发者的邮箱不是正在饱和的队列,或者即使是,也不是因为参与者花费了太多时间来处理消息。分发者(或响应所需的其他参与者)可能没有处理消息。
Actors 运行 inside a dispatcher 能够拥有一定数量的 actor(或 Future
回调或其他任务,每个都可以被视为等同于生成的 actor用于处理单个消息)在给定时间处理消息。如果在各自邮箱中有消息的 actor 数量多于可以处理消息的数量,则这些 actor 将在队列中进行调度(请注意,即使您碰巧有一个调度程序会产生同样多的调度程序,这也适用线程,因为它需要处理一条消息:由于 CPU 内核的数量有限,OS 内核调度程序的队列将充当调度程序队列的角色)。 Kamon 可以跟踪这个队列的深度。根据我的经验,检测线程饥饿(基本上是任务提交和任务开始执行之间的时间是否超过某个阈值)是否正在发生更有价值。 Lightbend 与 Akka 一起使用的商业工具包(免责声明:我受雇于 Lightbend)提供了用于检测是否正在发生饥饿并提供其他诊断信息的最小开销的工具。
如果观察到线程饥饿,并且排除了垃圾收集暂停或 CPU 节流(例如由于容器中的 运行ning)之类的事情,则线程饥饿的主要原因是参与者(或 actor-like 事物)处理一条消息的时间太长,要么是因为他们正在执行阻塞 I/O,要么是在处理单个消息时做的太多。如果阻塞 I/O 是罪魁祸首,请尝试将 I/O 移动到线程池中的 actor 或 futures 运行ning,线程池的线程数远远超过 CPU 内核的数量(一些为此甚至提倡使用无界线程池)。如果是在处理单个消息时进行过多计算的情况,请在处理过程中寻找有意义的位置,以捕获消息中剩余计算所需的状态并将该消息发送给自己(这基本上是等效的到协程屈服)。