Мне интересно, можно ли собрать полностью неблокирующее внутреннее веб-приложение Clojure с http-kit.
(На самом деле мне подойдет любой HTTP-сервер, совместимый с кольцом; я упоминаю http-kit, потому что он утверждает, что имеет управляемая событиями неблокирующая модель).
РЕДАКТИРОВАТЬ: TL; DR
Этот вопрос является признаком некоторых неправильных представлений о природе неблокирующих / асинхронных / событийно-управляемых систем. Если вы находитесь в том же месте, что и я, вот некоторые пояснения.
Создание системы, управляемой событиями, с преимуществом в производительности, заключающимся в том, что она неблокирующая (как в Node.js), возможно только в том случае, если все (скажем, большая часть) вашего ввода-вывода обрабатываются неблокирующим способом. с нуля. Это означает, что все ваши драйверы БД, HTTP-серверы и клиенты, веб-службы и т. Д. Должны в первую очередь предлагать асинхронный интерфейс. В частности:
- если ваш драйвер базы данных предлагает синхронный интерфейс, нет способа сделать его неблокирующим. (Ваша цепочка заблокирована, восстановить ее невозможно). Если вы хотите неблокировать, вам нужно использовать что-то еще.
- Утилиты координации высокого уровня, такие как core.async, не могут сделать систему неблокирующей. Они могут помочь вам управлять неблокирующим кодом, но не включайте его.
- Если ваши драйверы ввода-вывода синхронны, вы можете использовать core.async, чтобы получить дизайн преимущества асинхронности, но вы не получите от этого преимущества в производительности. Ваши потоки по-прежнему будут тратить время на ожидание каждого ответа.
А теперь конкретно:
- http-kit в качестве HTTP-сервера предлагает неблокирующий асинхронный интерфейс. Увидеть ниже.
- Однако многие промежуточные программы Ring, поскольку они по сути синхронны, несовместимы с этим подходом. По сути, любое промежуточное программное обеспечение Ring, обновляющее возвращаемый ответ, не будет использоваться.
Если я понял это правильно (а я не эксперт, поэтому, пожалуйста, скажите мне, если я работаю с неправильными предположениями), принципы такой неблокирующей модели для веб-приложения следующие:
- Пусть несколько сверхбыстрых потоков ОС обрабатывают все вычислительные ресурсы с интенсивным использованием ЦП; эти никогда не должны ждать.
- Иметь много "слабых потоков", обрабатывающих ввод-вывод (вызовы базы данных, вызовы веб-служб, спящий режим и т. Д.); эти в основном предназначены для ожидания.
- Это выгодно, потому что время ожидания, затрачиваемое на обработку запроса, обычно на 2 (доступ к диску) - 5 (вызовы веб-служб) порядков превышает время вычислений.
Из того, что я видел, эта модель по умолчанию поддерживается в Play Framework (Scala) и Node.js (JavaScript) с утилитами на основе обещаний для программного управления асинхронностью.
Давайте попробуем сделать это в приложении 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)))
)))
Задача construct-my-response
, требующая интенсивного использования ЦП, выполняется в go
-блоке, тогда как ожидание внешних ресурсов выполняется в thread
-блоках, как предложил Тим Болдридж в это видео на 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, которая решает эту проблему.