Siempre me resulta interesante comparar marcos de trabajo con fines similares, para mí es parte de un análisis previo para elegir la tecnología más adecuada en un nuevo proyecto y no provocar una deuda técnica.

Desde que nació Node JS como un entorno de ejecución para Javascript, es probablemente el rey en la generación de proyectos nuevos de lado de backend con funcionamiento asíncrono, por ejemplo manejadores de eventos y websockets. La asincronía es algo que PHP no tiene nativamente y por eso se creó el proyecto de Swoole.

Open Swoole es un marco de red de alto rendimiento basado en un modelo de programación de corrutinas de E/S sin bloqueo, asíncrono y basado en eventos para PHP. Diseñado para construir sistemas de concurrencia a gran escala. Está escrito en C/C++ e instalado como una extensión de PHP.

Con el rendimiento que ofrece Swoole se pueden escribir aplicaciones PHP escalables para servidores web, APIs, servidores de juegos, sistemas de chat, plataformas CMS, microservicios y servicios web en tiempo real, etc. Características que vienen muy bien a ciertos proyectos a gran escala.

Este es un ejercicio de rendimiento y pruebas de estrés para servidores montados en Docker:

LenguajeServidor
PHPPHP server native
PHPSwoole
JavascriptNode Js HTTP module

Las pruebas constan de un algoritmo simple llamado Serie de Fibonacci, que básicamente trata de una secuencia de números naturales infinita, a partir del 0 y el 1 y se van sumando en pares, de manera que cada número es igual a la suma de sus dos anteriores:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55… 

El algoritmo a desarrollar es el resultado de las ecuaciones:

f0 = 0
f1 = 1 
fn = f n − 1 + f n − 2 

Este tipo de ejercicios suelen utilizarse para entender la lógica de la recursividad.

Al final los resultados de la serie se arrojan en formato JSON, simulando una API. El código de los tres servidores se encuentra aquí y lo explicaré detalladamente.

PHP Fibonacci

Básicamente con las fórmulas anteriores escribí el siguiente código, donde si el número es < 1 (0, 1) devolvemos el número, caso contrario, atendemos la formula: fn = f n − 1 + f n − 2.

function fibonacci($n)
{
   if ($n <= 1) {
       return $n;
   }
 
   return fibonacci($n - 1) + fibonacci($n - 2);
}

Ejecutamos un ciclo for que pushea el resultado de llamar a fibonacci 20 veces:

$array = [];
 
for ($i = 0; $i < 20; $i++) {
   $array[] = fibonacci($i);
}

Al final se imprime un JSON:

header('Content-Type: application/json');
 
echo json_encode([
   'status' => 'success',
   'results' => $array
]);

PHP Swoole Fibonacci

Al ser el mismo lenguaje la implementación es similar, en el caso de la función fibonacci seguirá siendo la misma, la diferencia radica en utilizar el servidor que Swoole ofrece:

<?php
 
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
 
$hostname = "0.0.0.0";
$port = 3001;
 
function fibonacci($n)
{
   if ($n <= 1) {
       return $n;
   }
 
   return fibonacci($n - 1) + fibonacci($n - 2);
}
 
$http = new Server($hostname, $port);
 
$http->on("request", function (Request $request, Response $response) {
   $array = [];
 
   for ($i = 0; $i < 20; $i++) {
       $array[] = fibonacci($i);
   }
 
   $response->header("Content-Type", "application/json");
   $response->end(json_encode([
       'status' => 'success',
       'results' => $array
   ]));
});
 
$http->start();

Javascript Fibonacci

En este punto seguimos con un copy & paste con la diferencia de que es un lenguaje distinto:

const http = require("http");

const hostname = "0.0.0.0";
const port = 3000;

const fibonacci = (n) => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
};

const server = http.createServer((req, res) => {
  const array = [];

  for (let i = 0; i < 20; i++) {
    array.push(fibonacci(i));
  }

  res.statusCode = 200;
  res.setHeader("Content-Type", "application/json");
  res.end(
    JSON.stringify({
      status: "success",
      results: array,
    })
  );
});

server.listen(port, hostname);

Es un algoritmo simple pero increbantable

Pruebas de estrés

Para la ejecución de los benchmark utilice una herramienta de benchmarking http llamada wrk que arroja resultados impresionantes.

Como mencioné anteriormente el repositorio de código de estás pruebas esta aquí.

Si ya descargaste el repositorio tendrías que levantar los servicios ejecutando:

docker-compose up 
  • Si hiciste una modificación y no ves cambios, intenta ejecutar docker-compose up –build, debido a que levantamos los servicios con docker-compose y no hay un volumen asociado, los archivos de los servidores se copian en tiempo de buildeo a los contendores.

Levantados los servicios tendremos tres servidores:

http://localhost:3000 (Node ks)
http://localhost:3001 (PHP Swoole)
http://localhost:3002 (PHP native)

Podríamos ejecutar las pruebas para cada servidor:

wrk -t2 -c3 -d5s http://0.0.0.0:3000/
wrk -t2 -c3 -d5s http://0.0.0.0:3001/
wrk -t2 -c3 -d5s http://0.0.0.0:3002/

Si no tienes instalado wrk, puedes utilizar docker, por ejemplo hacía google.com:

docker run --rm williamyeh/wrk -t2 -c3 -d5s https://google.com

Daré un poco de contexto para antes de continuar con las pruebas más profundas, wrk recibe los siguientes parámetros:

  • -t2: Esto genera dos subprocesos independientes.
  • -c3: Cada subproceso abrirá 3 conexiones. Cada conexión simula por ejemplo, un navegador visitando la URL indicada.
  • -d5s: La duración de la prueba.

Una prueba con dos subprocesos y 3 conexiones cada uno, durante 5 segundos, no es para nada estresante, para una prueba más profunda use una máquina con las siguientes características:

Ubuntu 20.04
Intel i5 6 núcleos, 12 hilos
32GB RAM

Los parámetros que realice son 4 hilos, 500 conexiones cada uno, durante 30 segundos, los resultados son los siguientes:

NodeJs

docker run --rm williamyeh/wrk -c500 -d30s -t4 http://0.0.0.0:3000
Running 30s test @ http://0.0.0.0:3000
 4 threads and 500 connections
 Thread Stats   Avg      Stdev     Max   +/- Stdev
   Latency    58.90ms    3.79ms 100.24ms   89.52%
   Req/Sec     2.13k   218.13     2.53k    71.67%
 254405 requests in 30.05s, 60.90MB read
Requests/sec:   8466.78
Transfer/sec:      2.03MB

PHP Swoole

docker run --rm williamyeh/wrk -c500 -d30s -t4 http://0.0.0.0:3001
Running 30s test @ http://0.0.0.0:3001
 4 threads and 500 connections
 Thread Stats   Avg      Stdev     Max   +/- Stdev
   Latency    39.17ms    5.80ms 135.92ms   85.55%
   Req/Sec     3.20k   118.85     3.45k    76.34%
 382665 requests in 30.06s, 93.42MB read
Requests/sec:  12728.04
Transfer/sec:      3.11MB

PHP native

docker run --rm williamyeh/wrk -c500 -d30s -t4 http://0.0.0.0:3002
Running 30s test @ http://0.0.0.0:3002
  4 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   137.51ms  273.67ms   2.00s    90.44%
    Req/Sec   264.39    144.28   590.00     63.48%
  29306 requests in 30.06s, 6.93MB read
Requests/sec:    975.05
Transfer/sec:    236.15KB

Para que nos demos una idea apuntar a https://google.com nos arroja lo siguiente:

docker run --rm williamyeh/wrk -c500 -d30s -t4 https://google.com
Running 30s test @ https://google.com
  4 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   184.69ms   32.15ms 941.58ms   97.84%
    Req/Sec   675.31    121.46     0.97k    70.84%
  79723 requests in 30.06s, 53.45MB read
Requests/sec:   2651.84
Transfer/sec:      1.78MB

Los resultados son sencillos de analizar:

Running 30s test @ https://google.com
  4 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
  • Avg: Promedio.
  • Stdev: Desviación estándar.
  • Max: Máximo registro.
  • +/- Stdev: Es el porcentaje de los registros que están igual o por debajo de Stdev.

Esto aplica para los registros de Latency y Req/Sec, los resultados los podriamos analizar:

ServidorAvgStdevMax+/- Stdev
PHP NativeLatency: 137.51ms
Req/Sec: 264.39
Latency: 273.67ms
Req/Sec: 144.28
Latency: 2.00s
Req/Sec: 590.00
Latency: 90.44%
Req/Sec: 63.48%
NodeJsLatency: 58.90ms
Req/Sec: 2.13k
Latency: 3.79ms
Req/Sec: 218.13
Latency: 100.24ms
Req/Sec: 2.53k
Latency: 89.52%
Req/Sec: 71.67%
PHP SwooleLatency: 39.17ms
Req/Sec: 3.20k
Latency: 5.80ms
Req/Sec: 118.85
Latency: 135.92ms
Req/Sec: 3.45k
Latency: 85.55%
Req/Sec: 76.34%

Realmente como suponía PHP nativo quedaría a deber 2s tardó la maxima conexión en responder, la competencia está entre Node Js y Swoole que es muy reñido, pero Swoole gana al otorgar una latencia más baja, el número de peticiones por segundo se incrementa, la desviación estándar nos indica que no son muy alejados los valores del promedio.

Suponía que Swoole tendría un rendimiento bueno y no me decepcionó. Si te llega a la cabeza probar con un servidor encima como Nginx podría ser una buena idea, te invito a intentarlo. ¡Saludos!