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í.