Rendimiento de PHP

Un PHP rápido se ajusta, no se compra.

Nuestro equipo trabaja con PHP desde hace veinticinco años y sabe dónde pierde el tiempo una petición. Lo encontramos en su servidor y lo recuperamos, capa por capa.

Anatomía de una petición

Una página es una cadena de esperas: encontrar el servidor, abrir la conexión, hacer cola para PHP, compilar, ejecutar, consultar la base de datos, enviar. Cambie de estado para ver qué eslabones acortamos.

Ilustrativo: proporciones, no una medición
  1. Consulta DNS La responde el resolvedor del visitante. Igual en todos los estados: esta parte es la red del visitante. Igual en todos los estados: esta parte es la red del visitante.
  2. Conexión y TLS Una configuración TLS antigua, una conexión nueva para cada archivo. TLS 1.3 y una única conexión HTTP/2 reutilizada para cada archivo. TLS 1.3 y una única conexión HTTP/2 reutilizada para cada archivo.
  3. Esperando un worker de PHP Todos los workers están ocupados, así que la petición espera en la cola. Un pool dimensionado según la memoria: hay un worker libre listo. No hace falta ningún worker de PHP.
  4. Compilando los scripts PHP lee y compila cada archivo en cada petición. OPcache sirve los scripts compilados desde memoria. Nada que compilar.
  5. Buscando archivos y clases El autoloader recorre directorios para cada clase. Un autoloader con classmap y una caché de realpath ya caliente. Nada que cargar.
  6. Ejecutando su código El trabajo propio de la aplicación. Casi igual: el JIT rara vez cambia esto en un sitio típico. Su código no se ejecuta en esta visita.
  7. Consultas a la base de datos Faltan índices, y la misma consulta en cada página. Índices añadidos, respuestas repetidas guardadas en Redis. No se envía ninguna consulta.
  8. Enviando la página Se envía sin comprimir. Comprimida antes de salir del servidor. La página terminada, directamente desde la caché de páginas.
La red Trabajo en nuestro servidor El primer byte llega al visitante

Las barras de arriba son un dibujo. Esta cifra es una medición.

< 20 ms El primer byte de una página, desde nuestro servidor, medido en el propio servidor con el cifrado incluido.

El resto del tiempo es la distancia entre nuestros servidores en Alemania y tú. Esa parte depende de dónde estés, así que mídela desde tu propia conexión.

Mídelo desde tu conexión

Los ajustes que eliminan la mayor parte de la espera

Compilar, buscar archivos y cargar clases ocurre antes de que su código ejecute una sola línea. Con estos ajustes ocurre una vez, no en cada petición.

conf.d/10-opcache.iniUn ejemplo: los valores se fijan para cada sitio
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.preload=/srv/app/preload.php
opcache.preload_user=www-data
opcache.jit=tracing
opcache.jit_buffer_size=64M
realpath_cache_size=4096K
realpath_cache_ttl=600
deploy
$ composer install --no-dev --classmap-authoritative
  1. OPcache

    PHP compila cada archivo antes de ejecutarlo. OPcache guarda el resultado compilado en memoria compartida, y la siguiente petición se ahorra ese trabajo. Dimensionamos su memoria y su número de archivos según el sitio y leemos su tasa de aciertos para confirmar que no se descarta nada.

    En producción PHP deja de comprobar si cada archivo ha cambiado; cada despliegue vacía la caché por sí mismo.

  2. Precarga

    En un framework, la precarga carga sus archivos principales una sola vez al arrancar PHP y los mantiene listos para cada petición. Cambiar esos archivos exige un reinicio, así que el reinicio forma parte del despliegue.

  3. JIT

    El JIT convierte el código PHP más usado en código máquina. Ayuda en el trabajo que mantiene ocupado al procesador: procesamiento de imágenes, cálculos, bucles largos. Un sitio típico pasa su tiempo esperando a la base de datos y a la red, y ahí el JIT cambia poco. Lo activamos donde una medición muestra una diferencia, no en todas partes.

  4. Caché de realpath

    Cada include pregunta al disco dónde está realmente un archivo. Una caché de realpath más grande guarda esas respuestas en memoria, algo que cuenta en frameworks que cargan cientos de archivos por petición.

  5. Autoloader de Composer

    Un classmap autoritativo encuentra cada clase en una sola consulta en lugar de recorrer directorios, y los paquetes de desarrollo se quedan fuera del servidor.

El dial de ajuste: PHP-FPM

PHP-FPM mantiene un pool de workers de PHP. Si son pocos, las peticiones hacen cola; si son demasiados, el servidor se queda sin memoria y se ralentiza para todos. El pool se fija según lo que el servidor tiene de verdad.

pm = static
pm.max_children = 64

Un número fijo de workers, siempre activos

Para un servidor dedicado a un sitio con mucho tráfico. No se pierde tiempo arrancando procesos y la memoria que usa se conoce de antemano.

pm = dynamic
pm.max_children = 64
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12

Algunos workers listos, más bajo carga

Mantiene unos pocos workers en espera y añade más cuando sube el tráfico, hasta el límite. La elección habitual para un sitio con picos diarios.

pm = ondemand
pm.max_children = 64
pm.process_idle_timeout = 10s

Workers solo cuando llega una petición

Para muchos sitios pequeños en un servidor: la memoria va a los sitios que se visitan, y la primera petición tras un rato tranquilo paga un breve arranque.

Cómo fijamos pm.max_children

pm.max_children = memoria para PHP ÷ memoria por worker

pm.max_children 64
$ ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$1} END {print s/NR/1024 " MB"}'

Un ejemplo de cálculo, no una recomendación: las cifras reales salen de su servidor, y dejamos margen para los picos.

pm.max_requests
Renueva un worker tras un número fijo de peticiones, para que una fuga lenta de memoria nunca se convierta en una caída.
request_slowlog_timeout
Una petición que tarda más escribe su traza de pila en el registro de lentitud, que señala la función que hay que revisar.
request_terminate_timeout
Detiene una petición descontrolada antes de que retenga un worker para siempre.
pm.status_path
Muestra la cola y cuántas veces el pool llegó a su límite: lo primero que miramos cuando un sitio se ralentiza en horas punta.

Dos caminos del servidor web a PHP

Trabajamos con los dos y elegimos según lo que necesita el sitio. En ambos casos, la petición más rápida es la que PHP nunca tiene que responder.

LiteSpeed + LSAPI

  1. Visitante
  2. LiteSpeed
  3. LSCachecaché de páginas
  4. LSAPI
  5. lsphp

LiteSpeed se comunica con PHP mediante su propio protocolo LSAPI y guarda páginas completas en LSCache, que WordPress y otras aplicaciones pueden purgar justo cuando cambia el contenido.

Nginx + PHP-FPM

  1. Visitante
  2. Nginx
  3. fastcgi_cachecaché de páginas
  4. FastCGI
  5. PHP-FPM

Nginx sirve él mismo los archivos estáticos, pasa PHP a un pool de PHP-FPM por un socket local y puede guardar páginas terminadas en su caché FastCGI para los visitantes sin sesión iniciada.

  • Redis para objetos y sesiones

    Las respuestas que una página pide una y otra vez a la base de datos se quedan en memoria, y las sesiones dejan de tocar el disco.

  • Una caché de página completa

    Un visitante sin sesión iniciada recibe una página terminada sin que se consulte a PHP ni a la base de datos. Carritos, cuentas y pagos quedan fuera de ella.

  • HTTP/2 y compresión

    Una sola conexión cifrada transporta todos los archivos de la página, el texto se comprime antes de salir y los archivos estáticos llevan cachés largas con nombres versionados.

  • Tiempo en la base de datos

    El registro de consultas lentas y EXPLAIN muestran qué consulta espera y por qué; normalmente un índice o una consulta repetida explica casi todo.

    Cómo optimizamos bases de datos

Primero medimos, luego cambiamos una sola cosa

Cada cambio se hace frente a una medición previa, para poder mostrarle lo que consiguió. Estas son las mediciones.

  • opcache_get_status()Tasa de aciertos, memoria libre y si se están expulsando scripts.
  • pm.status_pathLa cola de espera y cuántas veces el pool llegó a su límite.
  • slowlogLa traza de pila de cada petición que tardó demasiado.
  • slow_query_log + EXPLAINQué consultas esperan, cuántas filas leen y qué índice falta.
  • redis-cli INFO statsAciertos frente a fallos de caché, y si se están expulsando claves.
  • curl -w %{time_starttransfer}El tiempo hasta el primer byte, medido igual antes y después.

¿Quiere la otra mitad de la cifra, la distancia entre nuestros servidores y usted? Hacer la prueba de velocidad

Díganos qué página va lenta

Envíenos la dirección y dónde se aloja hoy. Primero revisamos el servidor, después le decimos qué cambiaríamos y qué debería conseguir, y presupuestamos el trabajo tras esa conversación.