¿Cómo es Node.js inherentemente más rápido cuando todavía se basa en subprocesos internamente?


281

Acabo de ver el siguiente video: Introducción a Node.js y todavía no entiendo cómo obtienes 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 caros y solo deben dejarse en manos de los expertos en programación concurrente.

Más tarde, luego 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 llamadas 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 (que actualmente no están en la memoria) y 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 de los subprocesos, pero debajo todavía está usando los subprocesos para procesar las solicitudes de archivos IO (bloqueo).

Entonces, ¿no estás simplemente tomando un problema (enhebrado) y escondiéndolo mientras ese problema aún existe: principalmente múltiples hilos, cambio de contexto, bloqueos muertos ... etc.?

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


14
Me inclino a estar de acuerdo con usted en que el reclamo está algo simplificado en exceso. Creo que la ventaja de rendimiento del nodo se reduce a dos cosas: 1) los hilos reales están contenidos en un nivel bastante bajo, y por lo tanto permanecen limitados en tamaño y número, y la sincronización de hilos se simplifica; 2) La "conmutación" a nivel del sistema operativo select()es más rápida que los intercambios de contexto de subprocesos.
Puntiagudo

Respuestas:


140

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

Piense en uno de los ejemplos que da, donde entra una solicitud y ejecuta una consulta, y luego haga algo con los resultados de eso. Si lo escribe de forma procesal 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ó le hizo crear un nuevo hilo que ejecutaba el código anterior, tendrá un hilo allí, sin hacer nada mientras query() está ejecutando. (Apache, según Ryan, está utilizando 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 fuera realmente inteligente, expresaría el código anterior de una manera en la que el entorno podría funcionar y hacer algo más mientras ejecuta 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 que sea conveniente debido al lenguaje y el entorno, de ahí los puntos sobre los cierres, su código de tal manera que el entorno pueda ser inteligente sobre lo que 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 alguien haya afirmado algo así), pero es nuevo porque la forma en que se expresa es un poco diferente.

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


66
De acuerdo, definitivamente puedo ver cómo esto puede aumentar el rendimiento porque me parece que eres capaz de maximizar tu CPU porque no hay hilos ni pilas de ejecución esperando que IO regrese, así que lo que Ryan ha hecho se encuentra efectivamente Una forma de cerrar todos los huecos.
Ralph Caraveo

34
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 quisquillosa, pero aún así ...
jrtipton

16
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

77
Todavía no estoy seguro de entenderlo completamente. Si consideramos que dentro de una solicitud web, las operaciones de E / S son las que toman la mayor parte del tiempo necesario para procesar la solicitud y si para cada operación de E / S se crea un nuevo subproceso, para 50 solicitudes que vienen en una sucesión muy rápida, lo haremos probablemente tenga 50 hilos 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 es solo su parte IO, pero esa es la parte que lleva la mayor parte del tiempo y hace que el subproceso espere.
Florin Dumitrescu

13
@SystemParadox gracias por señalar eso. Realmente 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 núcleo, no usa hilos mientras realiza operaciones de E / S asíncronas. En cambio, 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 finaliza la operación de E / S 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.
Florin Dumitrescu

32

¡Nota! Esta es una vieja respuesta. Si bien aún es cierto en términos generales, algunos detalles podrían haber cambiado debido al rápido desarrollo de Node en los últimos años.

Está usando hilos porque:

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

Para falsificar IO sin bloqueo, los hilos son necesarios: bloquear IO en un hilo separado. Es una solución fea y causa mucha sobrecarga.

Es aún peor en el nivel de hardware:

  • Con DMA, la CPU descarga asincrónicamente IO.
  • Los datos se transfieren directamente entre el dispositivo IO y la memoria.
  • El núcleo envuelve esto en una llamada de sistema de bloqueo sincrónico.
  • Node.js envuelve la llamada del sistema de bloqueo en un hilo.

Esto es simplemente estúpido e ineficiente. ¡Pero funciona al menos! Podemos disfrutar de Node.js porque oculta los detalles feos y engorrosos detrás de una arquitectura asincrónica basada en eventos.

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

Editar: Discutí esto con un amigo y me dijo que una alternativa a los hilos es sondear con select : especifique un tiempo de espera de 0 y haga IO en los descriptores de archivo devueltos (ahora que se garantiza que no se bloquearán).


¿Qué hay de Windows?
Pacerier

Lo siento, no tengo idea. Solo sé que libuv es la capa neutral de la plataforma para realizar trabajos asincrónicos. 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 puede ser completamente diferente de Linux, pero para nosotros no importa porque libuv hace el trabajo duro por nosotros.
nalply

28

Me temo que estoy "haciendo lo incorrecto" aquí, si es así, bórrame y me disculpo. En particular, no veo cómo creo las pequeñas anotaciones 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á girando los pulgares, está haciendo el trabajo necesario. Si, por otro lado, simplemente está esperando 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 "girar 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 como 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 cuando se manejan solicitudes web independientes, entonces el subproceso es trivialmente simple, simplemente codifica el flujo lineal de cómo manejar un trabajo, y se sienta bien sabiendo que manejará múltiples solicitudes, y cada una Será efectivamente independiente. Personalmente, me aventuraría a 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 de hilo de arriba a abajo. (Pero sí, si tiene que comunicarse entre los subprocesos, la vida se pone muy difícil muy rápido, pero 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 subprocesos De todos modos, eso '

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 qué tan rápido es Node que otros servidores web tienen fallas horribles, sin embargo, tienen fallas de una manera que ilustra indirectamente una ventaja que definitivamente aceptaría para Node (y de ninguna manera es insignificante). Nodo parece que no necesita (ni siquiera permite, en realidad) ajuste. Si tiene un modelo con subprocesos, debe crear suficientes subprocesos para manejar la carga esperada. Haz esto mal y terminarás con un bajo rendimiento. Si hay muy pocos subprocesos, entonces la CPU está inactiva, pero no puede aceptar más solicitudes, crea demasiados subprocesos y desperdiciará la memoria del kernel y, en el caso de un entorno Java, también desperdiciará la memoria principal del montón . Ahora, para Java, desperdiciar el montón es la primera y mejor manera de arruinar el rendimiento del sistema, porque la recolección eficiente de basura (actualmente, esto podría cambiar con G1, pero parece que el jurado todavía está fuera de ese punto a principios de 2013 al menos) depende de tener mucho montón de repuesto. Entonces, está el problema, sintonice con muy pocos subprocesos, tiene CPU inactivas y bajo rendimiento, sintonice 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, en capas sobre el 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 prioridad de prioridad, y en segundo lugar, si aprende a enhebrar en un entorno de Windows, la división de tiempo está allí, le guste o no (por supuesto, esto refuerza el primer punto ; en particular, las primeras versiones de Java utilizaron prioridad de prioridad en implementaciones de Solaris y división de tiempo en Windows. Debido a que la mayoría de los programadores no entendieron y se quejaron de que "el subproceso no funciona en Solaris" cambiaron el modelo a timeslice en todas partes). De todos modos, la conclusión es que la división de tiempo crea cambios de contexto adicionales (y potencialmente innecesarios). Cada cambio de contexto lleva tiempo de CPU, y ese tiempo se elimina efectivamente del trabajo que se puede hacer en el trabajo real en cuestión. Sin embargo, la cantidad de tiempo invertido en el cambio de contexto debido a la división de tiempo no debe ser más que un porcentaje muy pequeño del tiempo total, a menos que esté sucediendo algo bastante extraño, 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 la división de tiempo son ineficientes (y esto no sucede en y ese tiempo se elimina efectivamente del trabajo que se puede hacer en el trabajo real en cuestión. Sin embargo, la cantidad de tiempo invertido en el cambio de contexto debido a la división de tiempo no debe ser más que un porcentaje muy pequeño del tiempo total, a menos que esté sucediendo algo bastante extraño, 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 la división de tiempo son ineficientes (y esto no sucede en y ese tiempo se elimina efectivamente del trabajo que se puede hacer en el trabajo real en cuestión. Sin embargo, la cantidad de tiempo invertido en el cambio de contexto debido a la división de tiempo no debe ser más que un porcentaje muy pequeño del tiempo total, a menos que esté sucediendo algo bastante extraño, 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 la división de tiempo son ineficientes (y esto no sucede enhilos 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 implicados en las declaraciones de rendimiento que a menudo están implicadas para Node.

De todos modos, disculpas por que todo sea largo y divagante, pero realmente siento que hasta ahora, la discusión no ha demostrado 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 pobre) creo que es la explicación real de todas las pruebas que he visto hasta ahora. ], en realidad, cuanto más lo pienso, más me pregunto si la memoria utilizada por un gran número de pilas podría ser significativa aquí. Los tamaños de pila predeterminados para hilos 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 brinda 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 creo 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


2
Un problema con los hilos: necesitan RAM. Un servidor muy ocupado puede ejecutar hasta unos pocos miles de hilos. Node.js evita los hilos y, por lo tanto, es más eficiente. La eficiencia no es ejecutar 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 hilos ahorramos RAM: solo una pila en lugar de unos pocos miles de pilas. Y también guardamos cambios de contexto.
finalmente

3
Pero el nodo no está eliminando 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

1
También el nodo almacena cierres de devoluciones de llamada en RAM, por lo que no puedo ver dónde gana.
Oleksandr Papchenko

@levi Pero nodejs no utiliza el tipo de "un hilo por solicitud". Utiliza un conjunto de hilos de E / S, probablemente para evitar la complicación de usar API de E / S asíncronas (¿y quizás POSIX open()no se puede hacer sin bloqueo?). De esta forma, amortiza cualquier impacto de rendimiento donde 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 la pila. Probablemente pueda atender miles de solicitudes con, digamos, 16 hilos de E / S muy bien.
binki

"Los tamaños de pila predeterminados para los hilos modernos tienden a ser bastante grandes, pero la memoria asignada por un sistema de eventos basado en el cierre 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 deberá mantener todo el árbol de llamadas de la aplicación de subproceso único en la memoria ("emulando pilas", por así decirlo) y podrá limpiarse cuando se libere una hoja de árbol como cierre asociado se "resuelve". Esto incluirá muchas referencias a cosas en el montón que no se pueden recolectar basura y afectará el rendimiento en el momento de la limpieza.
David Tonhofer

14

Lo que no entiendo es el punto de que Node.js todavía está usando hilos.

Ryan usa subprocesos para las partes que están bloqueando (La mayoría de node.js usa IO sin bloqueo) porque algunas partes son increíblemente difíciles de escribir sin bloqueo. Pero creo que Ryan desea tener todo sin bloqueo. En la diapositiva 63 (diseño interno) verá que Ryan usa libev (biblioteca que abstrae la notificación de eventos asíncronos) para el bucle de eventos sin bloqueo . Debido al bucle de eventos, node.js necesita subprocesos menores que reducen el cambio de contexto, el consumo de memoria, etc.


11

Los subprocesos se usan solo para tratar funciones que no tienen una instalación 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, no 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 roscado. Imagina una solicitud de disco, dame staticFile.x, haz 100 solicitudes para ese archivo. Cada solicitud normalmente ocupa un subproceso que recupera ese archivo, es decir, 100 subprocesos.

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

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

Entonces, son 100 hilos versus 1 hilo 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 chico inteligente!

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

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

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

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

Si una consulta ya se estaba ejecutando, otras consultas iguales simplemente saltarían al carro, por lo que puede tener 100 consultas en un solo recorrido de base de datos.


3
Lo de la base de datos es más una cuestión de no esperar la respuesta mientras retiene otras solicitudes (que pueden o no usar la base de datos), sino más bien pedir algo y luego dejar que lo llame cuando vuelva. No creo que los vincule, ya que sería bastante difícil hacer un seguimiento de la respuesta. Además, no creo que haya una interfaz MySQL que le permita mantener múltiples respuestas sin búfer en una conexión (??)
Tor Valamo

Es solo un ejemplo abstracto para explicar cómo los bucles de eventos pueden ofrecer más eficiencia, nodejs no hace nada con los DB sin módulos adicionales;)
BGerrissen

1
Sí, mi comentario fue más hacia las 100 consultas en una sola base de datos de ida y vuelta. : p
Tor Valamo

2
Hola BGerrissen: buena publicación. Entonces, cuando una consulta se está ejecutando, ¿otras consultas similares se "escucharán" como el ejemplo staticFile.X anterior? por ejemplo, 100 usuarios que recuperan la misma consulta, solo se ejecutará una consulta y las otras 99 escucharán la primera. Gracias !
CHAPa

1
Lo estás haciendo sonar como que nodejs memoriza automáticamente llamadas a funciones o algo así. Ahora, como no tiene que preocuparse por la sincronización de 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 que se le pregunta.
binki
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.