clojure - ¿Puedo hacer una aplicación back-end totalmente no bloqueante con http-kit y core.async?
nonblocking ring (1)
Me pregunto si es posible armar una aplicación web back-end de Clojure sin bloqueo con http-kit.
(En realidad, cualquier servidor http compatible con Ring estaría bien para mí, estoy mencionando http-kit porque claims tener un modelo basado en eventos y sin bloqueo).
EDITAR: TL; DR
Esta pregunta es un síntoma de algunos conceptos erróneos que tuve sobre la naturaleza de los sistemas no bloqueantes / asincrónicos / impulsados por eventos. En caso de que estés en el mismo lugar que yo, aquí hay algunas aclaraciones.
Hacer que un sistema basado en eventos con los beneficios de rendimiento de ser no bloqueante (como en Node.js) es posible solo si todos (por ejemplo, la mayoría) de su IO se manejan de una manera no bloqueante desde cero . Esto significa que todos sus controladores de base de datos, servidores HTTP y clientes, servicios web, etc. tienen que ofrecer una interfaz asíncrona en primer lugar. En particular:
- si su controlador de base de datos ofrece una interfaz síncrona, no hay forma de que no sea bloqueante. (Tu hilo está bloqueado, no hay forma de recuperarlo). Si quiere que no se bloquee, necesita usar algo más.
- Las utilidades de coordinación de alto nivel como core.async no pueden hacer que un sistema no bloquee. Pueden ayudarlo a administrar código que no sea de bloqueo, pero no lo habilite.
- Si sus controladores IO son sincrónicos, puede usar core.async para tener los beneficios de diseño de la asincronía, pero no obtendrá los beneficios de rendimiento de la misma. Sus hilos seguirán perdiendo el tiempo esperando cada respuesta.
Ahora, específicamente:
- http-kit como servidor HTTP ofrece una interfaz asincrónica sin bloqueo. Vea abajo.
- Sin embargo, muchos middlewares Ring, dado que son esencialmente sincrónicos, no serán compatibles con este enfoque. Básicamente, cualquier middleware Ring que actualice la respuesta devuelta no será utilizable.
Si lo hice bien (y no soy un experto, así que dime si estoy trabajando en suposiciones erróneas), los principios de un modelo sin bloqueo para una aplicación web son los siguientes:
- Tener unos pocos subprocesos de SO superrápidos que manejen toda la informática intensiva de CPU; estos nunca deben estar esperando .
- Tienen una gran cantidad de "hilos débiles" que manejan el IO (llamadas a bases de datos, llamadas al servicio web, durmiendo, etc.); estos están destinados principalmente a estar esperando .
- Esto es beneficioso porque el tiempo de espera dedicado al manejo de una solicitud suele ser de 2 (acceso al disco) a 5 (llamadas de servicios web) de mayor magnitud que el tiempo de computación.
Por lo que he visto, este modelo es compatible por defecto en las plataformas Play Framework (Scala) y Node.js (JavaScript), con utilidades basadas en promesas para administrar la asincronía mediante programación.
Tratemos de hacer esto en una aplicación clojure basada en Ring, con enrutamiento Compojure. Tengo una ruta que construye la respuesta llamando a la función 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))
Parece que la forma comúnmente aceptada de administrar la asincronía en aplicaciones Clojure es basada en CSP, con el uso de la biblioteca core.async , con la cual estoy totalmente bien. Entonces, si quisiera adoptar los principios de no bloqueo enumerados anteriormente, implementaría my-handle
esta manera:
(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)))
)))
La tarea construct-my-response
intensiva en CPU se realiza en un bloque- go
mientras que la espera de recursos externos se realiza en bloques- thread
, como lo sugiere Tim Baldridge en este video en core.async (38''55 '''')
Pero eso no es suficiente para que mi aplicación no sea bloqueante. Cualquier hilo que pase por mi ruta y llame a la función my-handle
, estará esperando a que se construya la respuesta, ¿verdad?
¿Sería beneficioso (como creo) hacer que este manejo de HTTP también sea no bloqueante, de ser así, ¿cómo puedo lograrlo?
EDITAR
Como señaló codemomentum, el ingrediente faltante para un manejo no bloqueante de la solicitud es usar canales de http-kit. Junto con core.async, el código anterior se convertiría en algo así:
(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))
)))
Esto le permite adoptar un modelo asíncrono de hecho.
El problema con esto es que es bastante incompatible con el middleware Ring. Un middleware Ring usa una llamada de función para obtener la respuesta, lo que la hace esencialmente sincrónica. En términos más generales, parece que el manejo impulsado por eventos no es compatible con una interfaz de programación funcional pura, porque desencadenar eventos significa tener efectos secundarios.
Me alegraría saber si hay una biblioteca de Clojure que aborde esto.
Con el enfoque asincrónico, puedes enviar los datos al cliente cuando está listo, en lugar de bloquear un hilo todo el tiempo que está preparado.
Para http-kit, debe usar un controlador asíncrono descrito en la documentación. Después de delegar la solicitud a un controlador asíncrono de manera adecuada, puede implementarla de la forma que prefiera utilizando core.async u otra cosa.
La documentación de Async Handler está aquí: http://http-kit.org/server.html#channel