Prestazioni PHP

Un PHP veloce si regola, non si compra.

Il nostro team lavora con PHP da venticinque anni e sa dove una richiesta perde il suo tempo. Lo troviamo sul vostro server e lo recuperiamo, uno strato alla volta.

Anatomia di una richiesta

Una pagina è una catena di attese: trovare il server, aprire la connessione, mettersi in coda per PHP, compilare, eseguire, interrogare il database, inviare. Passate da uno stato all'altro per vedere quali anelli accorciamo.

Illustrativo: proporzioni, non una misura
  1. Risoluzione DNS Risponde il resolver del visitatore. Uguale in ogni stato: questa parte è la rete del visitatore. Uguale in ogni stato: questa parte è la rete del visitatore.
  2. Connessione e TLS Una configurazione TLS datata, una nuova connessione per ogni file. TLS 1.3 e una sola connessione HTTP/2 riutilizzata per ogni file. TLS 1.3 e una sola connessione HTTP/2 riutilizzata per ogni file.
  3. In attesa di un worker PHP Tutti i worker sono occupati, quindi la richiesta aspetta in coda. Un pool dimensionato sulla memoria: un worker libero è pronto. Non serve alcun worker PHP.
  4. Compilazione degli script PHP legge e compila ogni file a ogni richiesta. OPcache serve gli script compilati dalla memoria. Niente da compilare.
  5. Ricerca di file e classi L'autoloader scorre le directory per ogni classe. Un autoloader con classmap e una cache realpath già calda. Niente da caricare.
  6. Esecuzione del vostro codice Il lavoro proprio dell'applicazione. Quasi uguale: il JIT raramente cambia questo in un sito tipico. Il vostro codice non viene eseguito per questa visita.
  7. Query al database Indici mancanti, e la stessa query su ogni pagina. Indici aggiunti, risposte ripetute conservate in Redis. Nessuna query viene inviata.
  8. Invio della pagina Inviata senza compressione. Compressa prima di lasciare il server. La pagina finita, direttamente dalla cache di pagina.
La rete Lavoro sul nostro server Il primo byte raggiunge il visitatore

Le barre qui sopra sono un disegno. Questo numero è una misura.

< 20 ms Il primo byte di una pagina dal nostro server, misurato sul server stesso, crittografia inclusa.

Il resto del tempo è la distanza tra i nostri server in Germania e te. Quella parte dipende da dove sei, quindi misurala dalla tua connessione.

Misuralo dalla tua connessione

Le impostazioni che eliminano gran parte dell'attesa

Compilare, trovare i file e caricare le classi avviene prima che il vostro codice esegua una sola riga. Queste impostazioni fanno sì che accada una volta sola, non a ogni richiesta.

conf.d/10-opcache.iniUn esempio - i valori si fissano sito per sito
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 ogni file prima di eseguirlo. OPcache conserva il risultato compilato in memoria condivisa, così la richiesta successiva salta quel lavoro. Dimensioniamo la sua memoria e il numero di file sul sito e leggiamo il tasso di successo per verificare che nulla venga scartato.

    In produzione PHP smette di controllare le modifiche di ogni file; ogni deploy svuota la cache da sé.

  2. Precaricamento

    Per un framework, il precaricamento carica i file principali una sola volta all'avvio di PHP e li tiene pronti per ogni richiesta. Una modifica a quei file richiede un riavvio, per questo il riavvio fa parte del deploy.

  3. JIT

    Il JIT trasforma il codice PHP più eseguito in codice macchina. Aiuta il lavoro che tiene occupato il processore - elaborazione di immagini, calcoli, cicli lunghi. Un sito tipico passa il tempo ad aspettare il database e la rete, e lì il JIT cambia poco. Lo attiviamo dove una misura mostra una differenza, non ovunque.

  4. Cache realpath

    Ogni include chiede al disco dove si trovi davvero un file. Una cache realpath più grande tiene quelle risposte in memoria, e questo conta per i framework che caricano centinaia di file per richiesta.

  5. Autoloader di Composer

    Una classmap autoritativa trova ogni classe con una sola ricerca invece di scorrere le directory, e i pacchetti di sviluppo restano fuori dal server.

La manopola di regolazione: PHP-FPM

PHP-FPM mantiene un pool di worker PHP. Troppo pochi e le richieste fanno la coda; troppi e il server esaurisce la memoria e rallenta per tutti. Il pool si regola su ciò che il server ha davvero.

pm = static
pm.max_children = 64

Un numero fisso di worker, sempre attivi

Per un server dedicato a un solo sito molto visitato. Nessun tempo speso ad avviare processi, e la memoria usata è nota in anticipo.

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

Alcuni worker pronti, altri sotto carico

Tiene alcuni worker inattivi in attesa e ne aggiunge altri quando il traffico sale, fino al limite. La scelta abituale per un sito con picchi giornalieri.

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

Worker solo quando arriva una richiesta

Per molti piccoli siti su un solo server: la memoria va ai siti visitati, e la prima richiesta dopo un periodo tranquillo paga un breve avvio.

Come fissiamo pm.max_children

pm.max_children = memoria per PHP ÷ memoria per worker

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

Un esempio di calcolo, non una raccomandazione: i numeri veri vengono dal vostro server, e lasciamo margine per i picchi.

pm.max_requests
Rinnova un worker dopo un numero fissato di richieste, così una lenta perdita di memoria non diventa mai un disservizio.
request_slowlog_timeout
Una richiesta che dura di più scrive il suo stack trace nel log delle lentezze, che indica la funzione da esaminare.
request_terminate_timeout
Ferma una richiesta fuori controllo prima che blocchi un worker per sempre.
pm.status_path
Mostra la coda e quante volte il pool ha toccato il limite - il primo posto in cui guardiamo quando un sito rallenta nelle ore di punta.

Due strade dal web server a PHP

Li gestiamo entrambi e scegliamo in base a ciò che serve al sito. In ogni caso la richiesta più veloce è quella a cui PHP non deve mai rispondere.

LiteSpeed + LSAPI

  1. Visitatore
  2. LiteSpeed
  3. LSCachecache di pagina
  4. LSAPI
  5. lsphp

LiteSpeed comunica con PHP tramite il proprio protocollo LSAPI e conserva pagine intere in LSCache, che WordPress e altre applicazioni possono svuotare esattamente quando il contenuto cambia.

Nginx + PHP-FPM

  1. Visitatore
  2. Nginx
  3. fastcgi_cachecache di pagina
  4. FastCGI
  5. PHP-FPM

Nginx serve da sé i file statici, passa PHP a un pool PHP-FPM tramite un socket locale e può conservare le pagine finite nella sua cache FastCGI per i visitatori non autenticati.

  • Redis per oggetti e sessioni

    Le risposte che una pagina chiede di continuo al database restano in memoria, e le sessioni smettono di toccare il disco.

  • Una cache di pagina intera

    Un visitatore non autenticato riceve una pagina finita senza che PHP o il database vengano interpellati. Carrelli, account e pagamenti ne restano fuori.

  • HTTP/2 e compressione

    Una sola connessione cifrata porta tutti i file della pagina, il testo viene compresso prima di partire e i file statici hanno lunghe durate di cache con nomi versionati.

  • Tempo nel database

    Lo slow query log ed EXPLAIN mostrano quale query aspetta e perché; di solito un indice o una query ripetuta ne sono la parte maggiore.

    Come ottimizziamo i database

Prima misuriamo, poi cambiamo una cosa sola

Ogni modifica si fa a confronto con una misura presa prima, così possiamo mostrarvi cosa ha prodotto. Queste sono le misure.

  • opcache_get_status()Tasso di successo, memoria libera e se gli script vengono espulsi.
  • pm.status_pathLa coda di attesa e quante volte il pool ha raggiunto il limite.
  • slowlogLo stack trace di ogni richiesta durata troppo.
  • slow_query_log + EXPLAINQuali query aspettano, quante righe leggono e quale indice manca.
  • redis-cli INFO statsSuccessi della cache contro mancati, e se le chiavi vengono espulse.
  • curl -w %{time_starttransfer}Il tempo fino al primo byte, misurato allo stesso modo prima e dopo.

Volete l'altra metà del numero - la distanza tra i nostri server e voi? Avvia il test di velocità

Diteci quale pagina è lenta

Inviateci l'indirizzo e dove gira oggi. Prima leggiamo il server, poi vi diciamo cosa cambieremmo e cosa dovrebbe ottenere - e prepariamo il preventivo dopo quella conversazione.