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&aacute;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!