Arquitectura y rendimiento de Node js

Resuelto sjtitus asked hace 6 años • 1 respuestas

Tengo una pregunta sobre la arquitectura y el rendimiento de Node js.

He leído mucho sobre el tema (incluido Stack Overflow) y todavía tengo un par de preguntas. Me gustaría hacer 2 cosas:

  1. Resuma lo que he aprendido al rastrear muchas fuentes diferentes de manera semiconcisa para ver si mis conclusiones son correctas.
  2. Haga un par de preguntas sobre los subprocesos y el rendimiento de Node para las que no he podido obtener respuestas exactas a partir de mi investigación.

Node tiene una arquitectura de manejo de eventos asíncrono y de un solo subproceso

De un solo subproceso : hay un único subproceso de evento que distribuye el trabajo asincrónico (el resultado suele ser una E/S, pero puede ser un cálculo) y realiza la ejecución de devolución de llamada (es decir, el manejo de los resultados del trabajo asincrónico).

  • El hilo de eventos se ejecuta en un "bucle de eventos" infinito realizando los 2 trabajos anteriores; a) manejar solicitudes enviando trabajo asíncrono, y b) notar que los resultados del trabajo asíncrono anterior están listos y ejecutar una devolución de llamada para procesar los resultados.

  • La analogía común aquí es la del tomador de pedidos de un restaurante: el hilo del evento es un camarero súper rápido que toma pedidos (solicitudes de servicios) del comedor y los entrega a la cocina para que los preparen (envía trabajo asincrónico), pero también se da cuenta. cuando la comida está lista (resultados asíncronos) y la devuelve a la mesa (ejecución de devolución de llamada).

  • El camarero no cocina nada; su trabajo consiste en ir y venir del comedor a la cocina lo más rápido posible. Si se atasca tomando un pedido en el comedor, o si se ve obligado a volver a la cocina para preparar una de las comidas, el sistema se vuelve ineficiente y el rendimiento del sistema se ve afectado.

Asíncrono El flujo de trabajo asíncrono resultante de una solicitud (por ejemplo, una solicitud web) es lógicamente una cadena: por ejemplo

   FIRST [ASYNC: read a file, figure out what to get from the database] THEN 
   [ASYNC: query the database] THEN 
   [format and return the result].

El trabajo etiquetado como "ASYNC" arriba es "trabajo de cocina" y "PRIMERO []" y "ENTONCES []" representan la participación del camarero que inicia una devolución de llamada.

Cadenas como esta se representan mediante programación de 3 formas comunes:

  • funciones anidadas/devoluciones de llamada

  • promesas encadenadas con .then()

  • Métodos asíncronos que await() en resultados asíncronos.

Todos estos enfoques de codificación son prácticamente equivalentes, aunque asynch/await parece ser el más limpio y facilita el razonamiento sobre la codificación asincrónica.

Esta es mi imagen mental de lo que está pasando... ¿es correcta? Se agradecen mucho los comentarios!

Preguntas

Mis preguntas se refieren al uso de operaciones asincrónicas compatibles con el sistema operativo, quién realiza realmente el trabajo asincrónico y las formas en que esta arquitectura es más eficaz que la arquitectura "generar un hilo por solicitud" (es decir, múltiples cocineros):

  1. Las bibliotecas de nodos se han diseñado para que sean asíncronas mediante el uso de la biblioteca asíncrona multiplataforma libuv, ¿correcto? ¿La idea aquí es que libuv presenta el nodo (en todas las plataformas) con una interfaz de E/S asíncrona consistente, pero luego utiliza operaciones de E/S asíncronas dependientes de la plataforma bajo el capó? En el caso de que la solicitud de E/S llegue "hasta el final" a una operación asíncrona compatible con el sistema operativo, ¿quién está "haciendo el trabajo" de esperar a que la E/S regrese y active el nodo? ¿Es el kernel, usando un hilo del kernel? Si no, ¿quién? En cualquier caso, ¿cuántas solicitudes puede atender esta entidad?

  2. He leído que libuv también utiliza un grupo de subprocesos (normalmente pthreads, ¿uno por núcleo?) Internamente. ¿Se trata de 'ajustar' operaciones que no "descienden completamente" como asíncronas, de modo que se pueda usar un subproceso para sentarse y esperar una operación sincrónica, de modo que libuv pueda presentar una API asíncrona?

  3. Con respecto al rendimiento, la ilustración habitual que se da para explicar el aumento de rendimiento que puede proporcionar una arquitectura tipo nodo es: imagine el enfoque de subproceso por solicitud (presumiblemente más lento y grueso): hay latencia, CPU y sobrecarga de memoria para generar un montón de subprocesos que simplemente están sentados esperando que se complete la E/S (incluso si no están ocupados esperando) y luego los derriban, y el nodo hace que esto desaparezca en gran medida porque utiliza un subproceso de evento de larga duración para enviar E/S asíncronas al sistema operativo/kernel, ¿verdad? Pero al final del día, ALGO está durmiendo en un mutex y se despierta cuando la E/S está lista... ¿es la idea de que si es el núcleo es mucho más eficiente que si es un hilo de usuario? Y finalmente, ¿qué pasa con el caso en el que la solicitud es manejada por el grupo de subprocesos de libuv... esto parece similar al enfoque de subproceso por solicitud excepto por la eficiencia del uso del grupo (evitando la apertura y el desmontaje)? pero en este caso, ¿qué sucede cuando hay muchas solicitudes y el grupo tiene un retraso?... la latencia aumenta y ahora lo estás haciendo peor que el hilo por solicitud, ¿verdad?

sjtitus avatar Mar 05 '18 07:03 sjtitus
Aceptado

Hay buenas respuestas aquí en SO que pueden brindarle una imagen más clara de la arquitectura. Sin embargo, tiene algunas preguntas específicas que pueden responderse.

¿Quién está "haciendo el trabajo" de esperar a que regrese la E/S y active el nodo? ¿Es el kernel, usando un hilo del kernel? Si no, ¿quién? En cualquier caso, ¿cuántas solicitudes puede atender esta entidad?

En realidad, tanto los subprocesos como las E/S asíncronas se implementan sobre la misma primitiva: la cola de eventos del sistema operativo.

Los sistemas operativos multitarea se inventaron para permitir a los usuarios ejecutar múltiples programas en paralelo utilizando un único núcleo de CPU. Sí, en aquel entonces existían sistemas de múltiples núcleos y múltiples subprocesos, pero eran grandes (generalmente del tamaño de dos o tres dormitorios promedio) y costosos (generalmente el costo de una o dos casas promedio). Estos sistemas pueden ejecutar múltiples operaciones en paralelo sin la ayuda de un sistema operativo. Todo lo que necesita es un programa de carga simple (llamado ejecutivo, un sistema operativo primitivo similar a DOS) y puede crear subprocesos en ensamblaje directamente sin la ayuda de un sistema operativo.

Las computadoras más baratas y producidas en masa sólo pueden ejecutar una cosa a la vez. Durante mucho tiempo esto fue aceptable para los usuarios. Sin embargo, las personas que se acostumbraron a los sistemas de tiempo compartido querían más de sus computadoras. Así se inventaron los procesos y los hilos.

Pero a nivel del sistema operativo no hay hilos. El propio sistema operativo proporciona el servicio de subprocesos (bueno... técnicamente PUEDES implementar subprocesos como una biblioteca sin necesidad de soporte del sistema operativo). Entonces, ¿cómo implementa el sistema operativo los subprocesos?

Interrumpe. Es el núcleo de todo procesamiento asincrónico.

Un proceso o subproceso es simplemente un evento que espera ser procesado por la CPU y administrado por el sistema operativo. Esto es posible porque el hardware de la CPU admite interrupciones. Cualquier subproceso o proceso que espera un evento de E/S (del mouse, disco, red, etc.) se detiene, se suspende y se agrega a la cola de eventos y otros procesos o subprocesos se ejecutan durante el tiempo de espera. También hay un temporizador integrado en la CPU que puede activar una interrupción (sorprendentemente, la interrupción se llama interrupción del temporizador). Esta interrupción del temporizador activa el sistema de administración de procesos/hilos del sistema operativo para que aún pueda ejecutar múltiples procesos en paralelo incluso si ninguno de ellos está esperando eventos de E/S.

Este es el núcleo de la multitarea. Este tipo de programación (usando temporizadores e interrupciones) normalmente no se enseña excepto en el diseño de sistemas operativos, programación integrada (donde a menudo es necesario hacer cosas similares a un sistema operativo sin un sistema operativo) y programación en tiempo real.

Entonces, ¿cuál es la diferencia entre E/S asíncronas y procesos?

Son exactamente lo mismo excepto por la API que el sistema operativo expone al programador:

  • Proceso/hilos : Hola programador, imagina que estás escribiendo un programa simple para una sola CPU y finge que tienes control total de la CPU. Adelante, usa mi E/S. Mantendré la ilusión de que controlas la CPU mientras yo me ocupo del lío de ejecutar las cosas en paralelo.

  • E/S asincrónica : ¿Crees que lo sabes mejor que yo? Bien, te dejo agregar detectores de eventos directamente a mi cola interna. Pero no voy a manejar qué función se llama cuando ocurre el evento. Voy a despertar bruscamente su proceso y usted se encargará de todo usted mismo.

En el mundo moderno de las CPU multinúcleo, el sistema operativo todavía realiza este tipo de gestión de procesos porque un sistema operativo moderno típico ejecuta docenas de procesos, mientras que la PC normalmente solo tiene dos o cuatro núcleos. Con las máquinas multinúcleo hay otra diferencia:

  • Proceso/hilos : dado que estoy manejando la cola de procesos por usted, supongo que no le importará si reparto la carga de los subprocesos que me pide que ejecute en varias CPU, ¿verdad? De esta manera dejaré que el hardware haga el trabajo en paralelo.

  • E/S asíncrona : Lo siento, no puedo distribuir todas las diferentes devoluciones de llamada de espera en diferentes CPU porque no tengo idea de qué diablos está haciendo tu código. ¡Un solo núcleo para ti!

He leído que libuv también utiliza un grupo de subprocesos (normalmente pthreads, ¿uno por núcleo?) internamente. ¿Es esto para 'ajustar' las operaciones que no "descienden completamente" como asíncronas?

Sí.

En realidad, hasta donde yo sé, todos los sistemas operativos proporcionan una interfaz de E/S asíncrona lo suficientemente buena como para que no necesite grupos de subprocesos. El lenguaje de programación Tcl ha estado manejando E/S asíncronas como un nodo sin la ayuda de grupos de subprocesos desde los años 80. Pero es muy complicado y no tan simple. Los desarrolladores de Node decidieron que no querían manejar este lío cuando se trata de E/S de disco y simplemente usar la API de archivos de bloqueo con subprocesos, mejor probada.

Pero al final del día, ALGO está durmiendo en un mutex y se despierta cuando la E/S está lista

Espero que mi respuesta a (1) también responda esta pregunta. Pero si quieres saber qué es ese algo, te sugiero que leas acerca de la select()función en C. Si sabes programación en C, te sugiero que intentes escribir un programa TCP/IP sin subprocesos usando select(). Google "seleccione c". Tengo una explicación mucho más detallada de cómo funciona todo esto en el nivel C en otra respuesta: sé que la función de devolución de llamada se ejecuta de forma asincrónica, pero ¿por qué?

¿Qué sucede cuando hay muchas solicitudes y el grupo tiene un trabajo pendiente?... la latencia aumenta y ahora lo estás haciendo peor que el hilo por solicitud, ¿verdad?

Espero que una vez que comprenda mi respuesta a (1), también se dé cuenta de que no hay forma de escapar del trabajo pendiente incluso si usa subprocesos. El hardware realmente no admite subprocesos a nivel de sistema operativo. Los subprocesos de hardware están limitados a la cantidad de núcleos, por lo que a nivel de hardware la CPU es un grupo de subprocesos. La diferencia entre un solo subproceso y varios subprocesos es simplemente que los programas de múltiples subprocesos realmente pueden ejecutar varios subprocesos en paralelo en el hardware, mientras que los programas de un solo subproceso pueden usar solo una CPU.

La única diferencia REAL entre la E/S asíncrona y los programas tradicionales de subprocesos múltiples es la latencia de creación de subprocesos. En este sentido, no hay ninguna ventaja que tengan programas como node.js sobre programas que usan grupos de subprocesos como nginx y apache2.

Sin embargo, debido a la forma en que funciona CGI, programas como node.js seguirán teniendo un mayor rendimiento porque no es necesario reiniciar el intérprete y todos los objetos en su programa para cada solicitud. Es por eso que la mayoría de los lenguajes se han trasladado a marcos web que se ejecutan como un servicio HTTP (como Express.js de node) o algo como FastCGI.


Nota: ¿Realmente quieres saber cuál es el problema con la latencia de creación de subprocesos? A finales de los 90 y principios de los 2000 hubo una prueba comparativa de servidores web. Tcl, un lenguaje notablemente 500% más lento que C en promedio (porque se basa en el procesamiento de cadenas como bash) logró superar a Apache (esto fue antes de Apache2 y desencadenó la re-arquitectura completa que creó Apache2). La razón es simple: tcl tenía una buena API de E/S asíncrona, por lo que es más probable que los programadores utilicen E/S asíncrona. Esto por sí solo venció a un programa escrito en C (no es que C no tenga E/S asíncronas, después de todo, tcl fue escrito en C).

La principal ventaja de node.js sobre lenguajes como Java no es que tenga E/S asíncronas. Es que la E/S asíncrona es generalizada y la API (devoluciones de llamada, promesas) es fácil de usar, por lo que puedes escribir un programa completo usando E/S asíncrona sin necesidad de bajar al ensamblado o C.

Si cree que las devoluciones de llamada son difíciles de usar, le sugiero encarecidamente que intente escribir ese select()programa basado en C.

slebetman avatar Mar 05 '2018 00:03 slebetman