Akka HTTP和长时间运行的请求

时间:2016-11-24 14:37:15

标签: multithreading scala concurrency akka akka-http

我们有一个用简单的Scala Akka HTTP实现的API - 面向大量计算(CPU和内存密集型)的几条路径。没有集群 - 所有都在一台强大的机器上运行。计算量很大 - 一个隔离请求可能需要60多秒才能完成。我们并不关心这么快的速度。没有阻塞IO,只有很多CPU处理。

当我开始对该事物进行性能测试时,一个有趣的模式显示:请求A1,A2,...,A10通过。他们使用的资源非常多,事实证明,Akka将为请求A5-A10返回HTTP 503。问题是,即使没有人拿起结果,计算仍在运行。

从那里我们看到级联性能崩溃:请求A11-A20到达服务器仍在处理请求A5-A10 。显然,这些新请求也有可能超出 - 甚至更高,因为服务器更繁忙。因此,当Akka触发超时时,它们中的一些将运行,使服务器更加繁忙和更慢,然后新一批请求通过......所以在运行系统一段时间后,您会看到几乎所有请求点开始失败并超时。在您停止加载后,您会在日志中看到一些请求仍在处理中。

我尝试在单独的ExecutionContext和系统调度程序中运行计算,尝试使其完全异步(通过Future组合),但结果仍然相同。繁琐的工作使服务器如此繁忙,最终几乎每个请求都会失败。

https://github.com/zcox/spray-blocking-test中描述了一个类似的情况,但重点转移到那里 - /ping对于我们来说无关紧要,就像处理长时间运行请求的端点上的或多或少的稳定责任一样。

问题:如何设计应用程序以更好地中断挂起请求?在重负载下,我可以容忍一小部分失败的请求,但几秒钟后将整个系统停止运行是不可接受的。

2 个答案:

答案 0 :(得分:1)

Akka HTTP不会自动终止已超时的请求的处理。通常需要额外的簿记才能获得回报,因此默认情况下不会打开。我认为这是一种疏忽,TBH,我自己也遇到过与Akka HTTP类似的问题。

我认为您需要在请求超时时手动中止处理,否则服务器在超载时将无法恢复,如您所见。

没有一种标准机制可以用来实现它(参见“How to cancel Future in Scala?”)。如果线程在没有i / o的情况下进行CPU工作,那么Thread.interrupt()将没有用处。相反,您应该创建一个DeadlinePromise或类似的,以显示请求是否仍处于打开状态,并在计算过程中传递并定期检查超时:

// in the HTTP server class:
val responseTimeout: Duration = 30.seconds

val routes = 
  path("slowComputation") {
    complete {
      val responseTimeoutDeadline: Deadline = responseTimeout.fromNow
      computeSlowResult(responseTimeoutDeadline)
    }
  }

// in the processing code:
def computeSlowResult(responseDeadline: Deadline): Future[HttpResponse] = Future {
  val gatherInputs: List[_] = ???
  gatherInputs.fold(0) { (acc, next) =>

    // check if the response has timed out
    if (responseDeadline.isOverdue())
      throw new TimeoutException()

    acc + next // proceed with the calculation a little
  }
}

(检查Promise是否已完成将比检查Deadline是否已过期便宜得多。我已将上面的代码放在上面,因为它更容易编写。)

答案 1 :(得分:0)

spray-blocking-test使用我认为不存在于Akka HTTP中的库。我有一个类似的问题,我解决了如下:

<强> application.conf

blocking-io-dispatcher {
  type = Dispatcher
  executor = "thread-pool-executor"
  thread-pool-executor {
    fixed-pool-size = 16
  }
  throughput = 1
}

<强>路线

complete {
  Try(new URL(url)) match {
    case scala.util.Success(u) => {
      val src = Source.fromIterator(() => parseMovies(u).iterator)

      src
        .via(findMovieByTitleAndYear)
        .via(persistMovies)
        .completionTimeout(5.seconds)
        .toMat(Sink.fold(Future(0))((acc, elem) => Applicative[Future].map2(acc, elem)(_ + _)))(Keep.right)
        // run the whole graph on a separate dispatcher
        .withAttributes(ActorAttributes.dispatcher("blocking-io-dispatcher"))
        .run.flatten
        .onComplete {
            _ match {
               case scala.util.Success(n) => logger.info(s"Created $n movies")
               case Failure(t) => logger.error(t, "Failed to process movies")
            }
        }

      Accepted
    }
    case Failure(t) => logger.error(t, "Bad URL"); BadRequest -> "Bad URL"
  }
}

当处理在后台继续发生时,响应立即返回。

补充阅读:

http://doc.akka.io/docs/akka/current/scala/dispatchers.html http://blog.akka.io/streams/2016/07/06/threading-and-concurrency-in-akka-streams-explained