如果我们考虑生产级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")
}
}
}
}
我认为答案很可能是'是',但在决定什么应该和不应该是阻止代码时有哪些指导原则?为什么?
答案 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)
如果你正在使用喷雾,那么一切都必须是非阻塞的 - 否则你将阻止(非常少数)调度线程,你的服务器将停止响应。