¿Cómo es Node.js intrínsecamente más rápido cuando todavía depende de Threads internamente?

287

Acabo de ver el siguiente video: Introducción a Node.js y todavía no entiendo cómo se obtienen los beneficios de velocidad.

Principalmente, en un momento, Ryan Dahl (creador de Node.js) dice que Node.js se basa en bucles de eventos en lugar de en subprocesos. Los subprocesos son costosos y solo deben dejarse en manos de los expertos en programación concurrente para que los utilicen.

Más tarde, muestra la pila de arquitectura de Node.js que tiene una implementación C subyacente que tiene su propio grupo de subprocesos internamente. Entonces, obviamente, los desarrolladores de Node.js nunca iniciarían sus propios subprocesos o usarían el grupo de subprocesos directamente ... usan devoluciones de llamada asíncronas. Eso lo entiendo.

Lo que no entiendo es el punto de que Node.js todavía está usando subprocesos ... solo está ocultando la implementación, entonces, ¿cómo es esto más rápido si 50 personas solicitan 50 archivos (no actualmente en la memoria)? Bueno, entonces no se requieren 50 subprocesos ?

La única diferencia es que, dado que se administra internamente, el desarrollador de Node.js no tiene que codificar los detalles del subproceso, pero en el fondo sigue utilizando los subprocesos para procesar las solicitudes de archivos IO (bloqueo).

Entonces, ¿no está realmente tomando un problema (subprocesos) y ocultándolo mientras ese problema todavía existe: principalmente múltiples subprocesos, cambio de contexto, interbloqueos, etc.?

Debe haber algún detalle que todavía no entiendo aquí.

2
  • 14
    Me inclino a estar de acuerdo con usted en que la afirmación está un tanto simplificada. Creo que la ventaja de rendimiento del nodo se reduce a dos cosas: 1) los subprocesos reales están todos contenidos en un nivel bastante bajo y, por lo tanto, permanecen restringidos en tamaño y número, y la sincronización de subprocesos se simplifica; 2) El "cambio" a nivel del sistema operativo a través de select()es más rápido que los cambios de contexto de subprocesos.
    Pointy
    2 de septiembre de 2010 a las 18:39
  • Consulte este stackoverflow.com/questions/24796334/…
    veritas
    30 de julio de 2014 a las 8:42
143

En realidad, aquí se combinan algunas cosas diferentes. Pero comienza con el meme de que los hilos son realmente difíciles. Entonces, si son difíciles, es más probable que, al usar subprocesos, 1) se rompan debido a errores y 2) no los use de la manera más eficiente posible. (2) es el que está preguntando.

Piense en uno de los ejemplos que da, donde entra una solicitud y ejecuta alguna consulta, y luego hace algo con los resultados de eso. Si lo escribe de una manera procedimental estándar, el código podría verse así:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

Si la solicitud que ingresó hizo que creara un nuevo hilo que ejecutó el código anterior, tendrá un hilo allí, sin hacer nada mientras se query()está ejecutando. (Apache, según Ryan, está usando un solo hilo para satisfacer la solicitud original, mientras que nginx lo está superando en los casos de los que habla porque no lo es).

Ahora, si fueras realmente inteligente, expresarías el código anterior de una manera en la que el entorno podría apagarse y hacer otra cosa mientras ejecutas la consulta:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

Esto es básicamente lo que está haciendo node.js. Básicamente, está decorando, de una manera conveniente debido al lenguaje y el entorno, de ahí los puntos sobre cierres, su código de tal manera que el entorno puede ser inteligente sobre qué se ejecuta y cuándo. De esa manera, node.js no es nuevo en el sentido de que inventó la E / S asincrónica (no es que nadie haya afirmado algo como esto), pero es nuevo en que la forma en que se expresa es un poco diferente.

Nota: cuando digo que el entorno puede ser inteligente sobre qué se ejecuta y cuándo, específicamente lo que quiero decir es que el hilo que usó para iniciar algunas E / S ahora se puede usar para manejar alguna otra solicitud, o algún cálculo que se puede hacer en paralelo, o iniciar alguna otra E / S en paralelo. (No estoy seguro de que el nodo sea lo suficientemente sofisticado como para comenzar a trabajar más para la misma solicitud, pero entiendes la idea).

12
  • 6
    De acuerdo, definitivamente puedo ver cómo esto puede aumentar el rendimiento porque me parece que puede maximizar su CPU porque no hay subprocesos o pilas de ejecución esperando a que IO regrese, por lo que lo que Ryan ha hecho se encuentra efectivamente una forma de cerrar todas las brechas. 3 de septiembre de 2010 a las 16:02
  • 36
    Sí, lo único que diría es que no es como si hubiera encontrado una manera de cerrar las brechas: no es un patrón nuevo. Lo que es diferente es que está usando Javascript para permitir que el programador exprese su programa de una manera mucho más conveniente para este tipo de asincronía. Posiblemente un detalle quisquilloso, pero aún así ... 3 de septiembre de 2010 a las 18:50
  • dieciséis
    También vale la pena señalar que para muchas de las tareas de E / S, Node usa cualquier API de E / S asíncrona a nivel de kernel que esté disponible (epoll, kqueue, / dev / poll, lo que sea)
    Paul
    15 de septiembre de 2011 a las 15:13
  • 7
    Todavía no estoy seguro de haberlo entendido completamente. Si consideramos que dentro de una solicitud web las operaciones IO son las que toman la mayor parte del tiempo necesario para procesar la solicitud y si para cada operación IO se crea un nuevo hilo, entonces para 50 solicitudes que vienen en una sucesión muy rápida, lo haremos probablemente tenga 50 subprocesos ejecutándose en paralelo y ejecutando su parte IO. La diferencia con los servidores web estándar es que allí toda la solicitud se ejecuta en el subproceso, mientras que en node.js solo su parte IO, pero esa es la parte que toma la mayor parte del tiempo y hace que el subproceso espere. 19 feb 2013 a las 17:56
  • 13
    @SystemParadox gracias por señalar eso. De hecho, hice una investigación sobre el tema últimamente y, de hecho, el problema es que la E / S asíncrona, cuando se implementa correctamente a nivel del kernel, no usa subprocesos mientras realiza operaciones de E / S asíncronas. En su lugar, el subproceso de llamada se libera tan pronto como se inicia una operación de E / S y se ejecuta una devolución de llamada cuando la operación de E / S finaliza y hay un subproceso disponible para ello. Por lo tanto, node.js puede ejecutar 50 solicitudes simultáneas con 50 operaciones de E / S en (casi) paralelo usando solo un hilo si el soporte asíncrono para las operaciones de E / S se implementa correctamente. 9 de mayo de 2013 a las 9:42
32

¡Nota! Ésta es una respuesta antigua. Si bien sigue siendo cierto en términos generales, es posible que algunos detalles hayan cambiado debido al rápido desarrollo de Node en los últimos años.

Está usando subprocesos porque:

  1. La opción O_NONBLOCK de open () no funciona en archivos .
  2. Hay bibliotecas de terceros que no ofrecen E / S sin bloqueo.

Para simular IO sin bloqueo, los subprocesos son necesarios: realice el bloqueo de IO en un subproceso separado. Es una solución desagradable y causa muchos gastos generales.

Es incluso peor a nivel de hardware:

  • Con DMA, la CPU descarga de forma asíncrona IO.
  • Los datos se transfieren directamente entre el dispositivo IO y la memoria.
  • El kernel envuelve esto en una llamada al sistema de bloqueo sincrónica.
  • Node.js envuelve la llamada al sistema de bloqueo en un hilo.

Esto es simplemente estúpido e ineficiente. ¡Pero al menos funciona! Podemos disfrutar de Node.js porque oculta los detalles desagradables y engorrosos detrás de una arquitectura asíncrona impulsada por eventos.

¿Quizás alguien implemente O_NONBLOCK para archivos en el futuro? ...

Editar: hablé de esto con un amigo y me dijo que una alternativa a los hilos es sondear con seleccionar : especificar un tiempo de espera de 0 y hacer IO en los descriptores de archivo devueltos (ahora que se garantiza que no se bloquearán).

2
  • ¿Qué pasa con Windows? 19/02/2017 a las 18:16
  • Lo siento, ni idea. Solo sé que libuv es la capa neutral de plataforma para realizar trabajo asincrónico. Al comienzo de Node no había libuv. Luego se decidió dividir libuv y esto facilitó el código específico de la plataforma. En otras palabras, Windows tiene su propia historia asincrónica que podría ser completamente diferente de Linux, pero para nosotros no importa porque libuv hace el trabajo duro por nosotros.
    nalply
    1 de nov. De 2017 a las 15:06
29

Me temo que estoy "haciendo algo incorrecto" aquí, si es así, elimíneme y le pido disculpas. En particular, no veo cómo creo las pequeñas anotaciones ordenadas que algunas personas han creado. Sin embargo, tengo muchas preocupaciones / observaciones que hacer en este hilo.

1) El elemento comentado en el pseudocódigo en una de las respuestas populares

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

es esencialmente falso. Si el hilo está computando, entonces no está jugando con los pulgares, está haciendo el trabajo necesario. Si, por el contrario, es simplemente la espera de la finalización de IO, entonces es que no utiliza tiempo de CPU, el punto central de la infraestructura de control del hilo en el kernel es que la CPU se encuentra algo útil que hacer. La única forma de "jugar con los pulgares", como se sugiere aquí, sería crear un bucle de sondeo, y nadie que haya codificado un servidor web real es lo suficientemente inepto para hacerlo.

2) "Los hilos son difíciles", solo tiene sentido en el contexto del intercambio de datos. Si tiene subprocesos esencialmente independientes, como es el caso al manejar solicitudes web independientes, entonces el subproceso es trivialmente simple, simplemente codifique el flujo lineal de cómo manejar un trabajo y siéntese sabiendo que manejará múltiples solicitudes, y cada será efectivamente independiente. Personalmente, me atrevería a aventurar que para la mayoría de los programadores, aprender el mecanismo de cierre / devolución de llamada es más complejo que simplemente codificar la versión del hilo de arriba a abajo. (Pero sí, si tiene que comunicarse entre los hilos, la vida se pone realmente difícil muy rápido, pero entonces no estoy convencido de que el mecanismo de cierre / devolución de llamada realmente cambie eso, solo restringe sus opciones, porque este enfoque aún se puede lograr con hilos . De todos modos, eso 'una discusión completamente diferente que realmente no es relevante aquí).

3) Hasta ahora, nadie ha presentado ninguna evidencia real de por qué un tipo particular de cambio de contexto requeriría más o menos tiempo que cualquier otro tipo. Mi experiencia en la creación de núcleos multitarea (a pequeña escala para controladores integrados, nada tan sofisticado como un sistema operativo "real") sugiere que este no sería el caso.

4) Todas las ilustraciones que he visto hasta la fecha que pretenden mostrar cuán más rápido es Node que otros servidores web son horriblemente defectuosas, sin embargo, son defectuosas de una manera que ilustra indirectamente una ventaja que definitivamente aceptaría para Node (y de ninguna manera es insignificante). Nodo parece que necesita (ni siquiera permite, en realidad) ajuste. Si tiene un modelo con subprocesos, debe crear suficientes subprocesos para manejar la carga esperada. Si lo haces mal, terminarás con un rendimiento deficiente. Si hay muy pocos subprocesos, entonces la CPU está inactiva, pero no puede aceptar más solicitudes, crea demasiados subprocesos y desperdiciará memoria del kernel, y en el caso de un entorno Java, también desperdiciará la memoria del montón principal. . Ahora, para Java, desperdiciar montón es la primera y mejor forma de arruinar el rendimiento del sistema,porque la recolección de basura eficiente (actualmente, esto podría cambiar con G1, pero parece que el jurado todavía está deliberando sobre ese punto al menos a principios de 2013) depende de tener mucho montón de reserva. Entonces, ahí está el problema, ajústelo con muy pocos subprocesos, tiene CPU inactivas y un rendimiento deficiente, ajústelo con demasiados y se atasca de otras maneras.

5) Hay otra forma en la que acepto la lógica de la afirmación de que el enfoque de Node "es más rápido por diseño", y es esta. La mayoría de los modelos de subprocesos utilizan un modelo de cambio de contexto dividido en el tiempo, superpuesto al modelo preventivo más apropiado (alerta de juicio de valor :) y más eficiente (no un juicio de valor). Esto sucede por dos razones, en primer lugar, la mayoría de los programadores no parecen entender la preferencia de prioridad, y en segundo lugar, si aprende a usar subprocesos en un entorno de Windows, el corte de tiempo está ahí, le guste o no (por supuesto, esto refuerza el primer punto ; en particular, las primeras versiones de Java usaban la preferencia de prioridad en las implementaciones de Solaris y el reparto de tiempo en Windows. Debido a que la mayoría de los programadores no entendían y se quejaban de que "los subprocesos no funcionan en Solaris".cambiaron el modelo a un intervalo de tiempo en todas partes). De todos modos, la conclusión es que el corte de tiempo crea cambios de contexto adicionales (y potencialmente innecesarios). Cada cambio de contexto requiere tiempo de la CPU, y ese tiempo se elimina efectivamente del trabajo que se puede realizar en el trabajo real en cuestión. Sin embargo, la cantidad de tiempo invertido en el cambio de contexto debido al reparto de tiempo no debería ser más que un porcentaje muy pequeño del tiempo total, a menos que esté sucediendo algo bastante extravagante, y no hay ninguna razón por la que pueda esperar que ese sea el caso en un servidor web simple). Entonces, sí, los cambios de contexto en exceso involucrados en el corte de tiempo son ineficientes (y estos no ocurren eny ese tiempo se elimina efectivamente del trabajo que se puede realizar en el trabajo real en cuestión. Sin embargo, la cantidad de tiempo invertido en el cambio de contexto debido al reparto de tiempo no debería ser más que un porcentaje muy pequeño del tiempo total, a menos que esté sucediendo algo bastante extravagante, y no hay ninguna razón por la que pueda esperar que ese sea el caso en un servidor web simple). Entonces, sí, los cambios de contexto en exceso involucrados en el corte de tiempo son ineficientes (y estos no ocurren eny ese tiempo se elimina efectivamente del trabajo que se puede realizar en el trabajo real en cuestión. Sin embargo, la cantidad de tiempo invertido en el cambio de contexto debido al reparto de tiempo no debería ser más que un porcentaje muy pequeño del tiempo total, a menos que esté sucediendo algo bastante extravagante, y no hay ninguna razón por la que pueda esperar que ese sea el caso en un servidor web simple). Entonces, sí, los cambios de contexto en exceso involucrados en el corte de tiempo son ineficientes (y estos no ocurren enEntonces, sí, los cambios de contexto en exceso involucrados en el corte de tiempo son ineficientes (y estos no ocurren enEntonces, sí, los cambios de contexto en exceso involucrados en el corte de tiempo son ineficientes (y estos no ocurren ensubprocesos del kernel como regla, por cierto) pero la diferencia será un pequeño porcentaje del rendimiento, no el tipo de factores de números enteros que están implícitos en las afirmaciones de rendimiento que a menudo están implícitas para Node.

De todos modos, me disculpo por que todo sea largo y divagante, pero realmente siento que hasta ahora, la discusión no ha probado nada, y me complacería saber de alguien en cualquiera de estas situaciones:

a) una explicación real de por qué Node debería ser mejor (más allá de los dos escenarios que he descrito anteriormente, el primero de los cuales (ajuste deficiente) creo que es la explicación real de todas las pruebas que he visto hasta ahora. ([editar ], en realidad, cuanto más lo pienso, más me pregunto si la memoria utilizada por una gran cantidad de pilas podría ser significativa aquí. Los tamaños de pila predeterminados para los subprocesos modernos tienden a ser bastante grandes, pero la memoria asignada por un el sistema de eventos basado en el cierre sería solo lo que se necesita)

b) un punto de referencia real que realmente le da una oportunidad justa al servidor de subprocesos elegido. Al menos de esa manera, tendría que dejar de creer que las afirmaciones son esencialmente falsas;> ([editar] eso es probablemente más fuerte de lo que pretendía, pero siento que las explicaciones dadas para los beneficios de rendimiento son incompletas en el mejor de los casos, y el los puntos de referencia mostrados no son razonables).

Saludos, Toby

8
  • 2
    Un problema con los subprocesos: necesitan RAM. Un servidor muy ocupado puede ejecutar hasta unos pocos miles de subprocesos. Node.js evita los subprocesos y, por lo tanto, es más eficiente. La eficiencia no radica en ejecutar el código más rápido. No importa si el código se ejecuta en subprocesos o en un bucle de eventos. Para la CPU es lo mismo. Pero al eliminar los subprocesos, ahorramos RAM: solo una pila en lugar de unos pocos miles de pilas. Y también guardamos cambios de contexto.
    nalply
    19 feb 2013 a las 20:15
  • 3
    Pero node no está acabando con los hilos. Todavía los usa internamente para las tareas de IO, que es lo que requieren la mayoría de las solicitudes web.
    levi
    30/03/2014 a las 16:51
  • 1
    Además, el nodo almacena cierres de devoluciones de llamada en RAM, por lo que no puedo ver dónde gana. 20/10/15 a las 11:41
  • @levi Pero nodejs no usa el tipo de cosas "un hilo por solicitud". Utiliza un grupo de subprocesos de E / S, probablemente para evitar la complicación con el uso de API de E / S asincrónicas (¿y tal vez POSIX open()no se puede hacer sin bloqueo?). De esta manera, amortiza cualquier impacto de rendimiento en el que el modelo tradicional fork()/ pthread_create()a pedido tendría que crear y destruir subprocesos. Y, como se menciona en la posdata a), esto también amortiza el problema del espacio de pila. Probablemente pueda atender miles de solicitudes con, digamos, 16 subprocesos de E / S sin problemas.
    binki
    21/08/16 a las 1:09
  • "Los tamaños de pila predeterminados para los subprocesos modernos tienden a ser bastante grandes, pero la memoria asignada por un sistema de eventos basado en cierres sería solo lo que se necesita" Tengo la impresión de que deberían ser del mismo orden. Los cierres no son baratos, el tiempo de ejecución tendrá que mantener todo el árbol de llamadas de la aplicación de un solo subproceso en la memoria ("emulando pilas", por así decirlo) y podrá limpiar cuando se libere una hoja de árbol como cierre asociado. se "resuelve". Esto incluirá muchas referencias a cosas en la pila que no se pueden recolectar como basura y afectarán al rendimiento en el momento de la limpieza. 28/10/2016 a las 16:45
14

What I don't understand is the point that Node.js still is using threads.

Ryan usa subprocesos para las partes que están bloqueando (la mayoría de node.js usa E / S sin bloqueo) porque algunas partes son increíblemente difíciles de escribir sin bloqueo. Pero creo que el deseo de Ryan es tener todo sin bloqueo. En la diapositiva 63 (diseño interno) se ve que Ryan usa libev (biblioteca que abstrae la notificación de eventos asincrónicos) para el bucle de eventos sin bloqueo . Debido al bucle de eventos, node.js necesita subprocesos menores, lo que reduce el cambio de contexto, el consumo de memoria, etc.

0
11

Los subprocesos se utilizan solo para tratar funciones que no tienen una facilidad asincrónica, como stat().

La stat()función siempre está bloqueando, por lo que node.js necesita usar un hilo para realizar la llamada real sin bloquear el hilo principal (bucle de eventos). Potencialmente, nunca se utilizará ningún subproceso del grupo de subprocesos si no necesita llamar a ese tipo de funciones.

7

No sé nada sobre el funcionamiento interno de node.js, pero puedo ver cómo el uso de un bucle de eventos puede superar el manejo de E / S con subprocesos. Imagina una solicitud de disco, dame staticFile.x, haz 100 solicitudes para ese archivo. Cada solicitud normalmente toma un hilo recuperando ese archivo, eso es 100 hilos.

Ahora imagine la primera solicitud que crea un hilo que se convierte en un objeto de editor, las otras 99 solicitudes primero miren si hay un objeto de editor para staticFile.x, si es así, escúchelo mientras está haciendo su trabajo, de lo contrario, inicie un nuevo hilo y, por lo tanto, un nuevo objeto de editor.

Una vez que se realiza el hilo único, pasa staticFile.x a los 100 oyentes y se destruye a sí mismo, por lo que la siguiente solicitud crea un nuevo hilo y un objeto editor.

Así que son 100 subprocesos frente a 1 subproceso en el ejemplo anterior, pero también 1 búsqueda de disco en lugar de 100 búsquedas de disco, la ganancia puede ser bastante fenomenal. ¡Ryan es un tipo inteligente!

Otra forma de verlo es uno de sus ejemplos al comienzo de la película. En lugar de:

pseudo code:
result = query('select * from ...');

Nuevamente, 100 consultas separadas a una base de datos versus ...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

Si una consulta ya estaba en marcha, otras consultas iguales simplemente se subirían al tren, por lo que puede tener 100 consultas en un solo viaje de ida y vuelta a la base de datos.

5
  • 3
    Lo de la base de datos es más una cuestión de no esperar la respuesta mientras se mantienen otras solicitudes (que pueden o no usar la base de datos), sino pedir algo y luego dejar que lo llame cuando regrese. No creo que los vincule, ya que sería bastante difícil hacer un seguimiento de la respuesta. Además, no creo que haya ninguna interfaz MySQL que le permita mantener múltiples respuestas sin búfer en una conexión (??) 2 de septiembre de 2010 a las 20:11
  • Es solo un ejemplo abstracto para explicar cómo los bucles de eventos pueden ofrecer más eficiencia, nodejs no hace nada con DB sin módulos adicionales;) 2/09/10 a las 21:32
  • 1
    Sí, mi comentario fue más hacia las 100 consultas en un solo viaje de ida y vuelta a la base de datos. :pag 3 de septiembre de 2010 a las 8:06
  • 2
    Hola BGerrissen: buen post. Entonces, cuando se está ejecutando una consulta, ¿otras consultas similares "escucharán" como en el ejemplo staticFile.X anterior? por ejemplo, 100 usuarios recuperando la misma consulta, solo se ejecutará una consulta y los otros 99 estarán escuchando la primera? Gracias !
    CHAPa
    22/06/11 a las 10:20
  • 1
    Estás haciendo que parezca que nodejs memoriza automáticamente las llamadas a funciones o algo así. Ahora, debido a que no tiene que preocuparse por la sincronización de la memoria compartida en el modelo de bucle de eventos de JavaScript, es más fácil almacenar en caché las cosas en la memoria de forma segura. Pero eso no significa que nodejs lo haga mágicamente por usted o que este sea el tipo de mejora de rendimiento sobre el que se pregunta.
    binki
    21 de agosto de 2016 a las 1:13