¿Cómo, en general, Node.js maneja 10,000 solicitudes concurrentes?


395

Entiendo que Node.js usa un solo subproceso y un bucle de eventos para procesar solicitudes que solo procesan una a la vez (que no es de bloqueo). Pero aún así, cómo funciona eso, digamos 10,000 solicitudes simultáneas. ¿El bucle de eventos procesará todas las solicitudes? ¿No tomaría eso demasiado tiempo?

No puedo entender (todavía) cómo puede ser más rápido que un servidor web multiproceso. Entiendo que el servidor web multiproceso será más costoso en recursos (memoria, CPU), pero ¿no sería aún más rápido? Probablemente estoy equivocado; explique cómo este subproceso único es más rápido en muchas solicitudes y qué hace normalmente (en un nivel alto) cuando atiende muchas solicitudes como 10,000.

Y también, ¿se escalará bien ese hilo único con esa gran cantidad? Tenga en cuenta que estoy empezando a aprender Node.js.


55
Porque la mayor parte del trabajo (mover datos) no involucra a la CPU.
OrangeDog

55
Tenga en cuenta también que el hecho de que solo haya un subproceso ejecutando Javascript no significa que no haya muchos otros subprocesos que funcionen.
OrangeDog

Esta pregunta es demasiado amplia o es un duplicado de varias otras preguntas.
OrangeDog


Junto con el subproceso único, Node.js hace algo llamado "E / S sin bloqueo". Aquí es donde se hace toda la magia
Anand N

Respuestas:


764

Si tiene que hacer esta pregunta, probablemente no esté familiarizado con lo que hacen la mayoría de las aplicaciones / servicios web. Probablemente esté pensando que todo el software hace esto:

user do an action
       
       v
 application start processing action
   └──> loop ...
          └──> busy processing
 end loop
   └──> send result to user

Sin embargo, no es así como funcionan las aplicaciones web, o incluso cualquier aplicación con una base de datos como back-end. Las aplicaciones web hacen esto:

user do an action
       
       v
 application start processing action
   └──> make database request
          └──> do nothing until request completes
 request complete
   └──> send result to user

En este escenario, el software pasa la mayor parte de su tiempo de ejecución usando un 0% de tiempo de CPU esperando que la base de datos regrese.

Aplicación de red multiproceso:

Las aplicaciones de red multiproceso manejan la carga de trabajo anterior de esta manera:

request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request

Entonces, el hilo pasa la mayor parte de su tiempo usando 0% de CPU esperando que la base de datos devuelva datos. Al hacerlo, tuvieron que asignar la memoria requerida para un subproceso que incluye una pila de programas completamente separada para cada subproceso, etc. Además, tendrían que iniciar un subproceso que, aunque no es tan costoso como iniciar un proceso completo, todavía no es exactamente barato.

Bucle de evento de subproceso único

Dado que pasamos la mayor parte de nuestro tiempo usando 0% de CPU, ¿por qué no ejecutamos un código cuando no usamos CPU? De esa manera, cada solicitud seguirá obteniendo la misma cantidad de tiempo de CPU que las aplicaciones multiproceso, pero no necesitamos iniciar un subproceso. Entonces hacemos esto:

request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response

En la práctica, ambos enfoques devuelven datos con aproximadamente la misma latencia, ya que es el tiempo de respuesta de la base de datos el que domina el procesamiento.

La principal ventaja aquí es que no necesitamos generar un nuevo hilo, por lo que no necesitamos hacer mucho, mucho malloc, lo que nos ralentizaría.

Magia, hilos invisibles

Lo aparentemente misterioso es cómo ambos enfoques anteriores logran ejecutar la carga de trabajo en "paralelo". La respuesta es que la base de datos está enhebrada. Por lo tanto, nuestra aplicación de subproceso único está aprovechando el comportamiento de subprocesos múltiples de otro proceso: la base de datos.

Donde el enfoque de un solo hilo falla

Una aplicación de subproceso único falla mucho si necesita hacer muchos cálculos de CPU antes de devolver los datos. Ahora, no me refiero a un bucle para procesar el resultado de la base de datos. Eso sigue siendo principalmente O (n). Lo que quiero decir es cosas como hacer la transformación de Fourier (codificación mp3, por ejemplo), el trazado de rayos (representación 3D), etc.

Otro escollo de las aplicaciones de subproceso único es que solo utilizará un solo núcleo de CPU. Entonces, si tiene un servidor de cuatro núcleos (no es raro hoy en día), no está utilizando los otros 3 núcleos.

Donde el enfoque multiproceso falla

Una aplicación multiproceso falla mucho si necesita asignar mucha RAM por hilo. Primero, el uso de RAM en sí mismo significa que no puede manejar tantas solicitudes como una aplicación de subproceso único. Peor aún, malloc es lento. La asignación de montones y montones de objetos (que es común para los marcos web modernos) significa que podemos llegar a ser más lentos que las aplicaciones de subprocesos únicos. Aquí es donde node.js generalmente gana.

Un caso de uso que termina empeorando la multiproceso es cuando necesita ejecutar otro lenguaje de script en su hilo. Primero, generalmente necesita malloc todo el tiempo de ejecución para ese idioma, luego necesita malloc las variables utilizadas por su script.

Entonces, si está escribiendo aplicaciones de red en C o go o java, la sobrecarga de subprocesos generalmente no será tan mala. Si está escribiendo un servidor web C para servir PHP o Ruby, entonces es muy fácil escribir un servidor más rápido en javascript o Ruby o Python.

Enfoque híbrido

Algunos servidores web utilizan un enfoque híbrido. Nginx y Apache2, por ejemplo, implementan su código de procesamiento de red como un grupo de subprocesos de bucles de eventos. Cada subproceso ejecuta un bucle de eventos procesando simultáneamente solicitudes de un solo subproceso, pero las solicitudes tienen equilibrio de carga entre múltiples subprocesos.

Algunas arquitecturas de subproceso único también utilizan un enfoque híbrido. En lugar de iniciar múltiples subprocesos desde un solo proceso, puede iniciar múltiples aplicaciones, por ejemplo, 4 servidores node.js en una máquina de cuatro núcleos. Luego, utiliza un equilibrador de carga para distribuir la carga de trabajo entre los procesos.

En efecto, los dos enfoques son imágenes especulares técnicamente idénticas entre sí.


106
Esta es, con mucho, la mejor explicación para el nodo que he leído hasta ahora. Esa "aplicación de subproceso único en realidad está aprovechando el comportamiento de subprocesos múltiples de otro proceso: la base de datos".
Realizó

¿Qué pasa si el cliente está haciendo una solicitud múltiple en el nodo, como por ejemplo obtener un nombre y modificarlo, y decir que estas operaciones empujan al servidor para que lo manejen muy rápido muchos clientes? ¿Cómo podría manejar tal escenario?
Remario

3
@CaspainCaldion Depende de lo que quieras decir con muy rápido y muchos clientes. Como es, node.js puede procesar más de 1000 solicitudes por segundo y la velocidad está limitada solo a la velocidad de su tarjeta de red. Tenga en cuenta que son 1000 solicitudes por segundo, no clientes conectados simultáneamente. Puede manejar los 10000 clientes simultáneos sin problema. El verdadero cuello de botella es la tarjeta de red.
slebetman

1
@Slebetman, la mejor explicación de todas. Pero una cosa, si tengo un algoritmo de aprendizaje automático, que processe algo de información y proporciona resultados en consecuencia, debo usar múltiples aproximación o de un solo hilo enhebrado
Ganesh Karewad

55
Los algoritmos @GaneshKarewad usan CPU, los servicios (base de datos, API REST, etc.) usan E / S. Si la IA es un algoritmo escrito en js, entonces debe ejecutarlo en otro hilo o proceso. Si la IA es un servicio que se ejecuta en otra computadora (como Amazon, Google o IBM AI), utilice una arquitectura de subproceso único.
slebetman

46

Lo que parece estar pensando es que la mayor parte del procesamiento se maneja en el bucle de eventos del nodo. El nodo en realidad explota el trabajo de E / S en subprocesos. Las operaciones de E / S generalmente toman órdenes de magnitud más largas que las operaciones de la CPU, entonces, ¿por qué la CPU espera eso? Además, el sistema operativo ya puede manejar las tareas de E / S muy bien. De hecho, debido a que Node no espera, logra una utilización de CPU mucho mayor.

Por analogía, piense en NodeJS como un camarero que toma los pedidos de los clientes mientras los chefs de E / S los preparan en la cocina. Otros sistemas tienen varios chefs, que toman un pedido de los clientes, preparan la comida, limpian la mesa y solo luego atienden al siguiente cliente.


55
Gracias por la analogía del restaurante! Encuentro analogías y ejemplos del mundo real mucho más fáciles de aprender.
LaVache

13

Entiendo que Node.js usa un solo subproceso y un bucle de eventos para procesar solicitudes que solo procesan una a la vez (que no es de bloqueo).

Podría estar malinterpretando lo que has dicho aquí, pero "uno a la vez" parece que no estás entendiendo completamente la arquitectura basada en eventos.

En una arquitectura de aplicación "convencional" (no controlada por eventos), el proceso pasa mucho tiempo sentado esperando que algo suceda. En una arquitectura basada en eventos como Node.js, el proceso no solo espera, sino que puede continuar con otro trabajo.

Por ejemplo: obtiene una conexión de un cliente, la acepta, lee los encabezados de la solicitud (en el caso de http), luego comienza a actuar sobre la solicitud. Puede leer el cuerpo de la solicitud, generalmente terminará enviando algunos datos al cliente (esto es una simplificación deliberada del procedimiento, solo para demostrar el punto).

En cada una de estas etapas, la mayor parte del tiempo se pasa esperando que lleguen algunos datos desde el otro extremo; el tiempo real dedicado al procesamiento en el subproceso JS principal suele ser bastante mínimo.

Cuando el estado de un objeto de E / S (como una conexión de red) cambia de manera que necesita procesamiento (por ejemplo, los datos se reciben en un socket, un socket se puede escribir, etc.), el hilo principal Node.js JS se despierta con una lista de artículos que necesitan ser procesados.

Encuentra la estructura de datos relevante y emite algún evento en esa estructura que hace que se ejecuten devoluciones de llamada, que procesan los datos entrantes o escriben más datos en un socket, etc. Una vez que se han procesado todos los objetos de E / S que necesitan procesamiento. procesado, el hilo principal Node.js JS esperará nuevamente hasta que se le indique que hay más datos disponibles (o alguna otra operación se ha completado o ha excedido el tiempo de espera).

La próxima vez que se despierte, bien podría deberse a la necesidad de procesar un objeto de E / S diferente, por ejemplo, una conexión de red diferente. Cada vez, se ejecutan las devoluciones de llamada relevantes y luego vuelve a dormir esperando que ocurra algo más.

El punto importante es que el procesamiento de diferentes solicitudes está intercalado, no procesa una solicitud de principio a fin y luego pasa a la siguiente.

En mi opinión, la principal ventaja de esto es que una solicitud lenta (por ejemplo, si está tratando de enviar 1 MB de datos de respuesta a un dispositivo de teléfono móvil a través de una conexión de datos 2G, o está haciendo una consulta de base de datos realmente lenta) " Bloquee los más rápidos.

En un servidor web multiproceso convencional, normalmente tendrá un subproceso para cada solicitud que se maneja, y procesará SOLO esa solicitud hasta que finalice. ¿Qué sucede si tienes muchas solicitudes lentas? Termina con muchos de sus hilos dando vueltas procesando estas solicitudes, y otras solicitudes (que podrían ser solicitudes muy simples que podrían manejarse muy rápidamente) se ponen en cola detrás de ellas.

Hay muchos otros sistemas basados ​​en eventos además de Node.js, y tienden a tener ventajas y desventajas similares en comparación con el modelo convencional.

No diría que los sistemas basados ​​en eventos son más rápidos en cada situación o con cada carga de trabajo: tienden a funcionar bien para las cargas de trabajo vinculadas a E / S, no tan bien para las vinculadas a CPU.


12

Pasos de procesamiento del modelo de bucle de evento de subproceso único

  • Clientes Enviar solicitud al servidor web.

  • El servidor web Node JS mantiene internamente un grupo de subprocesos limitados para proporcionar servicios a las solicitudes del cliente.

  • El servidor web Node JS recibe esas solicitudes y las coloca en una cola. Se conoce como "Cola de eventos".

  • El servidor web del nodo JS tiene internamente un componente, conocido como "bucle de eventos". Por qué obtuvo este nombre es que usa un bucle indefinido para recibir solicitudes y procesarlas.

  • Event Loop usa solo un subproceso. Es el corazón principal del modelo de procesamiento de plataforma Node JS.

  • Event Loop verifica que cualquier solicitud de cliente se coloque en Event Queue. Si no es así, espere las solicitudes entrantes indefinidamente.

  • En caso afirmativo, recoja una solicitud de cliente de la cola de eventos

    1. Inicia el proceso de solicitud del cliente
    2. Si esa solicitud del cliente no requiere ninguna operación de bloqueo de E / S, procese todo, prepare la respuesta y envíela de vuelta al cliente.
    3. Si esa solicitud del cliente requiere algunas operaciones de bloqueo de E / S, como interactuar con la base de datos, el sistema de archivos y los servicios externos, seguirá un enfoque diferente
  • Comprueba la disponibilidad de subprocesos del conjunto de subprocesos internos
  • Recoge un hilo y asigna esta solicitud de cliente a ese hilo.
  • Ese subproceso es responsable de tomar esa solicitud, procesarla, realizar operaciones de bloqueo de E / S, preparar la respuesta y enviarla nuevamente al bucle de eventos

    muy bien explicado por @Rambabu Posa para obtener más explicaciones, ve a lanzar este enlace


el diagrama dado en esa publicación de blog parece estar equivocado, lo que han mencionado en ese artículo no es completamente correcto.
rranj

11

Agregando a la respuesta de slebetman: cuando dice que Node.JSpuede manejar 10,000 solicitudes concurrentes, son esencialmente solicitudes sin bloqueo, es decir, estas solicitudes pertenecen principalmente a la consulta de la base de datos.

Internamente, event loopof Node.JSestá manejando un thread pool, donde cada hilo maneja un non-blocking requesty el bucle de eventos continúa escuchando más solicitudes después de delegar el trabajo a uno de los hilos del thread pool. Cuando uno de los hilos completa el trabajo, envía una señal al event loopque ha terminado aka callback. Event loopluego procese esta devolución de llamada y envíe la respuesta de regreso.

Como usted es nuevo en NodeJS, lea más sobre nextTickcómo entender cómo funciona el bucle de eventos internamente. Lea los blogs en http://javascriptissexy.com , fueron realmente útiles para mí cuando comencé con JavaScript / NodeJS.


2

Añadiendo a la respuesta de slebetman para más claridad sobre lo que sucede mientras se ejecuta el código.

El grupo de subprocesos internos en nodeJs solo tiene 4 subprocesos de forma predeterminada. y no es como si toda la solicitud estuviera conectada a un nuevo subproceso del grupo de subprocesos, toda la ejecución de la solicitud ocurre como cualquier solicitud normal (sin ninguna tarea de bloqueo), solo que cada vez que una solicitud tiene una ejecución prolongada o una operación pesada como db llamada, una operación de archivo o una solicitud http, la tarea se pone en cola en el grupo de subprocesos interno que proporciona libuv. Y como nodeJs proporciona 4 subprocesos en el grupo de subprocesos interno de forma predeterminada, cada quinta o siguiente solicitud concurrente espera hasta que un subproceso esté libre y una vez que estas operaciones hayan terminado, la devolución de llamada se empuja a la cola de devolución de llamada. y es recogido por el bucle de eventos y devuelve la respuesta.

Ahora aquí viene otra información que no es una sola cola de devolución de llamada, hay muchas colas.

  1. NextTick queue
  2. Cola de micro tareas
  3. Cola de temporizadores
  4. Cola de devolución de llamada de E / S (solicitudes, operaciones de archivo, operaciones de base de datos)
  5. Cola de encuesta IO
  6. Comprobar la cola de fase o SetImmediate
  7. cola de controladores cercanos

Cada vez que llega una solicitud, el código se ejecuta en este orden de devoluciones de llamada en cola.

No es como cuando hay una solicitud de bloqueo que se adjunta a un nuevo hilo. Solo hay 4 hilos por defecto. Entonces, hay otra fila haciendo cola allí.

Siempre que en un código se produce un proceso de bloqueo como la lectura del archivo, llama a una función que utiliza el subproceso del grupo de subprocesos y luego, una vez que se realiza la operación, la devolución de llamada se pasa a la cola respectiva y luego se ejecuta en el orden.

Todo se pone en cola en función del tipo de devolución de llamada y se procesa en el orden mencionado anteriormente.

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.