
🌐 También en: English · Deutsch · Français
Deep Dive de la serie en torno al Ansible Tour. Nivel: administración Linux avanzada. El tour explica los roles; los deep dives explican la técnica que hay debajo – cómo funciona, por qué está aquí, qué hace exactamente esta instalación con ella, cómo operarla en el día a día y qué otras opciones habría habido.
WordPress y Nextcloud son aplicaciones PHP, y en ambos pods cada petición dinámica acaba en un worker de PHP-FPM (parte 3, parte 4). Cuántos workers hay, cuánto puede correr uno y cuánto código compilado comparten decide lo rápidos que son los sitios y qué pasa bajo carga. Este artículo explica la mecánica, los ajustes elegidos aquí, las mediciones detrás – y dos valores por defecto que limitaban Nextcloud sin que nadie lo notara.
🧭 Cómo funcionan PHP-FPM y OPcache
Maestro, pool, workers
PHP-FPM («FastCGI Process Manager») es un proceso maestro que arranca y vigila procesos worker. Cada worker atiende exactamente una petición a la vez, de principio a fin. nginx, en el mismo pod, sirve él mismo los ficheros estáticos y pasa cada petición .php por FastCGI al puerto 9000. Un pool es un grupo de workers con ajustes propios; ambas aplicaciones usan el único pool www de las imágenes oficiales.
Cuántos workers
pm = static: un número fijo de workers, siempre en marcha.pm = dynamic: se mantienen listos entremin_spare_serversymax_spare_serversworkers inactivos; se arrancan nuevos hastamax_children.pm = ondemand: los workers arrancan solo cuando llega una petición y se paran tras un tiempo de inactividad.
pm.max_children es el verdadero límite de concurrencia: la petición número max_children + 1 espera en la listen queue hasta que se libere un worker. pm.max_requests recicla un worker tras ese número de peticiones, lo que acota el lento crecimiento de memoria de extensiones con fugas.
Dos tipos de límites de tiempo
max_execution_time es el límite propio de PHP y en Linux solo cuenta el tiempo de CPU del script – una petición que espera a la base de datos o a un bloqueo no lo consume. request_terminate_timeout es el límite de FPM sobre el tiempo real transcurrido: al vencer, el maestro mata el worker. Solo el segundo libera un worker colgado.
OPcache: compilar una vez
PHP compila cada script a bytecode antes de ejecutarlo. OPcache guarda ese bytecode en memoria compartida, que usan juntos todos los workers de un pool; un fichero se compila así una vez por pod en lugar de una vez por petición. Tres ajustes lo dimensionan: memory_consumption (el segmento), interned_strings_buffer (cadenas idénticas guardadas una sola vez) y max_accelerated_files (el número de huecos de su tabla hash). Este último se redondea internamente al siguiente número de una lista fija de primos – 10.000 se convierte en 16.229, 20.000 en 32.531. Cuando los huecos o la memoria se llenan, los ficheros siguientes simplemente no se cachean y se compilan en cada petición.
validate_timestamps decide si OPcache comprueba las fechas de modificación de los ficheros (cada revalidate_freq segundos). Desactivado es más rápido, pero entonces un fichero modificado sigue siendo bytecode antiguo hasta que FPM se reinicia.
APCu: una caché para la aplicación
APCu es un segundo segmento de memoria compartida que la propia aplicación llena de datos – Nextcloud guarda ahí búsquedas de apps y de configuración como caché local. Igual que OPcache, vale por pod, no entre pods; todo lo que deba ser coherente entre procesos y pods va a Redis.
🛡️ Por qué está aquí
- Es lo que traen las imágenes oficiales.
wordpress:fpmynextcloud:fpmincluyen PHP-FPM; nginx delante es el montaje documentado. - Concurrencia previsible. Un tope fijo de workers que encaje con el límite de memoria del contenedor es mejor que un servidor que lo acepta todo y luego lo mata el OOM killer.
- Control fino. Tamaño del pool, tiempos límite, página de estado y slow log se ajustan por aplicación, con independencia del servidor web.
🔧 Qué hace este repositorio
| Blog | Nextcloud A (mayor) | Nextcloud B | |
|---|---|---|---|
| PHP | 8.4 | 8.5 | 8.5 |
pm | dynamic | dynamic | dynamic |
max_children | 12 | 30 | 20 |
| start / min spare / max spare | 4 / 2 / 8 | 8 / 4 / 16 | 6 / 3 / 12 |
max_requests | 500 | 500 | 500 |
request_terminate_timeout | 300 s | 3.600 s | 3.600 s |
| Límite del contenedor | 1 GiB, 2 núcleos | 6 GiB, 3 núcleos | 5 GiB, 2 núcleos |
| OPcache | 128 MB, 10.000 ficheros, fechas comprobadas cada 60 s | 256 MB, 20.000 ficheros, fechas sin comprobar | igual |
| APCu | – | 128 MB | 128 MB |
Los ajustes del pool están en un ConfigMap montado como z-monitoring.conf en php-fpm.d/ – la «z» asegura que se lea el último y gane sobre el www.conf de la imagen. Los valores de Nextcloud vienen de variables de host con valores por defecto en la plantilla, de modo que un pequeño host de pruebas y los dos servidores de producción comparten una plantilla.
El valor por defecto que limitaba Nextcloud a cinco
En la primera versión de la pila de Nextcloud el pool no tenía ajustes propios. El contenedor corría por tanto con el valor original de FPM, pm.max_children = 5 – al margen de su límite de memoria de 4 GiB. Clientes de sincronización de escritorio, apps móviles y las llamadas WOPI de Collabora ocupan workers; la sexta petición PHP simultánea esperaba. Se corrigió en julio de 2026 con un pool de 12, luego 20; en octubre Prometheus mostraba picos de 17 workers activos en el servidor mayor, así que el tope original de cinco era muy real. La ronda de ajuste del 16 de octubre de 2026 subió los pools a 30 y 20. La regla: memoria por worker por max_children, más OPcache y APCu, debe quedar bien por debajo del límite del contenedor.
La caché de bytecode demasiado pequeña
La imagen de Nextcloud fija max_accelerated_files = 10000, internamente 16.229 huecos. Nextcloud con sus apps tiene 15.486 ficheros PHP en el servidor mayor y 17.811 en el pequeño. En el pequeño, más de 1.500 ficheros nunca cabían en la caché y se recompilaban en cada petición que los usaba. Desde el 16 de octubre ambos servidores corren con 20.000 (32.531 huecos) y 256 MB. El blog, con 4.452 ficheros PHP, está lejos de su límite.
Fechas sin comprobar – y lo que cuesta
En Nextcloud, validate_timestamps = 0: ningún stat() por fichero y petición. El precio: un fichero modificado no se nota hasta que el pod se reinicia. Las actualizaciones de imagen reinician el pod de todos modos, pero occ upgrade y occ app:update sobrescriben ficheros PHP en su sitio. Por eso el playbook de actualización vuelve a desplegar el pod de Nextcloud una segunda vez después. Para este artículo resultó que a la actualización opcional de apps del playbook principal le faltaba ese segundo reinicio – las apps actualizadas habrían seguido corriendo con su código antiguo. Corregido. El blog mantiene la comprobación de fechas con un intervalo de 60 segundos, porque WordPress actualiza los plugins en su sitio desde su propia página de administración.
Límites duros y el slow log
[www]
pm.status_path = /fpm-status
slowlog = /proc/1/fd/2
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 0
request_terminate_timeout = 3600s ; blog: 300srequest_terminate_timeouttampoco estaba fijado antes del 16 de octubre: una petición colgada en un bloqueo o en un almacenamiento externo retenía su worker hasta que el pod se reiniciaba. 3.600 s en Nextcloud, porque la descarga en flujo de una carpeta puede durar legítimamente muchos minutos; 300 s en el blog.- El slow log va a la salida de error del contenedor y con ello al log del pod. Una traza de pila necesitaría
ptrace, que un contenedor sin capabilities no tiene; la profundidad 0 registra solo la línea «executing too slowly». FPM intenta engancharse igualmente y escribe unfailed to ptraceinofensivo por cada petición lenta – aceptado, porqueSYS_PTRACEdebilitaría el contenedor. - La página de estado no se expone a través de nginx en absoluto; un sidecar
php-fpm_exporteren el mismo pod la lee por FastCGI y entrega las cifras a Prometheus (panel de Grafana 4912).
📊 Medido
| Últimos 7 días | Blog | Nextcloud A | Nextcloud B |
|---|---|---|---|
| Peticiones PHP por día | ~21.700 | ~23.600 | ~16.200 |
| Pico de workers activos | 8 de 12 | 13 de 30 | 9 de 20 |
max_children alcanzado | 0 | 0 | 0 |
| Peticiones lentas (> 5 s) | 0 | 0 | 0 |
| Memoria por worker (RSS / parte propia PSS) | ~78 / ~16 MB | 108–157 / 14–49 MB | 71–116 / 8–27 MB |
| Pico de memoria del contenedor | 538 MiB de 1 GiB | 763 MiB de 6 GiB | 277 MiB de 5 GiB |
| Periodos de CPU limitados | 2,1 % | 2,2 % | 0,2 % |
Tres lecturas. Primero, el RSS de un worker exagera su coste: cuenta en cada worker las páginas compartidas de OPcache y APCu. El PSS reparte las páginas compartidas entre los procesos que las usan y es la cifra honesta – 16 MB por worker del blog, no 78. Segundo, los pools de Nextcloud son ahora generosos: el pico usó menos de la mitad, y los límites de los contenedores quedan muy por encima de los picos. Tercero, el blog tuvo un valor atípico: un único minuto del 16 de octubre con 8 workers activos y 30 peticiones esperando en la cola – una ráfaga de unas 40 peticiones simultáneas. No se conserva ningún log de ese día (la recogida de logs empezó tres días después), así que la causa es desconocida. Fue un minuto en una semana, y desde entonces la cola se ha quedado en cero.
🧰 Operación diaria
# FPM status as the exporter sees it (it talks FastCGI to port 9000 itself)
k3s kubectl -n nextcloud exec deploy/nextcloud -c nginx -- \
curl -s http://127.0.0.1:9253/metrics | grep -E '^phpfpm_(active|idle|listen_queue|max_children)'
# slow requests
k3s kubectl -n nextcloud logs deploy/nextcloud -c nextcloud-fpm | grep -i 'executing too slow'
# memory per worker, honestly (PSS, in MB)
for p in $(pgrep -f '^php-fpm: pool www'); do
awk '/^Pss:/{s+=$2} END{printf "%d ", s/1024}' /proc/$p/smaps_rollup; done; echo
# new PHP code after an in-place update (Nextcloud)
k3s kubectl -n nextcloud rollout restart deployment/nextcloud- Dimensionar los workers. Medir el PSS bajo carga, multiplicarlo por el
max_childrenprevisto, sumar OPcache y APCu y compararlo con el límite del contenedor. Después vigilar «max_children reached» y la listen queue en Grafana; si ambos se quedan en cero, el pool es suficientemente grande. - Vaciar OPcache.
opcache_reset()en la línea de comandos vacía la caché propia de la CLI, no la de FPM. Para FPM la forma fiable es un reinicio – en Kubernetes, un rollout. - Un sitio colgado. Las métricas por proceso del exportador muestran cuánto tiempo lleva ocupado cada worker; junto con el slow log suele ser más rápido que buscar en los logs de acceso.
🆘 Chuleta: síntoma → comando
La versión corta de este artículo para el momento en que algo va mal. Ejecutar en el host como root; <…> son marcadores.
| Síntoma | Comando |
|---|---|
| Todos los workers ocupados, peticiones esperando | k3s kubectl -n nextcloud exec deploy/nextcloud -c nginx -- \ |
| «server reached pm.max_children» | k3s kubectl -n nextcloud logs deploy/nextcloud -c nextcloud-fpm | grep max_children |
| ¿Qué peticiones son lentas? | k3s kubectl -n nextcloud logs deploy/nextcloud -c nextcloud-fpm | grep -i 'executing too slow' |
| 502 Bad Gateway | k3s kubectl -n nextcloud logs deploy/nextcloud -c nginx --tail=30 |
| ¿Cuánta memoria necesita de verdad un worker? | for p in $(pgrep -f '^php-fpm: pool www'); do |
| Sigue corriendo código viejo tras una actualización | k3s kubectl -n nextcloud rollout restart deployment/nextcloud # OPcache: validate_timestamps=0 |
| Lo mismo en el blog | k3s kubectl -n wordpress logs deploy/wordpress -c wordpress-fpm --tail=30 |
🔭 Lo que viene
- PHP 8.5 para el blog en cuanto la imagen oficial de WordPress lo recomiende; Nextcloud ya corre con 8.5.
- El JIT está desactivado. Para aplicaciones web limitadas por E/S como estas aporta poco; sería un experimento con mediciones antes y después, no un valor por defecto.
- Una alerta sobre la cola de escucha: hecha el 21 de octubre de 2026; avisa por correo cuando las peticiones esperan un worker libre durante cinco minutos, algo que no ocurrió nunca en los 14 días medidos.
⚖️ Alternativas – y por qué PHP-FPM
| Opción | Punto fuerte | Por qué no aquí |
|---|---|---|
| Apache con mod_php | El montaje más sencillo, un solo tipo de proceso | PHP vive en cada proceso del servidor web, también en los que sirven ficheros estáticos; sin control de pool separado. |
| FrankenPHP / RoadRunner | Workers de larga vida, la aplicación se queda en memoria | Necesita una aplicación hecha para ello; WordPress y Nextcloud esperan un estado limpio en cada petición. |
| Swoole | PHP asíncrono | No es compatible con estas aplicaciones. |
| PHP-FPM + nginx | Lo que traen las imágenes, control fino, bien conocido | Elegido. |
Nada en este artículo es exótico. Las dos ganancias reales – veinticinco workers más y una caché en la que cabe el código – vinieron de mirar cuáles eran de verdad los valores por defecto, no de un ajuste ingenioso. Los valores por defecto son la suposición de otro sobre tu carga; con PHP-FPM, dos de ellos se equivocaban para Nextcloud por un factor de cuatro.


