Concurrencia en PHP con Swoole: corutinas, MySQL asíncrono y prácticas seguras

php Concurrencia en PHP con Swoole: corutinas, MySQL asíncrono y prácticas seguras

Concurrencia en PHP con Swoole: corutinas, MySQL asíncrono y prácticas seguras

Si quieres manejar muchas conexiones y tareas I/O-bound en PHP sin procesos o hilos pesados, Swoole y sus corutinas son la forma correcta. Aquí tienes ejemplos prácticos, errores comunes y configuraciones seguras para producción.

Instalación rápida

Instala la extensión y prueba que funciona:

pecl install swoole
php --ri swoole

Servidor HTTP sencillo con corutinas

Este ejemplo muestra un servidor HTTP basado en corutinas. Observa que todo corre en un único proceso, usando corutinas para concurrencia ligera.

<?php
Swoole\Coroutine::run(function () {
    $server = new Swoole\Coroutine\Http\Server('0.0.0.0', 9501, false);

    $server->handle('/', function ($request, $response) {
        // Swoole\Coroutine::getCid() devuelve la id de la coroutine
        $response->end('Hola desde coroutine #' . Swoole\Coroutine::getCid());
    });

    $server->start();
});
?>

Consultas MySQL paralelas (ejemplo práctico)

Ejecuta múltiples consultas concurrentes sin bloquear el event loop usando el cliente MySQL para corutinas.

<?php
Swoole\Coroutine::run(function () {
    $dbConfig = ['host' => '127.0.0.1', 'user' => 'root', 'password' => 'secret', 'database' => 'test'];
    $chan = new Swoole\Coroutine\Channel(10);

    for ($i = 0; $i < 10; $i++) {
        Swoole\Coroutine::create(function () use ($dbConfig, $chan) {
            $db = new Swoole\Coroutine\MySQL();
            if (!$db->connect($dbConfig)) {
                $chan->push(['error' => $db->connect_error ?? $db->error]);
                return;
            }
            // defer cierra la conexión cuando la coroutine termina
            defer(function () use ($db) { $db->close(); });

            $res = $db->query('SELECT id, name FROM users LIMIT 1');
            $chan->push(['result' => $res]);
        });
    }

    $results = [];
    for ($i = 0; $i < 10; $i++) {
        $results[] = $chan->pop();
    }

    var_dump($results);
});
?>

Evita llamadas bloqueantes: hooks y alternativas

Muchas funciones de PHP bloquean (PDO, file_get_contents, sleep, etc.). Convierte I/O bloqueante en no bloqueante con hooks o usa las APIs de Swoole:

  • Activa hooks al inicio para que Swoole convierta llamadas I/O comunes: Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
  • Usa Swoole\Coroutine::sleep(1) en lugar de sleep(1).
  • Para archivos grandes, usa Swoole\Coroutine\System::readFile() o las APIs asíncronas.

Pool de workers simple con Channel

<?php
Swoole\Coroutine::run(function () {
    $poolSize = 5;
    $tasks = new Swoole\Coroutine\Channel(100);
    $results = new Swoole\Coroutine\Channel(100);

    // workers
    for ($i = 0; $i < $poolSize; $i++) {
        Swoole\Coroutine::create(function () use ($tasks, $results) {
            while (true) {
                $task = $tasks->pop(); // bloquea la coroutine, no el proceso
                if ($task === null) break; // señal de shutdown
                // procesar tarea
                $results->push('ok:' . $task);
            }
        });
    }

    // push de tareas
    for ($i = 0; $i < 20; $i++) $tasks->push($i);

    // cerrar pool
    for ($i = 0; $i < $poolSize; $i++) $tasks->push(null);

    // leer resultados
    for ($i = 0; $i < 20; $i++) var_dump($results->pop());
});
?>

Manejo de señales y reinicio ordenado

En producción quieres shutdowns ordenados. Usa señales para apagar el servidor y cerrar recursos:

<?php
Swoole\Coroutine::run(function () {
    $server = new Swoole\Coroutine\Http\Server('0.0.0.0', 9501, false);
    // setup handlers...

    Swoole\Process::signal(SIGTERM, function () use ($server) {
        // detén aceptar nuevas conexiones y finaliza cuando terminen las actuales
        $server->shutdown();
    });

    $server->start();
});
?>

Errores comunes y cómo evitarlos

  • No uses extensiones que no sean coroutine-safe en el worker (p. ej. algunas versiones de PDO o extensiones con bloqueo interno). Prefiere clientes asíncronos de Swoole.
  • No mantengas estado global mutable entre corutinas; usa Swoole\Coroutine::getContext() o estructuras locales.
  • Evita cargar código dinámico (eval, include/require en caliente) sin control; un proceso Swoole es de larga vida.
  • Habilita logs y métricas (latencia, usos de memoria por coroutine) y supervisa leaks de memoria.

Despliegue y rendimiento

  • Ejecuta como proceso CLI supervisado por systemd, docker o un supervisor. Swoole está pensado para procesos de larga vida.
  • Opciones de opcache: si tu proceso es de larga vida, configura opcache.validate_timestamps=0 en producción y usa una estrategia de reload al desplegar (graceful restart).
  • Haz pruebas de carga con wrk o Vegeta y perfila con herramientas compatibles; evita xdebug en producción.

Consejo avanzado: habilita hooks al inicio de tu script para convertir automáticamente llamadas bloqueantes conocidas, y aplica estas opciones en bootstrap:

<?php
Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
// desactiva validación de timestamps en opcache en prod y usa reload controlado
// opcache.validate_timestamps = 0
?>

Audita las librerías que uses (especialmente clientes HTTP, DB y cache). Si una dependencia no es coroutine-safe, reemplázala por su equivalente asíncrono o encapsúlala en un proceso/servicio separado.

Tu siguiente paso: implementa métricas (Prometheus) y trazas para identificar tareas que siguen bloqueando; esa información es la que te permitirá escalar de forma segura.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión