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 desleep(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.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación