Cómo funciona el modelo de E / S sin bloqueo único en Node.js


325

No soy un programador de Nodo, pero estoy interesado en cómo funciona el modelo de E / S de un solo hilo sin bloqueo . Después de leer el artículo de comprensión-el-nodo-js-event-loop , estoy realmente confundido al respecto. Dio un ejemplo para el modelo:

c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
     c.end();
    }
);

Que: Cuando hay dos solicitudes A (viene primero) y B ya que solo hay un solo hilo, el programa del lado del servidor manejará la solicitud A en primer lugar: hacer consultas SQL es una declaración inactiva que representa la espera de E / S. Y el programa está atascado en la I/Oespera, y no puede ejecutar el código que representa la página web detrás. ¿El programa cambiará a la solicitud B durante la espera? En mi opinión, debido al modelo de subproceso único, no hay forma de cambiar una solicitud de otra. Pero el título del código de ejemplo dice que todo se ejecuta en paralelo, excepto su código .

(PD: No estoy seguro si malinterpreto el código o no, ya que nunca he usado Node). ¿Cómo cambia Node A a B durante la espera? ¿Y puede explicar el modelo de Nodo de bloqueo único IO de Node de una manera simple? Le agradecería si pudiera ayudarme. :)

Respuestas:


374

Node.js se basa en libuv , una biblioteca multiplataforma que abstrae apis / syscalls para entradas / salidas asíncronas (sin bloqueo) proporcionadas por los sistemas operativos compatibles (Unix, OS X y Windows al menos).

IO asincrónica

En este modelo de programación, la operación de abrir / leer / escribir en dispositivos y recursos (sockets, sistema de archivos, etc.) administrados por el sistema de archivos no bloquea el hilo de llamada (como en el modelo típico síncrono tipo c) y simplemente marca el proceso (en la estructura de datos a nivel de kernel / OS) para ser notificado cuando haya nuevos datos o eventos disponibles. En el caso de una aplicación similar a un servidor web, el proceso es responsable de determinar a qué solicitud / contexto pertenece el evento notificado y procesar la solicitud desde allí. Tenga en cuenta que esto necesariamente significará que estará en un marco de pila diferente del que originó la solicitud al sistema operativo, ya que este último tuvo que ceder al despachador de un proceso para que un solo proceso de subprocesos maneje nuevos eventos.

El problema con el modelo que describí es que no es familiar y difícil de razonar para el programador, ya que no es de naturaleza secuencial. "Debe realizar una solicitud en la función A y manejar el resultado en una función diferente donde los locales de A generalmente no están disponibles".

Modelo de nodo (estilo de paso de continuación y bucle de evento)

Node aborda el problema aprovechando las características del lenguaje javascript para hacer que este modelo tenga un aspecto un poco más sincrónico al inducir al programador a emplear un cierto estilo de programación. Cada función que solicita IO tiene una firma similar function (... parameters ..., callback)y necesita recibir una devolución de llamada que se invocará cuando se complete la operación solicitada (tenga en cuenta que la mayor parte del tiempo se pasa esperando que el sistema operativo indique la finalización, tiempo que puede ser gastado haciendo otro trabajo). El soporte de Javascript para cierres le permite usar variables que ha definido en la función externa (llamada) dentro del cuerpo de la devolución de llamada; esto permite mantener el estado entre las diferentes funciones que invocará el tiempo de ejecución del nodo de forma independiente. Vea también Continuation Passing Style .

Además, después de invocar una función que genera una operación IO, la función de llamada generalmente returncontrolará el bucle de eventos del nodo . Este bucle invocará la siguiente devolución de llamada o función que se programó para su ejecución (muy probablemente porque el SO notificó el evento correspondiente); esto permite el procesamiento concurrente de múltiples solicitudes.

Puede pensar en el bucle de eventos del nodo como algo similar al despachador del núcleo: el núcleo programaría la ejecución de un subproceso bloqueado una vez que se complete su E / S pendiente, mientras que el nodo programará una devolución de llamada cuando se haya producido el evento correspondiente.

Altamente concurrente, sin paralelismo

Como comentario final, la frase "todo se ejecuta en paralelo excepto su código" hace un trabajo decente al capturar el punto en que el nodo permite que su código maneje solicitudes de cientos de miles de sockets abiertos con un solo hilo simultáneamente multiplexando y secuenciando todos sus js lógica en un solo flujo de ejecución (aunque decir que "todo funciona en paralelo" probablemente no sea correcto aquí - ver Concurrencia vs Paralelismo - ¿Cuál es la diferencia? ). Esto funciona bastante bien para los servidores de aplicaciones web, ya que la mayor parte del tiempo se dedica a esperar a la red o al disco (base de datos / sockets) y la lógica no es realmente intensiva en la CPU, es decir: funciona bien para las cargas de trabajo vinculadas a IO .


45
A preguntas de seguimiento: ¿cómo sucede realmente la E / S entonces? Node está haciendo una solicitud al sistema y solicita que se le notifique cuando finalice. Entonces, ¿el sistema ejecuta un subproceso que está haciendo E / S o el sistema también está realizando la E / S de forma asincrónica a nivel de hardware mediante interrupciones? Algo en algún lugar tiene que esperar a que termine la E / S, y eso se bloqueará hasta que se complete y consuma una cierta cantidad de recursos.
Philip

66
Acabo de notar que este comentario de seguimiento es respondido por @ user568109 a continuación, desearía que hubiera una manera de combinar estas dos respuestas.
lfalin

44
Desearía que pudieras escribir una respuesta el doble de tiempo, así que entendería el doble de mejor.
Rafael Eyng

Nodo es compatible en muchos lugares, para el registro. Cuando estaba diseñando firmware para enrutadores MIPS32, Node.JS podía ejecutarse en ellos a través de OpenWRT.
Qix - MONICA FUE MALTRATADA

¿Cómo se puntúa sobre apache? Apache también es capaz de manejar conexiones concurrentes con un hilo separado.
Suhail Gupta

210

Bueno, para dar una perspectiva, déjame comparar node.js con apache.

Apache es un servidor HTTP multiproceso, para cada solicitud que recibe el servidor, crea un subproceso independiente que maneja esa solicitud.

Node.js, por otro lado, está controlado por eventos, manejando todas las solicitudes de forma asíncrona desde un solo hilo.

Cuando A y B se reciben en Apache, se crean dos hilos que manejan las solicitudes. Cada uno maneja la consulta por separado, cada uno espera los resultados de la consulta antes de servir la página. La página solo se sirve hasta que finaliza la consulta. La búsqueda de consultas está bloqueando porque el servidor no puede ejecutar el resto del hilo hasta que reciba el resultado.

En el nodo, c.query se maneja de forma asíncrona, lo que significa que mientras c.query obtiene los resultados para A, salta para manejar c.query para B, y cuando los resultados llegan para A llegan, devuelve los resultados a la devolución de llamada que envía el respuesta. Node.js sabe ejecutar la devolución de llamada cuando finaliza la búsqueda.

En mi opinión, debido a que es un modelo de subproceso único, no hay forma de cambiar de una solicitud a otra.

En realidad, el servidor de nodo hace exactamente eso por usted todo el tiempo. Para hacer cambios, (el comportamiento asincrónico) la mayoría de las funciones que usaría tendrán devoluciones de llamada.

Editar

La consulta SQL se toma de la biblioteca mysql . Implementa el estilo de devolución de llamada, así como el emisor de eventos para poner en cola las solicitudes SQL. No los ejecuta de forma asíncrona, eso es hecho por los hilos internos de libuv que proporcionan la abstracción de E / S sin bloqueo. Los siguientes pasos ocurren para realizar una consulta:

  1. Abra una conexión a db, la conexión en sí se puede hacer de forma asincrónica.
  2. Una vez que db está conectado, la consulta se pasa al servidor. Las consultas se pueden poner en cola.
  3. El bucle principal del evento recibe una notificación de finalización con devolución de llamada o evento.
  4. El bucle principal ejecuta su devolución de llamada / controlador de eventos.

Las solicitudes entrantes al servidor http se manejan de manera similar. La arquitectura interna del hilo es algo como esto:

bucle de eventos node.js

Los subprocesos de C ++ son los libuv que realizan la E / S asíncrona (disco o red). El bucle principal de eventos continúa ejecutándose después de enviar la solicitud al grupo de subprocesos. Puede aceptar más solicitudes, ya que no espera ni duerme. Las consultas SQL / solicitudes HTTP / lecturas del sistema de archivos suceden de esta manera.


16
El diagrama es muy útil.
Anmol Saraf

14
Espera, entonces en tu diagrama tienes el "conjunto de hilos interno de C ++", lo que significa que todas las operaciones de bloqueo de E / S generarán un hilo, ¿verdad? Entonces, si mi aplicación Node funciona con alguna IO para cada solicitud , ¿prácticamente no hay diferencia entre el modelo Node y el modelo Apache? No entiendo esta parte, lo siento.
gav.newalkar

21
@ gav.newalkar No generan un hilo, las solicitudes se ponen en cola. Los subprocesos en el conjunto de subprocesos los procesan. Los hilos no son dinámicos y por solicitud como en Apache. Por lo general, son fijos y difieren de un sistema a otro.
user568109

10
@ user568109 Pero Apache también está utilizando un conjunto de subprocesos ( httpd.apache.org/docs/2.4/mod/worker.html ). Entonces, al final, la diferencia entre una configuración con node.js difiere de una con Apache al frente solo en el lugar donde se encuentra el conjunto de subprocesos, ¿no es así?
Kris

13
Ese diagrama debe estar en la primera página de los documentos oficiales.
bouvierr

52

Node.js usa libuv detrás de escena. libuv tiene un grupo de subprocesos (de tamaño 4 por defecto). Por lo tanto, Node.js usa hilos para lograr concurrencia.

Sin embargo , su código se ejecuta en un solo subproceso (es decir, todas las devoluciones de llamada de las funciones de Node.js se invocarán en el mismo subproceso, el llamado bucle de subproceso o bucle de evento). Cuando la gente dice "Node.js se ejecuta en un solo hilo", en realidad están diciendo "las devoluciones de llamada de Node.js se ejecutan en un solo hilo".


1
Respuesta breve pero clara (y)
Sudhanshu Gaur

1
buena respuesta, agregaría que las E / S ocurren fuera de este evento principal, bucle, subproceso, subproceso de solicitud
Ionut Popa

esa es la respuesta que estaba buscando durante 2 horas, cómo se logró la concurrencia en la aplicación de un solo subproceso
Muhammad Ramzan

sí, es difícil obtener la respuesta del "siguiente nivel". Esto explica dónde se hace realmente el IO (en un grupo de subprocesos en otro lugar)
Oliver Shaw

9

Node.js se basa en el modelo de programación de bucle de eventos. El bucle de eventos se ejecuta en un solo hilo y espera repetidamente eventos y luego ejecuta cualquier controlador de eventos suscrito a esos eventos. Los eventos pueden ser por ejemplo

  • la espera del temporizador ha finalizado
  • el siguiente fragmento de datos está listo para escribirse en este archivo
  • hay una nueva solicitud HTTP nueva en camino

Todo esto se ejecuta en un solo hilo y ningún código JavaScript se ejecuta en paralelo. Mientras estos controladores de eventos sean pequeños y esperen aún más eventos, todo funcionará bien. Esto permite que múltiples solicitudes sean manejadas simultáneamente por un solo proceso Node.js.

(Hay un poco de magia debajo del capó como donde se originan los eventos. Algunos de ellos involucran hilos de trabajo de bajo nivel que se ejecutan en paralelo).

En este caso de SQL, suceden muchas cosas (eventos) entre hacer la consulta de la base de datos y obtener sus resultados en la devolución de llamada . Durante ese tiempo, el bucle de eventos continúa bombeando vida a la aplicación y avanzando otras solicitudes, un pequeño evento a la vez. Por lo tanto, se atienden varias solicitudes simultáneamente.

vista de alto nivel del bucle de eventos

De acuerdo con: "Bucle de eventos de 10,000 pies - concepto central detrás de Node.js" .


5

La función c.query () tiene dos argumentos

c.query("Fetch Data", "Post-Processing of Data")

La operación "Obtener datos" en este caso es una consulta DB, ahora esto puede ser manejado por Node.js generando un subproceso de trabajo y dándole la tarea de realizar la consulta DB. (Recuerde que Node.js puede crear hilos internamente). Esto permite que la función regrese instantáneamente sin demora

El segundo argumento "Postprocesamiento de datos" es una función de devolución de llamada, el marco de nodo registra esta devolución de llamada y es llamado por el bucle de eventos.

Por lo tanto, la declaración c.query (paramenter1, parameter2)volverá instantáneamente, permitiendo que el nodo atienda otra solicitud.

PD: Acabo de empezar a entender el nodo, en realidad quería escribir esto como comentario a @Philip, pero como no tenía suficientes puntos de reputación, lo escribí como respuesta.


3

si lee un poco más: "Por supuesto, en el back-end, hay hilos y procesos para el acceso a la base de datos y la ejecución del proceso. Sin embargo, estos no están expuestos explícitamente a su código, por lo que no puede preocuparse por ellos más que por saber que las interacciones de E / S, por ejemplo, con la base de datos, o con otros procesos, serán asíncronas desde la perspectiva de cada solicitud, ya que los resultados de esos hilos se devuelven a su código a través del bucle de eventos ".

about - "todo se ejecuta en paralelo excepto su código" - su código se ejecuta sincrónicamente, cada vez que invoca una operación asincrónica como esperar IO, el bucle de eventos maneja todo e invoca la devolución de llamada. simplemente no es algo en lo que tengas que pensar.

en su ejemplo: hay dos solicitudes A (viene primero) y B. ejecuta la solicitud A, su código continúa ejecutándose sincrónicamente y ejecuta la solicitud B. el bucle de eventos maneja la solicitud A, cuando finaliza invoca la devolución de llamada de la solicitud A con el resultado, lo mismo pasa con la solicitud B.


3
"Por supuesto, en el backend, hay subprocesos y procesos para el acceso a la base de datos y la ejecución del proceso. Sin embargo, estos no están expuestos explícitamente a su código" - Si tomo de esta frase, entonces no veo ninguna diferencia entre lo que Node do o cualquier marco multiproceso, digamos el Spring Framework de Java, sí. Hay hilos, pero no controlas su creación.
Rafael Eyng

@RafaelEyng Creo que para manejar la serie de solicitudes múltiples, el nodo siempre tendrá un solo hilo para eso. No estoy seguro de si cada devolución de llamada se coloca en una nueva instancia de subprocesos, aparte de otros procesos como db access, pero al menos sabemos que ese nodo no crea instancias de subprocesos cada vez que recibe una solicitud que tendrá que esperar en línea antes del procesamiento (ejecuciones antes la devolución de llamada).
Cold Cerberus

1

De acuerdo, la mayoría de las cosas deberían estar claras hasta ahora ... la parte difícil es el SQL : si en realidad no se está ejecutando en otro hilo o proceso en su totalidad, la ejecución de SQL debe dividirse en pasos individuales (por un ¡Procesador SQL hecho para ejecución asincrónica!), Donde se ejecutan los que no bloquean, y los que bloquean (por ejemplo, la suspensión) en realidad se pueden transferir al núcleo (como una interrupción / evento de alarma) y poner en la lista de eventos para el bucle principal.

Eso significa que, por ejemplo, la interpretación del SQL, etc. se realiza de inmediato, pero durante la espera (almacenada como un evento que vendrá en el futuro por el núcleo en alguna estructura kqueue, epoll, ...; junto con las otras operaciones IO ) el bucle principal puede hacer otras cosas y eventualmente verificar si algo sucedió con esos IO y espera.

Entonces, para reformularlo de nuevo: el programa nunca (se permite) atascarse, las llamadas inactivas nunca se ejecutan. Su deber lo realiza el kernel (escribir algo, esperar que algo venga por la red, esperar que transcurra el tiempo) u otro hilo o proceso. - El proceso Node verifica si el núcleo ha completado al menos una de esas tareas en la única llamada de bloqueo al sistema operativo una vez en cada ciclo de bucle de eventos. Ese punto se alcanza cuando todo se realiza sin bloqueo.

¿Claro? :-)

No se Node. ¿Pero de dónde viene el c.query?


kqueue epoll es para notificaciones de E / S asíncronas escalables en kernel de Linux. Nodo tiene libuv para eso. El nodo está completamente en tierra de usuario. No depende de qué kernel implemente.
user568109

1
@ user568109, libuv es el intermediario de Node. Cualquier marco asíncrono depende (directamente o no) de algún soporte de E / S asíncrono en el núcleo. ¿Entonces?
Robert Siemer

Perdón por la confusion. Las operaciones de socket requieren E / S sin bloqueo del núcleo. Se encarga del manejo asincrónico. Pero la E / S de archivo asíncrono es manejada por el propio libuv. Tu respuesta no dice eso. Trata a ambos como iguales, siendo manejado por el núcleo.
user568109
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.