¿Cuántas peticiones puede manejar realmente un servidor Node.js?
¿Cuántas solicitudes puede manejar realmente un servidor Node.js? Pruebas de carga con Express en Heroku y AWS frente a Django, una estimación de capacidad de 40K solicitudes por segundo, y qué ralentiza a las APIs.
Fuentes: How Many Requests Can a Real-World Node.js Server-Side App Handle?
Cualquiera que sea nuevo en Node.js termina haciendo la misma pregunta práctica: ¿cuántas solicitudes puede manejar una aplicación Node.js en producción, y cuántas instancias de servidor se necesitan para un volumen determinado de tráfico? La respuesta depende de muchos más factores que solo el framework.
Un objetivo al que aspirar
Se dice que la plataforma de diseño PicsArt procesa alrededor de 40,000 solicitudes por segundo, con un backend compuesto en su mayor parte por Node.js.
Esa cifra es enorme, y depende de numerosos factores además del servidor backend. En este ejercicio, esos otros factores se dejan de lado, manteniendo únicamente el hardware y software backend, y en particular Node.js.
Lo que muestran las pruebas de rendimiento sintéticas
El equipo detrás de Fastify publicó una prueba de rendimiento en la que Express maneja aproximadamente 15,000 solicitudes por segundo, mientras que el módulo HTTP básico de Node alcanza unas 70,000.
Un MacBook Pro 15” de 2015 mostró cifras casi idénticas. Incluso 15 K solicitudes por segundo con Express son impresionantes, pero pruebas como esta se realizan con un manejador muy simple, que está lejos de ser una API real. Lo que hace que una API sea “del mundo real” son los datos, cómo se transfieren y cómo interactúan los componentes; por eso, un manejador realista debe comunicarse con una base de datos u otros servicios.
De todos modos, la diferencia entre el formato raw y Express no es muy importante. Los frameworks establecidos como Express suelen ser la mejor opción, ya que en la práctica ninguna única instancia podrá atender 100 K solicitudes por segundo. Por eso se instaló Express para estas pruebas, junto con un manejador pequeño que realiza operaciones variadas.
El manejador de pruebas
El manejador ejecuta seis pasos:
- Insertar un nuevo usuario en la base de datos.
- Obtener 20 usuarios de la base de datos.
- Convertir en cadena y analizar el array de 20 usuarios.
- Convierta recursivamente esos 20 objetos de usuario a formato camelCase.
- Ordene el array.
- Ejecute cálculos simples y operaciones de salida estándar, como
Date.nowyconsole.log, que también consumen recursos de procesamiento.
La respuesta pesaba aproximadamente 12 KB. Cada objeto de usuario tenía un tamaño de 609 bytes:
Resultado 1: Node.js en Heroku con AWS RDS
La aplicación se desplegó en Heroku en hardware con un costo de 25 dólares al mes, y las solicitudes se enviaron a la mayor tasa posible sin aumentar la latencia promedio. La instancia gestionó 31,000 solicitudes en 60 segundos, lo que representa un promedio de 515 solicitudes por segundo.
Resultado 2: Django en Heroku con AWS RDS
Para ver qué haría una tecnología que no fuera de un solo hilo, se creó un manejador equivalente en Django bajo condiciones idénticas y se colocó en el mismo servidor. Los resultados fueron ligeramente peores que los de Node.js, alcanzando 23,000 solicitudes en 60 segundos, es decir, 389 por segundo.
Resultado 3: tres instancias de Node.js en AWS Elastic Beanstalk
Una tercera configuración imitó un despliegue real en producción en AWS. La base de datos y el backend se encontraban en Frankfurt (eu-central-1), la región más cercana, a unos 3,000 km del lugar donde se realizaban las pruebas. Tres instancias de Node.js estaban detrás del equilibrio de carga de AWS. Manejaron 19,000 solicitudes en 10 segundos, lo que equivale a 1,858 solicitudes por segundo. Luego, una prueba de carga continua generó 1.5 millones de solicitudes exitosas en 13 minutos.
Escalar a 40,000 solicitudes por segundo
Una instancia puede manejar alrededor de 600 solicitudes por segundo, así que, ¿cuántas se necesitarían para 40,000? Supongamos que el endpoint promedio en una plataforma grande es el doble de pesado que el procesador de pruebas. Eso da 40,000 / (600 / 2) = 133 instancias distribuidas por todo el mundo para una aplicación a escala global. No es un problema grave.
Los endpoints más pesados podrían requerir hasta 3 veces más recursos, pero el caché y la comunicación basada en eventos pueden recuperar el ancho de banda, por lo que 2 veces es una estimación razonable intermedia.
Qué ralentiza una API
Innumerables factores pueden disminuir la velocidad de una API. Estos son los relacionados con el rendimiento que se encuentran en la mayoría de las aplicaciones web del lado del servidor.
Tamaño de los datos
El volumen de datos almacenado en la base de datos o recuperado de ella es importante, especialmente en las bases de datos relacionales, donde las grandes cantidades de información suelen implicar consultas complejas o un gran esfuerzo por parte del DBMS para trabajar con su memoria en disco. En todos los casos, más bytes significan más tiempo de transmisión. Almacenar archivos en la base de datos constituye un patrón antiestándar; el almacenamiento de objetos es la opción más adecuada. Idealmente, un registro debe mantenerse dentro de unas decenas de kilobytes, generalmente entre 1 y 20 KB.
Distancia a la base de datos
Una mayor distancia entre el backend y la base de datos implica viajes más largos para las consultas y los resultados. Tomemos dos backends, uno en Virginia y otro en Fráncfort, con una sola base de datos en Virginia. La solicitud de un usuario estadounidense llega al backend de EE. UU. en unos 10 a 40 ms, y el trayecto desde el backend hasta la base de datos dura de 1 a 5 ms. Eso está bien. Un usuario alemán llega al backend de Fráncfort en 10 a 20 ms, pero la consulta a Virginia tarda de 120 a 130 ms. Por lo tanto, una aplicación de una sola región debe mantener los datos y la lógica del backend cerca uno del otro, mientras que una aplicación global debe estar preparada para realizar ingeniería compleja para seguir siendo rápida.
Hardware de la base de datos
El hardware también tiene un gran impacto. Una aplicación pequeña debería adquirir hardware de mejor calidad para adaptarse a su tráfico; de lo contrario, será necesario recurrir a esa ingeniería compleja.
Configuración de la base de datos
Las bases de datos vienen con configuraciones predeterminadas para no convertirse en otro problema de gestión, pero a gran escala requieren atención. El número máximo de conexiones es un parámetro importante de vigilar, ya que puede generar cuellos de botella en la aplicación o sobrecargar al servidor de la base de datos; además, las bibliotecas de cliente como los ORMs suelen modificarlo. El tiempo de espera de conexión es otro factor a considerar.
Número de llamadas a la base de datos
Una API real puede realizar decenas de llamadas separadas a la base de datos para completar una tarea, y también puede llamar a otros servicios que a su vez realizan decenas de llamadas cada uno. Un caso notable fue una API de un sistema bancario monolítico que presentaba un tiempo de respuesta de 9 segundos en el entorno de desarrollo en la nube. Su procesador ejecutaba 300 líneas de código, con aproximadamente 30 llamadas a la base de datos y unas 10 llamadas a servicios internos. Esto tiende a ocurrir con un enfoque de “código primero” en lugar de uno de “diseño primero”.
Las soluciones dependen de la lógica empresarial. Las colas y otras técnicas asíncronas pueden trasladar parte del trabajo de la base de datos al fondo, lo que requiere dividir servicios grandes en pequeños. Las consultas combinadas también pueden reducir la cantidad de llamadas.
Gancho
Los ODM y ORM más populares ofrecen gancho o middleware, que resultan útiles para manejar ciertas lógicas. Durante las operaciones en masa, pueden convertirse en un cuello de botella oculto, ya que los complementos como el convertidor de camelCase o pascal_case también consumen recursos de procesamiento y tiempo. Los gancho ayudan, pero lo que se escribe en ellos y dónde se coloca merece una segunda reflexión.
Trabajo intensivo en CPU
Las tareas que requieren mucha potencia de la CPU ralentizan el servidor y también hacen que el usuario final tenga que esperar. En Node.js, muchas de ellas dependen del pool de hilos. Eso no bloquea las operaciones de entrada/salida asíncronas, pero aún así ralentiza la API. Por ejemplo, al hacer clic en “Registrarse”, se espera a que el pool de hilos haga un hash de la contraseña, lo cual lleva más de 100 milisegundos.
Cuellos de botella en el bucle de eventos
Node.js ejecuta el código en el bucle de eventos, en un único hilo, por lo que el rendimiento del código afecta a toda la aplicación. JSON.parse y JSON.stringify pueden ralentizar las APIs, y los bucles anidados y las funciones recursivas son problemáticos. Ignorar cómo funciona Node.js puede hacer que la aplicación quede inaccesible, dependiendo del tamaño de los datos y del tráfico.
Diseño deficiente de la infraestructura
La infraestructura a gran escala no solo presenta más problemas que un sistema monolítico, sino que son diferentes, y a menudo requieren soluciones arquitectónicas complejas. Con muchos componentes, una mala arquitectura afecta gravemente la velocidad de las API y hace que el sistema sea ineficiente en el uso de recursos, por lo que se paga más por algo que podría costar menos. Al diseñar aplicaciones complejas, piense en los problemas que resuelve cada tecnología y conozca las alternativas. Considere cómo podrían cambiar los requisitos en dos o seis meses, y pruebe cada decisión teniendo en cuenta esa suposición. Piense también en el SLA y en los requisitos no funcionales, incluyendo qué ocurre si el tráfico aumenta.
Escala y configuración de los componentes
Los componentes añadidos a la infraestructura pueden ralentizar toda la plataforma. Tomemos RabbitMQ, que, al igual que cualquier tecnología, cuenta con numerosos parámetros de configuración. Los valores predeterminados son adecuados para comenzar, pero mediante investigación y un análisis más detallado de un componente específico, una revisión de la configuración o una refactorización pueden mejorar significativamente la infraestructura. Los componentes también deben escalar junto con el uso para no convertirse en un cuello de botella.