¿Cuál es la respuesta de Haskell a Node.js?

217

Creo que la comunidad de Erlang no tiene envidia de Node.js, ya que lo hace sin bloqueo de E / S de forma nativa y tiene formas de escalar fácilmente las implementaciones a más de un procesador (algo que ni siquiera está integrado en Node.js). Más detalles en http://journal.dedasys.com/2010/04/29/erlang-vs-node-js y Node.js o Erlang

¿Qué hay de Haskell? ¿Puede Haskell proporcionar algunos de los beneficios de Node.js, es decir, una solución limpia para evitar el bloqueo de E / S sin recurrir a la programación de subprocesos múltiples?


Hay muchas cosas que son atractivas con Node.js

  1. Eventos: sin manipulación de subprocesos, el programador solo proporciona devoluciones de llamada (como en el marco Snap)
  2. Se garantiza que las devoluciones de llamada se ejecuten en un solo hilo: no es posible la condición de carrera.
  3. Agradable y simple API compatible con UNIX. Bonificación: Excelente soporte HTTP. DNS también disponible.
  4. Cada E / S es por defecto asíncrona. Esto hace que sea más fácil evitar las cerraduras. Sin embargo, el procesamiento excesivo de la CPU en una devolución de llamada afectará a otras conexiones (en este caso, la tarea debería dividirse en subtareas más pequeñas y reprogramarse).
  5. Mismo idioma para el lado del cliente y del lado del servidor. (Sin embargo, no veo demasiado valor en este caso. JQuery y Node.js comparten el modelo de programación de eventos, pero el resto es muy diferente. Simplemente no puedo ver cómo compartir código entre el lado del servidor y el lado del cliente Ser útil en la práctica.)
  6. Todo esto empaquetado en un solo producto.
gawi
fuente
17
Creo que deberías hacer esta pregunta a los programadores .
Jonas
47
No incluir un fragmento de código no lo convierte en una pregunta subjetiva.
gawi
20
No sé mucho sobre node.js, pero una cosa me llamó la atención sobre su pregunta: ¿por qué le parece tan desagradable la posibilidad de hilos? Los subprocesos deberían ser exactamente la solución correcta para multiplexar E / S. Aquí utilizo ampliamente el término hilos, incluidos los procesos de Erlang. ¿Quizás te preocupan las cerraduras y el estado mutable? No tiene que hacer las cosas de esa manera: use el paso de mensajes o las transacciones si eso tiene más sentido para su aplicación.
Simon Marlow
99
@gawi No creo que suene muy fácil de programar, sin tener que evitar, tienes que lidiar con la posibilidad de inanición y largas latencias. Básicamente, los subprocesos son la abstracción correcta para un servidor web: no hay necesidad de lidiar con las E / S asíncronas y todas las dificultades que conlleva, solo hágalo en un subproceso. Por cierto, escribí un artículo sobre servidores web en Haskell que puede encontrar interesante: haskell.org/~simonmar/papers/web-server-jfp.pdf
Simon Marlow el
3
"Se garantiza que las devoluciones de llamada se ejecuten en un solo subproceso: no es posible la condición de carrera". Incorrecto. Puede tener condiciones de carrera fácilmente en Node.js; solo asuma que una acción de E / S se completará antes que otra, y BOOM. Lo que sí es imposible es un tipo particular de condiciones de carrera, a saber, el acceso simultáneo no sincronizado al mismo byte en la memoria.
plegado a la derecha el

Respuestas:

219

De acuerdo, después de haber visto un poco de la presentación de node.js a la que @gawi me señaló, puedo decir un poco más sobre cómo Haskell se compara con node.js. En la presentación, Ryan describe algunos de los beneficios de Green Threads, pero luego continúa diciendo que no encuentra que la falta de abstracción de hilo sea una desventaja. No estoy de acuerdo con su posición, particularmente en el contexto de Haskell: creo que las abstracciones que proporcionan los hilos son esenciales para hacer que el código del servidor sea más fácil de corregir y más robusto. En particular:

  • El uso de un subproceso por conexión le permite escribir código que expresa la comunicación con un solo cliente, en lugar de escribir código que trata con todos los clientes al mismo tiempo. Piénselo de esta manera: un servidor que maneja múltiples clientes con hilos se ve casi igual que uno que maneja un solo cliente; La principal diferencia es que hay un forklugar en el primero. Si el protocolo que está implementando es del todo complejo, administrar la máquina de estado para varios clientes simultáneamente es bastante complicado, mientras que los hilos le permiten simplemente escribir la comunicación con un solo cliente. El código es más fácil de entender y más fácil de entender y mantener.

  • las devoluciones de llamada en un solo subproceso del sistema operativo es multitarea cooperativa, a diferencia de la multitarea preventiva, que es lo que obtienes con los subprocesos. La principal desventaja con la multitarea cooperativa es que el programador es responsable de asegurarse de que no haya inanición. Pierde modularidad: comete un error en un solo lugar y puede arruinar todo el sistema. Esto es realmente algo de lo que no debe preocuparse, y la prevención es la solución simple. Además, la comunicación entre devoluciones de llamada no es posible (sería un punto muerto).

  • la concurrencia no es difícil en Haskell, porque la mayoría del código es puro y, por lo tanto, es seguro para subprocesos por construcción. Hay simples primitivas de comunicación. Es mucho más difícil dispararse en el pie con concurrencia en Haskell que en un idioma con efectos secundarios sin restricciones.

Simon Marlow
fuente
42
Ok, entiendo que node.js es la solución a 2 problemas: 1- la concurrencia es difícil en la mayoría de los idiomas, 2- el uso de hilos del sistema operativo es expansivo. La solución de Node.js es utilizar la concurrencia basada en eventos (w / libev) para evitar la comunicación entre subprocesos y evitar problemas de escalabilidad de subprocesos del sistema operativo. Haskell no tiene el problema n. ° 1 debido a la pureza. Para el n. ° 2, Haskell tiene hilos livianos + administrador de eventos que se optimizó recientemente en GHC para contextos a gran escala. Además, el uso de Javascript no se puede percibir como una ventaja para ningún desarrollador de Haskell. Para algunas personas que usan Snap Framework, Node.js es "simplemente malo".
gawi
44
El procesamiento de solicitudes es la mayoría de las veces una secuencia de operaciones interdependientes. Tiendo a estar de acuerdo en que usar callbacks para cada operación de bloqueo puede ser engorroso. Los hilos son más adecuados que la devolución de llamada para esto.
gawi
10
¡Sí! Y la nueva multiplexación de E / S en GHC 7 hace que los servidores de escritura en Haskell sean aún mejores.
andreypopp
3
Su primer punto no tiene mucho sentido para mí (como un extraño) ... Al procesar una solicitud en node.js, su devolución de llamada trata con un solo cliente. Administrar el estado solo se convierte en algo de qué preocuparse cuando se escala a múltiples procesos, e incluso entonces es bastante fácil usar las bibliotecas disponibles.
Ricardo Tomasi
12
No es un tema separado. Si esta pregunta es una búsqueda genuina de las mejores herramientas para el trabajo en Haskell, o una verificación de si existen herramientas excelentes para el trabajo en Haskell, entonces la suposición implícita de que la programación multiproceso sería inadecuada debe ser cuestionada, porque Haskell sí hilos de manera bastante diferente, como Don Stewart señala. Las respuestas que explican por qué la comunidad de Haskell tampoco está celosa de Node.js están muy relacionadas con esta pregunta. La respuesta de Gawi sugiere que fue una respuesta apropiada a su pregunta.
AndrewC
154

¿Puede Haskell proporcionar algunos de los beneficios de Node.js, es decir, una solución limpia para evitar el bloqueo de E / S sin recurrir a la programación de subprocesos múltiples?

Sí, de hecho, los eventos y los hilos están unificados en Haskell.

  • Puede programar en subprocesos ligeros explícitos (por ejemplo, millones de subprocesos en una sola computadora portátil).
  • O; puede programar en un estilo asíncrono basado en eventos, basado en notificaciones de eventos escalables.

Los hilos se implementan realmente en términos de eventos y se ejecutan en múltiples núcleos, con migración de subprocesos sin interrupciones, con rendimiento documentado y aplicaciones.

Por ejemplo para

Colecciones concurrentes nbody en 32 núcleos

texto alternativo

En Haskell tienes tanto eventos como hilos, y como todos los eventos están bajo el capó.

Lea el documento que describe la implementación.

Don Stewart
fuente
2
Gracias. Necesito digerir todo esto ... Esto parece ser específico de GHC. Supongo que está bien. El lenguaje Haskell es en algún momento como cualquier cosa que GHC pueda compilar. De manera similar, la "plataforma" de Haskell es más o menos el tiempo de ejecución de GHC.
gawi
1
@gawi: Ese y todos los otros paquetes que se incluyen en él para que sea útil de inmediato. Y esta es la misma imagen que vi en mi curso de CS; y la mejor parte es que no es difícil en Haskell lograr resultados asombrosos similares en sus propios programas.
Robert Massaioli
1
Hola Don, ¿crees que podrías vincular al servidor web haskell que funciona mejor (Warp) al responder preguntas como estas? Aquí está el punto de referencia bastante relevante contra Node.js: yesodweb.com/blog/2011/03/…
Greg Weber
44
Solo en teoria. Los "hilos ligeros" de Haskell no son tan ligeros como crees. Es mucho, mucho, mucho más barato registrar una devolución de llamada en una interfaz epoll que programar un llamado hilo verde, por supuesto, son más baratos que los hilos del sistema operativo, pero no son gratuitos. Crear 100.000 de ellos utiliza aprox. 350 MB de memoria y toma algo de tiempo. Pruebe 100.000 conexiones con node.js. No hay problema . Sería mágico si no fuera más rápido ya que ghc usa epoll debajo del capó, por lo que no pueden ser más rápidos que usar epoll directamente. Sin embargo, la programación con interfaz de hilos es bastante agradable.
Kr0e
3
Además: el nuevo administrador de E / S (ghc) utiliza un algoritmo de programación que tiene (m log n) complejidad (donde m es el número de subprocesos ejecutables yn el número total de subprocesos). Epoll tiene complejidad k (k es el número de fd's legibles / grabables =. Entonces ghc tiene O (k * m log n) sobre toda la complejidad, lo que no es muy bueno si enfrenta conexiones de alto tráfico. Node.js tiene la complejidad lineal causada por epoll. Y no hablemos del rendimiento de Windows ... Node.js es mucho más rápido porque usa IOCP.
Kr0e
20

En primer lugar, no creo que node.js esté haciendo lo correcto exponiendo todas esas devoluciones de llamada. Terminas escribiendo tu programa en CPS (estilo de paso de continuación) y creo que debería ser el trabajo del compilador hacer esa transformación.

Eventos: sin manipulación de subprocesos, el programador solo proporciona devoluciones de llamada (como en el marco Snap)

Con esto en mente, puede escribir usando un estilo asíncrono si lo desea, pero al hacerlo, se perdería la escritura en un estilo sincrónico eficiente, con un hilo por solicitud. Haskell es ridículamente eficiente en el código síncrono, especialmente en comparación con otros idiomas. Son todos los eventos debajo.

Se garantiza que las devoluciones de llamada se ejecuten en un solo hilo: no es posible la condición de carrera.

Aún podría tener una condición de carrera en node.js, pero es más difícil.

Cada solicitud está en su propio hilo. Cuando escribes código que tiene que comunicarse con otros hilos, es muy sencillo hacerlo seguro gracias a las primitivas de concurrencia de Haskell.

Agradable y simple API compatible con UNIX. Bonificación: Excelente soporte HTTP. DNS también disponible.

Echa un vistazo a los ataques y compruébalo por ti mismo.

Cada E / S es por defecto asíncrona (sin embargo, esto puede ser molesto a veces). Esto hace que sea más fácil evitar las cerraduras. Sin embargo, el procesamiento excesivo de la CPU en una devolución de llamada afectará a otras conexiones (en este caso, la tarea debería dividirse en subtareas más pequeñas y reprogramarse).

No tiene tales problemas, ghc distribuirá su trabajo entre los hilos reales del sistema operativo.

Mismo idioma para el lado del cliente y del lado del servidor. (Sin embargo, no veo demasiado valor en este caso. JQuery y Node.js comparten el modelo de programación de eventos, pero el resto es muy diferente. Simplemente no puedo ver cómo compartir código entre el lado del servidor y el lado del cliente podría Ser útil en la práctica.)

Haskell no puede ganar aquí ... ¿verdad? Piense de nuevo, http://www.haskell.org/haskellwiki/Haskell_in_web_browser .

Todo esto empaquetado en un solo producto.

Descarga ghc, enciende cabal. Hay un paquete para cada necesidad.

dan_waterworth
fuente
Solo estaba jugando al abogado del diablo. Entonces, sí, estoy de acuerdo con tus puntos. Excepto la unificación del lenguaje del lado del cliente y del lado del servidor. Si bien creo que es técnicamente factible, no creo que eventualmente pueda reemplazar todo el ecosistema de Javascript en su lugar hoy (JQuery y amigos). Si bien es un argumento presentado por los partidarios de Node.js, no creo que sea muy importante. ¿Realmente necesitas compartir tanto código entre tu capa de presentación y tu backend? ¿Realmente apuntamos a que los programadores conozcan un solo idioma?
gawi
La verdadera victoria es que puede renderizar páginas tanto en el lado del servidor como del cliente, haciendo que las páginas en tiempo real sean más fáciles de crear.
dan_waterworth
@dan_waterworth exactamente, ver meteorito o derby.js
mb21
1
@gawi Tenemos servicios de producción donde el 85% del código se comparte entre el cliente y el servidor. Esto se conoce como JavaScript universal en la comunidad. Estamos usando React para renderizar dinámicamente contenido en el servidor para disminuir el tiempo de la primera representación útil en el cliente. Si bien soy consciente de que puede ejecutar Haskell en el navegador, no conozco ningún conjunto de mejores prácticas "universales de Haskell" que permitan la representación del lado del servidor y del lado del cliente utilizando la misma base de código.
Eric Elliott
8

Personalmente, veo Node.js y la programación con devoluciones de llamada como algo innecesariamente de bajo nivel y un poco antinatural. ¿Por qué programar con devoluciones de llamada cuando un buen tiempo de ejecución como el que se encuentra en GHC puede manejar las devoluciones de llamada por usted y hacerlo de manera bastante eficiente?

Mientras tanto, el tiempo de ejecución de GHC ha mejorado enormemente: ahora presenta un "nuevo nuevo administrador de E / S" llamado MIO donde "M" significa multinúcleo, creo. Se basa en la base del administrador de E / S existente y su objetivo principal es superar la causa de la degradación del rendimiento de más de 4 núcleos. Los números de rendimiento proporcionados en este documento son bastante impresionantes. Verse a sí mismo:

Con Mio, los servidores HTTP realistas en Haskell escalan a 20 núcleos de CPU, logrando un rendimiento máximo hasta un factor de 6.5x en comparación con los mismos servidores que usan versiones anteriores de GHC. La latencia de los servidores Haskell también se mejora: [...] bajo una carga moderada, reduce el tiempo de respuesta esperado en 5.7x en comparación con versiones anteriores de GHC

Y:

También mostramos que con Mio, McNettle (un controlador SDN escrito en Haskell) puede escalar efectivamente a más de 40 núcleos, alcanzar un rendimiento completo de más de 20 millones de nuevas solicitudes por segundo en una sola máquina y, por lo tanto, convertirse en el más rápido de todos los controladores SDN existentes .

Mio ha llegado a la versión GHC 7.8.1. Personalmente, veo esto como un gran paso adelante en el rendimiento de Haskell. Sería muy interesante comparar el rendimiento de las aplicaciones web existentes compilado por la versión anterior de GHC y 7.8.1.

vlprans
fuente
6

Los eventos de mi humilde opinión son buenos, pero la programación por medio de devoluciones de llamada no lo es.

La mayoría de los problemas que hacen especial la codificación y depuración de aplicaciones web proviene de lo que las hace escalables y flexibles. Lo más importante, la naturaleza sin estado de HTTP. Esto mejora la navegabilidad, pero impone una inversión de control donde el elemento IO (el servidor web en este caso) llama a diferentes controladores en el código de la aplicación. Este modelo de evento -o modelo de devolución de llamada, dicho con mayor precisión- es una pesadilla, ya que las devoluciones de llamada no comparten ámbitos variables y se pierde una vista intuitiva de la navegación. Es muy difícil evitar todos los posibles cambios de estado cuando el usuario navega de un lado a otro, entre otros problemas.

Se puede decir que los problemas son similares a la programación de GUI donde el modelo de evento funciona bien, pero las GUI no tienen navegación ni botón de retroceso. Eso multiplica las transiciones de estado posibles en las aplicaciones web. El resultado del intento de resolver este problema son marcos pesados ​​con configuraciones complicadas y muchos identificadores mágicos omnipresentes sin cuestionar la raíz del problema: el modelo de devolución de llamada y su falta inherente de compartir ámbitos variables, y ninguna secuencia, por lo que la secuencia tiene que ser construido vinculando identificadores.

Existen marcos basados ​​en secuencia como ocsigen (ocaml) seaside (smalltalk) WASH (descontinuado, Haskell) y mflow (Haskell) que resuelven el problema de la gestión del estado mientras mantienen la navegabilidad y la REST-plenitud. Dentro de estos marcos, el programador puede expresar la navegación como una secuencia imperativa donde el programa envía páginas y espera respuestas en un solo hilo, las variables están dentro del alcance y el botón Atrás funciona automáticamente. Esto inherentemente produce un código más corto, más seguro y más legible donde la navegación es claramente visible para el programador. (advertencia justa: soy el desarrollador de mflow)

revs agocorona
fuente
En node.js, las devoluciones de llamada se utilizan para manejar E / S asíncrona, por ejemplo, a bases de datos. Estás hablando de algo diferente que, aunque interesante, no responde la pregunta.
Robin Green
Tienes razón. Tomó tres años tener una respuesta que, espero, cumpla con sus objeciones: github.com/transient-haskell
agocorona
Node ahora admite funciones asíncronas, lo que significa que puede escribir código de estilo imperativo que sea realmente asíncrono. Utiliza promesas debajo del capó.
Eric Elliott
5

La pregunta es bastante ridícula porque 1) Haskell ya ha resuelto este problema de una manera mucho mejor y 2) más o menos de la misma manera que Erlang lo ha hecho. Aquí está el punto de referencia contra el nodo: http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks

Dale a Haskell 4 núcleos y puede hacer 100k solicitudes (simples) por segundo en una sola aplicación. Node no puede hacer tantos, y no puede escalar una sola aplicación a través de los núcleos. Y no tiene que hacer nada para cosechar esto porque el tiempo de ejecución de Haskell no bloquea. El único otro lenguaje (relativamente común) que tiene IO sin bloqueo integrado en el tiempo de ejecución es Erlang.

Greg Weber
fuente
14
¿Ridículo? La pregunta no es "¿Haskell tiene una respuesta" sino más bien "cuál es la respuesta de Haskell". En el momento en que se hizo la pregunta, GHC 7 ni siquiera se lanzó, por lo que Haskell aún no estaba "en el juego" (excepto tal vez para marcos que usan libev como Snap). Aparte de eso, estoy de acuerdo.
gawi
1
No sé si esto fue cierto cuando publicó esta respuesta, pero ahora, de hecho, hay módulos de nodo que permiten que las aplicaciones de nodo escalen fácilmente a través de los núcleos. Además, ese enlace está comparando node.js ejecutándose en un solo núcleo con haskell ejecutándose en 4 núcleos. Me gustaría ver que se ejecute nuevamente en una configuración más justa, pero, por desgracia, el repositorio de Github se ha ido.
Tim Gautier
2
Haskell utilizando más de 4 núcleos degrada el rendimiento de la aplicación. Hubo un documento sobre este tema, se trabajó activamente, pero sigue siendo un problema. Por lo tanto, ejecutar 16 instancias de Node.js en un servidor de 16 núcleos probablemente sea mucho mejor que una sola aplicación ghc que use + RTS -N16, que de hecho será más lenta que + RTS -N1 debido a este error de tiempo de ejecución. Es porque usan solo un IOManager que se ralentizará cuando se use con muchos subprocesos del sistema operativo. Espero que solucionen este error, pero existe desde entonces, así que no tendría muchas esperanzas ...
Kr0e 02 de
Cualquiera que vea esta respuesta debe saber que Node puede procesar fácilmente 100k solicitudes simples en un solo núcleo y es trivialmente fácil escalar una aplicación Node sin estado en muchos núcleos. pm2 -i max path/to/app.jsse escalará automáticamente al número óptimo de instancias en función de los núcleos disponibles. Además, Node también no se bloquea por defecto.
Eric Elliott
1

Al igual que nodejs ha dejado de liberar, el Marco web Snap Haskell también ha dejado de liberar .

Chawathe Vipul S
fuente
1
¿Cómo responde esto a la pregunta?
dfeuer
1
@dfeuer El enlace debe leerse, Snap Haskell Web Framework se ha liberado, no sé por qué falla el formateo. El tiempo de ejecución del servidor de nodo tenía que ver con Linux libev cuando comenzó, al igual que Snap Web FrameWork. Haskell con Snap es como ECMAscript con nodejs, por lo que la forma en que Snap evoluciona junto con nodejs es más relevante que Haskell, que puede compararse más correctamente con ECMAscript en este contexto.
Chawathe Vipul S