Veinte peticiones consecutivas, veinte conexiones MySQL nuevas. Eso es PHP-FPM haciendo exactamente aquello para lo que fue diseñado: entregarte un proceso limpio y arrasar con todo cuando sale la respuesta. Las mismas veinte peticiones contra FrankenPHP en modo worker abrieron cero conexiones nuevas, porque la que se estableció al arrancar las sirvió todas. La medición viene de un artículo en francés sobre la migración de un sitio Sulu, y es la pieza sobre FrankenPHP más convincente que he leído este año, precisamente porque su cifra estrella es pequeña.
Pequeña, como en 30 a 40 milisegundos recortados de una respuesta que ahora ronda los 155 ms. No el factor diez que ves en las diapositivas de conferencia. Y esa modestia es exactamente la razón para fiarte. El modo worker elimina una sola cosa: el trabajo de arranque que tu framework repite en cada petición. No hace nada por tu SQL, nada por el renderizado de plantillas, nada por tu lógica de dominio. Si una página pasa 400 ms en la base de datos, el modo worker la verá educadamente pasar 400 ms en la base de datos. Quien te prometa más te está vendiendo su arnés de benchmarks.
La objeción obvia es que todo esto ya lo cacheamos. Es cierto, y no cubre la parte cara. OPcache mantiene el bytecode compilado en memoria compartida, así que PHP deja de reparsear tus dos mil ficheros. El contenedor compilado de Symfony en var/cache/prod/ hace que el framework deje de releer YAML. Pero bajo FPM, los objetos construidos a partir de todo ese código cacheado, la instancia del kernel, el contenedor cableado, los metadatos de entidades de Doctrine, la conexión abierta a la base de datos, se destruyen al final de cada petición y se reconstruyen al principio de la siguiente. Varios megabytes de estado preparado, montados y descartados, cientos de veces por minuto. El modo worker mantiene esos objetos vivos a lo largo de cientos de peticiones. Ese es todo el truco, y basta.
Ahora la parte honesta, porque un proceso que recuerda cosas también recuerda tus errores. El back office del informe se rompió de una forma que me parece instructiva: Sulu decide al arrancar el kernel si está sirviendo el sitio público o el admin, un worker congela esa decisión para siempre, y el index.php de serie contiene un exit que mata en seco el bucle del worker. Resultado: /admin devolvía 404 en producción. El arreglo fue enrutar el admin a través de un front controller clásico y dejar en el worker solo el sitio público. La memoria es la otra preocupación, y aquí los números son tranquilizadores más que triunfales: 166 MB poco después de un reinicio, 271 MB tras unas 18 horas de tráfico, con una meseta muy por debajo del límite de 1 GB. Tres salvaguardas hacen posible esa meseta: el runner de Symfony llama a gc_collect_cycles() después de cada petición, FrankenPHP recicla cada worker tras 500 peticiones por defecto (ajustable vía FRANKENPHP_LOOP_MAX), y el límite de memoria del contenedor atrapa lo que se escape. Nadie afirma que tus fugas desaparezcan. El sistema simplemente se niega a dejar que se acumulen para siempre, que es la versión adulta de la promesa.
La trampa operativa es la que tatuaría en cada script de despliegue: frankenphp reload no reemplaza tus workers. Recarga la configuración de Caddy y deja tres procesos sujetando un kernel que apunta a un contenedor de servicios que tu cache:clear acaba de borrar. El sitio responde 500 hasta que alguien ejecuta un reinicio de verdad, docker compose restart php en este montaje. Súmale opcache.validate_timestamps a 0, que evita que PHP haga stat() sobre miles de ficheros por petición pero también implica que las ediciones a mano en el servidor no hacen nada, en silencio. Ambos ajustes solo son seguros porque el despliegue reinicia PHP automáticamente. Oigo la queja de que esto es frágil. Yo le daría la vuelta: si tu proceso todavía depende de editar ficheros en una máquina en producción y cruzar los dedos, el modo worker no creó ese riesgo, solo dejó de taparlo.
Hay una victoria más silenciosa debajo de la historia del worker. FrankenPHP es Kévin Dunglas incrustando el motor de PHP dentro de Caddy, el servidor en Go que Matthew Holt arrancó en 2014, de modo que un solo binario ahora termina TLS, habla desde HTTP/1.1 hasta HTTP/3 y sirve ficheros estáticos. El equipo del informe eliminó nginx por completo, y resultó que el rate limiting de nginx había causado la lentitud que desencadenó toda la investigación. Una configuración que leer en vez de dos sitios donde equivocarse. Mi detalle favorito: assets precomprimidos con brotli, generados una sola vez en el despliegue a calidad máxima. Su hoja de estilos pasó de 88 KB en crudo a 24 KB comprimida al vuelo y a 14 KB precomprimida. Y sí, hay algo satisfactorio en que PHP tome prestada de golpe la maquinaria de concurrencia de Go, goroutines incluidas, sin que nadie tenga que reescribir un controlador.
Lo que más me impresiona es lo que midieron y luego rechazaron. El JIT: ganancia casi nula, porque una aplicación web vive de I/O y de trabajo con strings y arrays, no de bucles numéricos, así que queda apagado. opcache.preload: redundante cuando tres workers residentes ya mantienen el kernel caliente, y arriesgado con la lectura de anotaciones de Sulu. Una segunda caché HTTP en Caddy: descartada, porque la caché de Symfony ya sabe invalidarse sola y una segunda caché significa una segunda ruta de invalidación que mantener honesta. Eso es ingeniería por sustracción, y es más rara de lo que debería.
Así que el balance dice: de unos 190 ms a 155 ms, unos 140 MB de RAM retenidos de forma permanente por tres workers, y una tubería de despliegue que debe reiniciar PHP sin un humano de por medio. Con esta evidencia, yo aceptaría el trato en la mayoría de aplicaciones Symfony o Laravel con memoria de sobra, y lo evitaría en cualquier cosa que se apoye en estáticos que acumulan estado o en llamadas a exit dispersas. Pero sé que mi umbral no es el tuyo. ¿Aparcarías 140 MB de memoria siempre ocupada para ahorrar 35 ms por petición, y si ya has pulsado el interruptor, qué fue lo primero que se rompió cuando tu aplicación de repente recordó la petición anterior?




Comentarios
Aún no hay comentarios — escribe el primero.
Inicia la conversación
Sin cuenta ni contraseña — introduce tu correo y te enviamos un enlace de acceso de un solo uso. ¿Primera vez? Todo se configura automáticamente.
Tu valoración se aplicará automáticamente al iniciar sesión.
Revisa tu bandeja de entrada
Hemos enviado un enlace de acceso a …. Ábrelo en este dispositivo — esta pestaña te conectará automáticamente.
¿No llega nada? Mira en la carpeta de spam — y marca el correo como «No es spam» para que la próxima vez llegue directo.