Rendimiento: Python vs NodeJS, intentando una comparativa justa (I)
Una semana más dando guerra. Hoy toca un tema que va a levantar ampollas: Python contra NodeJS.
Antes de nada: programo en Python y NodeJS no me entusiasma, pero voy a intentar ser objetivo. Si las pruebas dicen que NodeJS rinde mejor, lo asumiré como un hombre y seguiré con mi vida.
Y un aviso antes de entrar en materia: esta semana empiezan los cursos de Python en Securízame, en Madrid. Desde cero hasta avanzado, y la parte avanzada la doy yo. Toda la información está en cursos.securizame.com/python-avanzado.
De qué va este post
Voy a hacer varias pruebas de carga para ver cuál de los dos lenguajes da más potencia para desarrollo web. Solo rendimiento.
No entro en si uno es más sencillo de programar, en paradigmas ni en nada parecido. Casi todo eso es subjetivo y no es lo que busco aquí.
Qué vamos a comparar
Dos cosas:
- NodeJS con el módulo Express.
- Python con asyncio y el módulo aiohttp.
En Python uso asyncio y aiohttp, y en NodeJS no añado ningún módulo para conseguir asincronía. ¿Por qué? Porque NodeJS está orientado a eventos de nacimiento, igual que a montar servicios web. Forma parte de su ADN.
Python, en su funcionamiento normal, no está orientado a eventos. Para que la comparación sea más justa uso asyncio, el módulo nuevo de Python 3.4 que trae justo eso, y aiohttp, que facilita montar servidores HTTP encima de asyncio.
Nota: en Python hay más librerías que dan asincronía. Me quedo con asyncio porque es la que pretende ser la referencia del lenguaje a día de hoy.
Cómo son las pruebas
Muy simples.
Sitio probado
Un servidor web pequeño que devuelve una página HTML.
Qué se mide
- Cuántas peticiones por segundo atiende cada uno.
- Tiempo total en procesar X peticiones.
- Cuántos clientes concurrentes aguanta.
Código usado
Estos son los fuentes de las pruebas.
Python
import asyncio
from aiohttp import web
@asyncio.coroutine
def home(request):
return web.Response(body=b"""<!DOCTYPE html>
<html lang="es">
<head>
<title>Hello world!</title>
</head>
<body>
<p>Página de prueba</p>
</body>
</html>""")
@asyncio.coroutine
def json_path(request):
return web.json_response({"test": 1, "data": "hello world" })
def main():
app = web.Application()
app.router.add_route('GET', '/', home)
app.router.add_route('GET', '/json', json_path)
web.run_app(app, port=8081, backlog=5000)
if __name__ == '__main__':
main()
NodeJS
const express = require('express');
// Constantes
const PORT = 8080;
// Aplicación
const app = express();
// Rutas
app.get('/', function (req, res) {
res.send(`<!DOCTYPE html>
<html lang="es">
<head>
<title>Hello world!</title>
</head>
<body>
<p>Página de prueba</p>
</body>
</html>`);
});
app.get('/json', function (req, res) {
res.send(JSON.stringify({ test: 1, data: "hello world" }));
});
// Arranque del servidor
app.listen(PORT, backlog=5000);
console.log('Running on http://localhost:' + PORT);
Nota: para que las pruebas salgan bien hay que tocar los parámetros del kernel del sistema. Los valores por defecto se quedan cortos y hay que subirlos para poder atender más conexiones de las que vienen configuradas. Los comandos están aquí:
http://b.oldhu.com/2012/07/19/increase-tcp-max-connections-on-mac-os-x/
Resultados
Tiempo en procesar peticiones
Comando usado: time ab -n NN http://127.0.0.1:PORT/
Donde NN es el número total de peticiones.
Tiempo total en procesar las peticiones
| Peticiones | NodeJS | Python |
|---|---|---|
| 2000 | 0.99 seg | 2.02 seg |
| 5000 | 2.57 seg | 5.49 seg |
| 10000 | 4.81 seg | 12.85 seg |
Peticiones por segundo
| Peticiones | NodeJS | Python |
|---|---|---|
| 2000 | 2072 req/seg | 1005 req/seg |
| 5000 | 1971 req/seg | 915 req/seg |
| 10000 | 2084 req/seg | 779 req/seg |
Tiempo en atender X clientes
Comando usado: time ab -c CC -n 10000 http://127.0.0.1:PORT/
Donde CC es el número de clientes concurrentes.
Nota importante 1
En la librería aiohttp, el paquete web trae una restricción de 128 conexiones concurrentes. No sé si es a propósito o un olvido.
Las pruebas están hechas con esa limitación y también con un parche que he preparado para la librería. El parche son dos líneas de código, se lo he mandado al autor y se puede ver aquí:
https://github.com/KeepSafe/aiohttp/pull/892
Nota importante 2
NodeJS viene configurado con 512 conexiones concurrentes por defecto. Para aguantar más hay que cambiar el valor backlog al llamar a la función listen, así:
app.listen(PORT, backlog=5000);
La documentación oficial está en:
https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback
Ejecución con la configuración por defecto
Es decir: sin el parche de Python y sin tocar el backlog de NodeJS.
| Clientes concurrentes | NodeJS | Python |
|---|---|---|
| 100 | 3.18 seg | 9.82 seg |
| 250 | 3.10 seg | - |
| 500 | 10.75 seg | - |
| 1000 | - | - |
Ejecución con la configuración mejorada
Es decir: con el parche de Python y con el backlog de NodeJS a 5000.
| Clientes concurrentes | NodeJS | Python |
|---|---|---|
| 100 | 3.18 seg | 9.82 seg |
| 250 | 3.10 seg | 10.581 seg |
| 500 | 8.27 seg | 9.90 seg |
| 1000 | 2.89 seg | 9.77 seg |
| 4000 | - | 11.56 seg |
Las celdas con “-“ significan que el servidor rechaza conexiones y no aguanta ese flujo.
Conclusiones
Bueno, los datos son los que son. Que cada cual juzgue. Yo me quedo con dos cosas.
NodeJS rinde bastante más, en general.
Y lo curioso: con 4000 clientes concurrentes NodeJS no es capaz de atenderlos, y Python, a su “chino chano” (poco a poco), sí.
No soy experto en NodeJS. Si alguien sabe más que yo (y no hace falta mucho) y cree que el código está mal o no del todo bien, que lo comente, por favor.
¡Chau!