我想知道是否可以将一个完全非阻塞的 Clojure 后端 Web 应用程序与 http-kit 放在一起。
(实际上任何与 Ring 兼容的 http 服务器对我来说都可以;我提到 http-kit 是因为它声称具有事件驱动的非阻塞模型)。
编辑:TL;博士
这个问题是我对非阻塞/异步/事件驱动系统的性质有一些误解的症状。如果你和我在同一个地方,这里有一些澄清。
只有当所有(比如说,大部分) IO 从头开始以非阻塞方式处理时,才能创建一个具有非阻塞性能优势的事件驱动系统(如在 Node.js 中)。这意味着您的所有数据库驱动程序、HTTP 服务器和客户端、Web 服务等都必须首先提供异步接口。尤其:
- 如果您的数据库驱动程序提供同步接口,则无法使其成为非阻塞的。(您的线程被阻止,无法检索它)。如果你想要非阻塞,你需要使用别的东西。
- 像 core.async 这样的高级协调实用程序不能使系统成为非阻塞的。它们可以帮助您管理非阻塞代码,但不要启用它。
- 如果您的 IO 驱动程序是同步的,您可以使用 core.async 来获得异步的设计优势,但您不会获得它的性能优势。您的线程仍将浪费时间等待每个响应。
现在,具体来说:
- http-kit 作为 HTTP 服务器提供了一个非阻塞的异步接口。见下文。
- 但是,许多 Ring 中间件,因为它们本质上是同步的,因此与这种方法不兼容。基本上,任何更新返回响应的 Ring 中间件都将不可用。
如果我做对了(而且我不是专家,所以如果我的假设是错误的,请告诉我),这种 Web 应用程序的非阻塞模型的原则如下:
- 让几个超快的操作系统线程处理所有 CPU 密集型计算;这些绝不能等待。
- 有很多“弱线程”处理 IO(数据库调用、Web 服务调用、休眠等);这些主要是为了等待。
- 这是有益的,因为处理请求所花费的等待时间通常比计算时间高 2(磁盘访问)到 5(Web 服务调用)数量级。
据我所知,Play Framework (Scala) 和Node.js (JavaScript) 平台默认支持此模型,并使用基于 Promise 的实用程序以编程方式管理异步。
让我们尝试在基于 Ring 的 clojure 应用程序中使用 Compojure 路由来执行此操作。我有一个通过调用my-handle
函数构造响应的路由:
(defroutes my-routes
(GET "/my/url" req (my-handle req))
)
(def my-app (noir.util.middleware/app-handler [my-routes]))
(defn start-my-server! []
(http-kit/run-server my-app))
似乎在 Clojure 应用程序中管理异步的普遍接受的方式是基于 CSP 的,使用core.async库,我完全没问题。所以如果我想接受上面列出的非阻塞原则,我会这样实现my-handle
:
(require '[clojure.core.async :as a])
(defn my-handle [req]
(a/<!!
(a/go ; `go` makes channels calls asynchronous, so I'm not really waiting here
(let [my-db-resource (a/thread (fetch-my-db-resource)) ; `thread` will delegate the waiting to "weaker" threads
my-web-resource (a/thread (fetch-my-web-resource))]
(construct-my-response (a/<! my-db-resource)
(a/<! my-web-resource)))
)))
CPU 密集型construct-my-response
任务在go
-block 中执行,而等待外部资源在thread
-blocks 中完成,正如 Tim Baldridge 在 core.async (38'55'')上的视频中所建议的那样
但这还不足以使我的应用程序不阻塞。无论线程通过我的路线并调用该my-handle
函数,都将等待构建响应,对吗?
使这种 HTTP 处理也非阻塞是否有益(我相信),如果是这样,我该如何实现呢?
编辑
正如 codemomentum 指出的那样,对请求进行非阻塞处理的缺失要素是使用 http-kit 通道。结合 core.async,上面的代码会变成这样:
(defn my-handle! [req]
(http-kit/with-channel req channel
(a/go
(let [my-db-resource (a/thread (fetch-my-db-resource))
my-web-resource (a/thread (fetch-my-web-resource))
response (construct-my-response (a/<! my-db-resource)
(a/<! my-web-resource))]
(send! channel response)
(close channel))
)))
这使您确实可以采用异步模型。
问题在于它与 Ring 中间件几乎不兼容。Ring 中间件使用函数调用来获取响应,这使得它本质上是同步的。更一般地说,事件驱动处理似乎与纯函数式编程接口不兼容,因为触发事件意味着有副作用。
我很高兴知道是否有一个 Clojure 库可以解决这个问题。