使用core.async与阻塞式客户端/驱动程序:是否有性能优势?

4

我正在使用Clojure编写Web应用程序的后端,其中包括以下内容:

  • http-kit 作为HTTP服务器和客户端(非阻塞)
  • monger 作为我的DB驱动程序(阻塞)
  • clj-aws-s3 作为S3客户端(阻塞)

我知道像NodeJS和Play Framework上的事件驱动、非阻塞堆栈的性能优势(这个问题帮助了我),以及它如何产生更好的负载能力。因此,我考虑使用core.async使我的后端异步。

我的问题是:通过在阻塞的客户端/驱动程序库上使用core.async,可以重现非阻塞Web堆栈的性能优势吗?


详细说明:

我目前正在做的事情是通常的同步调用:

(defn handle-my-request [req]
  (let [data1 (db/findData1)
        data2 (db/findData2)
        data3 (s3/findData3)
        result (make-something-of data1 data2 data3)]
    (ring.util.response/response result))
  )

我计划做的是将涉及到IO的任何调用都包装在一个thread块中,并在一个go块内进行同步。
(defn handle-my-request! [req resp-chan] ;; resp-chan is a core.async channel through which the response must be pushed
  (go 
    (let [data1-ch (thread (db/findData1)) ;; spin of threads to fetch the data (involves IO)
          data2-ch (thread (db/findData2))
          data3-ch (thread (s3/findData3))
          result (make-something-of (<! data1-ch) (<! data2-ch) (<! data3-ch))] ;; synchronize
     (->> (ring.util.response/response result)
       (>! resp-chan)) ;; send response
     )))

这样做有意义吗?

我这样做是因为这是最佳实践,但性能优势对我来说仍然是个谜。我原以为同步堆栈的问题在于它们每个请求使用一个线程。现在看来它们使用的线程数超过了一个。

感谢您的帮助,祝您拥有美好的一天。


1
这种方式的轻微好处是,如果异步驱动程序可用,集成将非常无缝(假设API遵循惯例)。 - Dax Fohl
你在dAni的回答发布后开启了悬赏,并且它似乎完全回答了问题。你还有什么不确定的吗? - Dax Fohl
dAni的回答更多关于处理单个请求的速度;我的问题更多关于整体负载能力。 - Valentin Waeselynck
2个回答

2

你的例子的好处在于findData1、2和3是并行运行的,这可以减少响应时间,但代价是使用更多的线程。

根据我的经验,通常情况下,调用findData2取决于findData1的结果,而findData3取决于findData2的结果,这意味着这些调用不能并行化。在这种情况下,使用core.async没有意义。


谢谢,那么潜在的负载能力受益如何?由于驱动程序被阻塞,它会受到影响吗? - Valentin Waeselynck
那是正确的。负载容量不会提高。 - DanLebrero
@ValentinWaeselynck 启动新线程的开销实际上会使情况变得更糟。您需要运行一些负载测试来确定情况有多糟。不过,我猜除非您正在运行clojure-clr,否则影响不大。 - Dax Fohl
这些线程来自线程池,因此除非您的负载非常波动,否则可以忽略创建它们的成本。 - DanLebrero
另一方面,如果我对其他操作有非阻塞式库(例如,我有一个真正的非阻塞式HTTP客户端和服务器,像http-kit),那么我不会为这些操作阻塞任何线程;这样,每个线程的阻塞时间就减少了,因此仍然是一种改进。 - Valentin Waeselynck

1
简单的回答是不会,这样你根本无法增加容量。如果你有足够的内存来保存100个线程,那么每3秒钟你就有300个"线程秒"的容量。所以,假设每个块需要1秒钟才能执行。无论每个请求是同步运行、在整个三秒钟内占用线程,还是阻塞异步运行,在一秒钟内占用线程三次,你都无法在三秒钟内服务超过100个请求。
然而,如果你使一个步骤异步化,那么你的代码每个请求只需要两个线程秒,因此你现在可以在三秒钟内服务300/2=150个请求。
更复杂的答案是,这可能会使情况变得更好或更糟,这取决于客户端或Web服务器如何处理超时、客户端重试请求的速度/频率、您的代码可并行性、线程交换的成本等因素。如果在同步实现中尝试进行200个请求,则100个请求将在3秒后完成,其余100个请求将在6秒后完成。在异步实现中,由于它们都在各种异步交汇处竞争线程,大多数请求将花费5-6秒才能完成。所以就是这样。但是,如果块是可并行化的,那么一些请求可能只需要1秒钟就能完成,所以也是这样。
因此,在极限情况下,它有点依赖于情况,但最终容量是线程秒,并且按照这个标准,同步或阻塞异步都是相同的。这不是Clojure特定的,肯定有更深入的资源详细说明所有边缘情况,而不仅仅是我在这里提供的内容。

谢谢,这正是我需要的答案。如果您能提供这些资源的参考资料,我会非常感兴趣。 - Valentin Waeselynck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接