Spray.io:何时(不)使用非阻塞路由处理?

时间:2015-04-01 06:37:03

标签: scala akka spray

如果我们考虑生产级REST API,我们应该尽可能多地使用非阻塞,例如

def insertDbAsync(rows: RowList): Future[Unit] = ...
...
val route =
path("database" / "insertRowList") {
  post {
    entity(as[RowList]) { rows =>
      log.info(s"${rows.length} rows received")
      val async = insertDbAsync(rows)
      onComplete(async) {
        case Success(response) =>
          complete("success")
        case Failure(t) =>
          complete("error")
      }
    }
  }
}

我认为答案很可能是'是',但在决定什么应该和不应该是阻止代码时有哪些指导原则?为什么?

2 个答案:

答案 0 :(得分:5)

Spray使用Akka作为底层平台,因此推荐与actor(Blocking Needs Careful Management)相同。阻止代码可能需要太多线程,这可能是:

  • 杀死actor的轻量级:默认情况下,数百万个actor可能在一个线程上运行。假设一个非阻塞的actor需要0.001个线程。一个被阻止的演员(阻塞时间比平常多100倍)将占用1个线程平均值(并不总是相同的线程)。首先,你拥有的线程越多 - 你释放的内存越多 - 每个被阻塞的线程都会在阻塞之前保存完整的callstack,包括来自堆栈的引用(因此GC不能删除它们)。其次,如果你有超过number_of_processors个线程 - 你将失去性能。第三,如果你使用一些动态池 - 添加新线程可能需要一些时间。

  • 导致线程无法使用 - 您可能在池中填充了无效的线程 - 因此在阻塞操作完成之前无法处理新任务(0%CPU负载,但等待处理100500条消息)。它甚至可能导致死锁。但是,Akka默认使用Fork-Join-Pool,所以如果您的阻止代码被管理(用scala.concurrent.blocking包围 - Await.result内部有这样的内容) - 它将通过创建新线程的成本来防止饥饿而不是阻止一个,但它不会补偿其他问题。

  • 传统上会造成死锁,所以这对设计来说很糟糕

如果代码在外面阻止,您可以将其包围在未来:

 import scala.concurrent._
 val f = Future {
     someNonBlockingCode()
     blocking { //mark this thread as "blocked" so fork-join-pool may create another one to compensate
        someBlocking()
     }  
 }

在单独的演员中:

 f pipeTo sender //will send the result to `sender` actor

内部喷涂路线:

 onComplete(f) { .. }

最好在单独的池/调度程序(基于fork-join-pool)中执行这样的期货。

P.S。作为期货的替代品(它们可能不太方便设计持续性)你可以考虑Akka I/O,Continuations / Coroutines,Actor pools(也在单独的调度员内),Disruptor等。< / p>

答案 1 :(得分:0)

如果你正在使用喷雾,那么一切都必须是非阻塞的 - 否则你将阻止(非常少数)调度线程,你的服务器将停止响应。