8 Agosto 2026

Quando un Hosting si sporca le mani in un Debug applicativo, il caso reale del tema Woodmart per un sito WooCommerce

Un caso reale di Web Performance in cui un problema apparentemente infrastrutturale si è rivelato un loop applicativo nel tema Woodmart capace di saturare PHP.

Ottimizzazione Applicativa WooCommerce e WordPress

Indice dei contenuti dell'articolo:

Ci sono migrazioni in cui il risultato è esattamente quello che ci si aspetta. Il cliente arriva da un hosting che non riesce più a garantire tempi di risposta adeguati, si sposta su un’infrastruttura più performante d’elite come la nostra, viene applicato uno stack server ottimizzato per WordPress e WooCommerce e il problema scompare.

Bene, questa che sto per raccontati NON è una di quelle storie.

In questo caso il cliente arrivava da un altro hosting lamentando problemi importanti di Web Performance, tempi di caricamento insoddisfacenti e un comportamento del sito difficile da spiegare in rapporto al traffico effettivo. Il progetto era un e-commerce WooCommerce e, al momento della migrazione verso Managed Server S.r.l., la convinzione iniziale era abbastanza semplice: spostando il sito su un’infrastruttura correttamente dimensionata e sul nostro stack ottimizzato lato server, la totalità dei problemi (o buona parte sarebbe scomparsa).

Lo pensava il cliente, (in fondo siamo famosi per questo).

Lo pensavamo anche noi.

Ebbene, sbagliavamo entrambi !

La nuova infrastruttura era decisamente più che adeguata. NGINX, PHP-FPM, MariaDB, cache applicative come REDIS da Object Cache e cache HTTP come la prestigiosa Varnish Cache erano configurati per gestire carichi molto superiori a quelli reali del sito. Eppure qualcosa continuava a non tornare. Soprattutto, continuava a comparire un carico CPU enorme in condizioni in cui, guardando il traffico, quel carico semplicemente non avrebbe dovuto esistere.

Quello che inizialmente sembrava un normale lavoro di migrazione e ottimizzazione sistemistica si è trasformato quindi in qualcosa che, in teoria, dovrebbe stare dall’altra parte della barricata: un vero debug applicativo dentro WordPress, WooCommerce e il tema Woodmart.

Ed è proprio qui che un servizio managed mostra una differenza sostanziale rispetto alla semplice fornitura di hosting. Quando i grafici dicono che il server funziona, il database non è saturo, lo storage non aspetta, la rete non è congestionata e la cache non spiega il fenomeno, non puoi continuare a cambiare parametri di PHP sperando che il problema sparisca.

A un certo punto devi sporcarti le mani.

La migrazione aveva risolto il server, non il problema

L’ambiente sul quale è stato analizzato il problema disponeva di 8 vCPU e 64 GB di RAM, con AlmaLinux 9.8, NGINX, PHP-FPM 8.3.32, MariaDB, WordPress 7.0.3, WooCommerce 10.9.3 e Woodmart 8.1.2. Il catalogo contava 479 prodotti pubblicati e l’installazione utilizzava 28 plugin attivi.

In altre parole, non eravamo davanti a un WooCommerce enorme installato su una VPS sottodimensionata. E non eravamo nemmeno davanti a un server che stava facendo swap o aspettando continuamente lo storage.

Il dato più importante era un altro.

Con un traffico nell’ordine di circa 30 richieste al minuto, contando peraltro anche gli asset statici, il server presentava un load average stabilmente nell’ordine di:

7.70 / 7.83 / 7.90

su una macchina da 8 core ovvero il 100% della CPU.

Ancora più interessante era la distribuzione del tempo CPU:

  • 88,5% user;
  • 1,4% system;
  • 0,0% iowait.

Quel dato vale più di molte supposizioni.

Se una macchina sta aspettando il disco, ci aspettiamo iowait. Se il collo di bottiglia è il database, normalmente vediamo query, lock, attese, I/O, connessioni o comunque evidenze compatibili. Se il problema è di rete abbiamo un’altra famiglia di segnali. Qui invece quasi tutto il tempo veniva consumato in CPU userland.

Il processore stava eseguendo codice.

Tantissimo codice.

E non stava aspettando praticamente nulla.

Andamento del carico osservato durante l’incidente: non normali picchi legati alle visite, ma un plateau prolungato prossimo alla saturazione degli 8 core.

Questa distinzione è fondamentale perché evita uno degli errori più comuni nel troubleshooting delle performance WordPress: interpretare qualunque lentezza come un problema di hosting.

Un hosting può essere lento. Un database può essere configurato male. PHP-FPM può essere sottodimensionato. Lo storage può essere inadatto. La cache può essere assente. Sono tutti problemi reali.

Ma quando sette core stanno facendo calcolo PHP quasi ininterrottamente per un sito che riceve pochissime richieste, aggiungere hardware significa solo permettere al bug di consumare più hardware.

Il primo errore di prospettiva: pensare che bastasse la cache

In Managed Server utilizziamo normalmente più livelli di caching proprio perché su WordPress e WooCommerce è fondamentale evitare che ogni accesso debba ricostruire l’intera pagina passando da PHP e database.

Nel caso analizzato erano coinvolti diversi livelli: proxy cache NGINX in configurazione microcache, Varnish e object cache Redis. Durante il debug questi strati erano stati progressivamente disattivati per togliere variabili dall’analisi.

Abbiamo scoperto però anche un ulteriore dettaglio interessante: un edge cache era rimasto attivo. L’header bestcache.io: STALE e una homepage restituita in appena 15-40 millisecondi dimostravano che alcune delle prime misure apparentemente effettuate “senza cache” non stavano in realtà interrogando direttamente l’origin.

Le misure sono state quindi ripetute utilizzando URL strutturalmente non cacheabili e cache-buster.

Ma il vero problema concettuale era un altro ed era impensabile !

Una cache può evitare di eseguire un lavoro. Non può rendere finito un lavoro infinito.

Il modello può essere semplificato così:

carico origin ≈ richieste × miss rate × costo del MISS

Quasi tutta l’ottimizzazione tramite cache lavora sul secondo fattore: il miss rate.

Se una pagina costa 400 millisecondi da generare e la serviamo dalla cache il 99% delle volte, abbiamo ottenuto un risultato eccellente. L’origin paga quei 400 millisecondi raramente e tutte le altre richieste vengono servite a costo quasi nullo.

Ma cosa accade se il costo del MISS non è 400 millisecondi?

Cosa accade se il MISS entra in un ciclo che non termina?

Il modello cambia completamente.

Il MISS che non diventa mai un HIT

Una cache HTTP può memorizzare una risposta solo dopo che quella risposta è stata prodotta.

Sembra un’ovvietà, ma è esattamente il dettaglio che ha reso questo incidente particolarmente insidioso.

La sequenza era la seguente:

  1. arriva una richiesta verso una determinata pagina prodotto;
  2. la cache non possiede l’oggetto e registra un MISS;
  3. la richiesta viene inoltrata a PHP;
  4. durante il rendering PHP entra nel codice di navigazione prodotto precedente/successivo di Woodmart;
  5. il codice entra in un loop;
  6. la richiesta non termina;
  7. non viene prodotta una risposta HTTP completa;
  8. la cache, non avendo ricevuto alcuna risposta, non ha niente da memorizzare;
  9. la richiesta successiva verso la stessa risorsa è nuovamente un MISS.

Non esiste quindi il passaggio fondamentale:

MISS → rendering → risposta → oggetto in cache → HIT successivi.

Il processo rimane bloccato a metà:

MISS → PHP → loop.

Diagramma dell’amplificazione del problema applicativo attraverso la cache

Una pagina sana produce una risposta cacheabile. La pagina affetta dal bug non conclude il rendering: non viene creato nessun oggetto e ogni nuovo tentativo raggiunge nuovamente PHP.

Questa è una delle lezioni più importanti di tutto il caso.

La cache non corregge la complessità patologica del codice applicativo.

Può nasconderla. Può ridurre la frequenza con cui la incontriamo. Ma se per qualche ragione una richiesta deve arrivare davvero all’origin, il costo reale dell’applicazione torna immediatamente a galla.

Quando la cache smette di proteggere e comincia ad amplificare

La parte ancora più interessante dell’incidente è che uno dei meccanismi normalmente utilizzati proprio per rendere più robusto un sito finiva per contribuire alla generazione automatica del carico.

Nei log erano presenti richieste provenienti da 127.0.0.1 verso la URL problematica a intervalli estremamente regolari: circa 61 secondi.

I timestamp rilevati durante l’analisi mostravano sequenze del tipo:

17:14:43 · 17:15:43 · 17:16:43 · 17:17:46 · 17:39:20 · 17:40:20 · 17:41:23 · 17:42:23

Una periodicità di questo tipo è compatibile con un meccanismo automatico di rivalidazione.

Ed ecco il paradosso.

Il sistema prova a rivalidare una risorsa. Non la trova pronta in cache e raggiunge PHP. PHP entra in loop. La rivalidazione non riceve una nuova risposta valida. Poco dopo parte un’altra rivalidazione. Anche questa raggiunge PHP e occupa un altro worker.

Ogni tentativo aggiunge un processo che consuma praticamente un intero core.

La cache, progettata per alleggerire l’origin, in questa particolare modalità di guasto si comportava quindi come un generatore periodico del problema.

Non perché la cache fosse configurata male nel senso tradizionale del termine, ma perché l’applicazione non era più in grado di soddisfare il contratto fondamentale sul quale una cache si basa: ricevere una richiesta e, prima o poi, restituire una risposta.

Sette worker PHP, sette core quasi completamente occupati

L’ispezione dei processi PHP-FPM ha permesso di separare immediatamente due popolazioni di worker.

Da una parte c’erano processi normali, con decine di secondi di CPU accumulata nel corso di molte ore.

Dall’altra c’erano processi che, pur essendo nati nello stesso intervallo temporale, avevano accumulato migliaia di secondi di CPU.

Il campionamento diretto di /proc/PID/stat, effettuato a intervalli di dieci secondi, ha quantificato il comportamento: i sette processi anomali consumavano circa il 98% di un core ciascuno in maniera continua.

I worker anomali non erano semplicemente occupati: stavano consumando quasi un core intero ciascuno in modo continuativo.

Su una macchina da otto vCPU significa lasciare sostanzialmente un solo core al resto del sistema.

E qui è utile osservare anche la configurazione PHP-FPM:

pm = static
pm.max_children = 32
request_terminate_timeout = 7200
php_admin_value[max_execution_time] = 7200
php_admin_flag[log_errors] = off

; nessun pm.status_path
; nessuno slowlog

La configurazione permetteva fino a 32 worker PHP contemporanei, ma naturalmente 32 worker non equivalgono a 32 core. Se sette worker entrano in CPU spin su una macchina da otto core, la capacità computazionale disponibile è già quasi completamente impegnata.

Inoltre, sia request_terminate_timeout sia max_execution_time erano impostati a 7200 secondi.

Due ore.

Questo significa che una singola richiesta patologica poteva teoricamente consumare 7200 secondi di CPU, cioè un core per due ore, prima di essere terminata.

Non stiamo parlando di una pagina che impiega quattro o cinque secondi a caricarsi.

Stiamo parlando di una richiesta che può trasformarsi in due ore-core di lavoro inutile.

Perché Redis non poteva risolvere nulla

Quando si parla di WooCommerce lento, Redis viene spesso proposto come soluzione universale.

Redis è utilissimo, ma bisogna capire quale problema risolve.

Un object cache riduce il costo di recupero di oggetti, opzioni, risultati e dati che altrimenti richiederebbero accessi ripetuti al database. Se un’applicazione esegue moltissime query identiche, o ricostruisce continuamente gli stessi oggetti, Redis può avere un impatto molto importante.

Nel nostro caso, però, avevamo:

0,0% iowait.

Inoltre la memoria RSS dei processi coinvolti rimaneva identica al byte anche dopo decine di minuti. Uno dei campionamenti riportava, per esempio:

635436 kB → 635436 kB

Nessuna crescita.

Nessuna nuova allocazione significativa.

Nessuna attività di I/O compatibile con un workload che stesse continuando a interrogare un database.

Il processo stava girando su dati già residenti in memoria.

Era un ciclo di controllo che non progrediva.

In una situazione del genere Redis è ortogonale al problema. Può rendere più veloce il recupero dei dati utilizzati prima di entrare nel loop, ma una volta entrati in un ciclo infinito non esiste object cache capace di far avanzare la variabile che dovrebbe portare alla condizione di uscita.

Non era MariaDB, non era OPcache, non era wp-cron

Un debugging serio non consiste nel trovare rapidamente un colpevole plausibile.

Consiste soprattutto nel eliminare metodicamente tutti i colpevoli plausibili che non sono responsabili.

Nel corso dell’analisi sono state verificate diverse ipotesi.

Action Scheduler presentava soltanto 22 azioni pending e nessuna azione in-progress. Jetpack Full Sync non risultava bloccato. OPcache aveva un hit rate del 99,73%, senza OOM restart e senza hash restart. Le opzioni autoload occupavano circa 0,56 MB su 1.493 righe, un valore incompatibile con una spiegazione del carico osservato. Non risultavano schedulazioni ricorrenti con intervallo zero.

Anche l’ipotesi di una botnet che colpiva parametri ?per_row= è stata esclusa perché quelle richieste ricevevano risposta 444 direttamente a livello NGINX, senza raggiungere PHP.

Il database non mostrava la saturazione attesa da un problema SQL e, come già detto, il sistema registrava 0% di iowait.

Il profiling di una pagina sana mostrava inoltre che l’intero bootstrap dei plugin non rappresentava un’anomalia macroscopica. Una richiesta di controllo concludeva il caricamento dopo i mu-plugin in circa 0,934 secondi, con 332 query e 48 MB di picco memoria.

Alcuni valori rilevati:

core:wp-includes                                     0,136 s
plugins:yith-woocommerce-advanced-reviews-premium   0,094 s
plugins:woocommerce                                  0,070 s
plugins:woocommerce-paypal-payments                  0,019 s
plugins:wordpress-seo                                0,012 s

Nessun plugin mostrava un costo tale da spiegare sette core continuamente occupati.

Questi test sono importanti anche da un punto di vista metodologico.

Avremmo potuto disattivare plugin a caso, vedere temporaneamente il carico scendere o cambiare comportamento, attribuire la causa a una correlazione accidentale e dichiarare concluso l’intervento.

Non è il nostro modo di lavorare.

Una causa è una causa quando esiste una catena di evidenze riproducibili che collega il sintomo al codice responsabile.

Il confronto che ha escluso definitivamente l’infrastruttura

Un’altra evidenza particolarmente significativa proveniva da un secondo pool PHP ospitato sullo stesso server.

Stesso sistema operativo.

Stesso NGINX.

Stesso PHP.

Stesso MariaDB.

Stessa macchina fisica o virtuale sottostante.

I worker di quel pool mostravano nell’ordine di 57-62 secondi complessivi di CPU contro oltre 5.000 secondi accumulati dai worker anomali del WooCommerce oggetto dell’analisi.

Se il problema fosse stato il kernel, la virtualizzazione, la CPU fisica, lo storage, MariaDB o la configurazione generale dell’host, sarebbe stato difficile spiegare una separazione così netta tra due workload che condividevano la stessa infrastruttura.

Il problema seguiva il sito.

E quando un problema segue il codice e non il server, bisogna smettere di fare tuning del server e iniziare a guardare il codice.

Il problema successivo: non avevamo gli strumenti con cui normalmente si fa questo lavoro

Individuare che il problema fosse applicativo era solo metà del lavoro.

Bisognava adesso capire dove PHP stesse spendendo tutta quella CPU.

Ed è qui che l’incidente è diventato particolarmente interessante.

Gli strumenti standard che utilizzeremmo normalmente per ispezionare un processo in esecuzione non erano disponibili oppure non erano utilizzabili nel contesto specifico.

ptrace, strace e gdb non potevano essere utilizzati sui figli FPM nel modo necessario. L’estensione pcntl non era presente. Non erano installati profiler come XHProf o Tideways.

PHP-FPM non aveva configurato pm.status_path.

Non era configurato uno slowlog.

E soprattutto:

log_errors era disattivato.

Il risultato era quasi perfetto per rendere invisibile questa classe di problemi.

Una richiesta entrava nel loop, continuava a consumare CPU, il client prima o poi abbandonava la connessione e il processo PHP continuava a lavorare fino al timeout di due ore.

Nessun errore applicativo.

Nessuno stack trace.

Nessun log PHP utile.

Nessun 504 necessariamente correlabile alla durata reale del processo.

Dal punto di vista dell’osservabilità era quasi un buco nero.

Abbiamo quindi costruito gli strumenti di debug che mancavano

Quando non puoi osservare direttamente lo stack di un processo PHP-FPM in produzione, devi trovare altri modi per correlare:

PID → richiesta HTTP → comportamento CPU → percorso applicativo.

Sono stati quindi introdotti temporaneamente tre strumenti diagnostici.

1. Un request tracer START/END

Il primo era estremamente semplice concettualmente.

All’inizio della richiesta venivano registrati timestamp, PID, identificativo e URI. Alla fine della richiesta uno shutdown handler registrava la corrispondente riga END.

@file_put_contents(
$log,
sprintf(
"%s\tSTART\t%d\t%s\t%s\t%s\n",
gmdate('H:i:s'),
getmypid(),
$id,
$method,
$uri
),
FILE_APPEND
);

register_shutdown_function(function () use ($id) {
@file_put_contents(
$log,
sprintf(
"%s\tEND\t%d\t%s\t%.2fs\n",
gmdate('H:i:s'),
getmypid(),
$id,
microtime(true) - $t0
),
FILE_APPEND
);
});

Una richiesta sana produce quindi una coppia:

START → END

Una richiesta bloccata produce invece:

START → ...

senza END.

Per estrarre le richieste ancora in volo era sufficiente correlare le due tipologie di record:

awk -F'\t' '$2=="START"{s[$4]=$0} $2=="END"{delete s[$4]} END{for(k in s) print s[k]}' req-trace.log

Questo tracer ha consentito di isolare quattro richieste orfane, tutte concentrate sulla stessa URL.

E soprattutto quelle richieste presentavano la stessa periodicità osservata nel comportamento dei processi.

2. Uno spin watcher sui processi PHP-FPM

Il secondo strumento campionava periodicamente /proc/PID/stat.

Lo scopo non era profilare PHP a livello di funzione, ma rilevare una condizione molto più semplice:

questo processo continua ad accumulare CPU quasi al 100% senza terminare?

Quando il watcher individuava un worker in spin poteva correlarne il PID con il request tracer e stabilire quale richiesta HTTP fosse realmente in esecuzione in quel processo.

Questo passaggio ha eliminato una parte enorme dello spazio di ricerca.

3. Breadcrumb e backtrace direttamente dall’applicazione

Il terzo strumento è quello che ha definitivamente chiuso il caso.

Non potendo interrogare comodamente il processo dall’esterno, abbiamo fatto in modo che fosse l’applicazione stessa a fotografare il proprio stack mentre il loop era in corso.

Un hook globale contava le esecuzioni e, raggiunte determinate soglie, acquisiva un debug_backtrace().

add_action('all', function () {
$n = ++$GLOBALS['__bc_n'];

```
if ($n === 20000 || $n === 80000 || $n === 200000) {
    $bt = debug_backtrace(
        DEBUG_BACKTRACE_IGNORE_ARGS,
        80
    );

    // Serializzazione diagnostica su file.
}

if ($n > 200000) {
    exit('diagnostic abort');
}
```

});

Il risultato è stato impressionante.

In una singola richiesta il breadcrumb ha raggiunto 300.000 hook in circa 4,2 secondi.

L’hook alloptions risultava invocato 26.684 volte.

Il numero di iterazioni cresceva a velocità incompatibile con un normale rendering: il codice non stava lavorando lentamente, stava ripetendo continuamente lo stesso percorso.

A quel punto non stavamo più ipotizzando un loop.

Lo stavamo osservando.

Lo stack trace porta direttamente dentro Woodmart

Il backtrace catturato durante il ciclo mostrava questa sequenza:

#5  wc_get_product()
themes/woodmart/.../class-adjacent-products.php:92

#6  WC_Adjacent_Products->get_product()
themes/woodmart/.../functions.php:588

#7  woodmart_get_next_product()
themes/woodmart/woocommerce/single-product/navigation.php:13

#8  wc_get_template()
themes/woodmart/woocommerce/content-single-product.php:203

#9  load_template()
wp-includes/template.php:816

Il percorso era ormai estremamente chiaro.

Non stavamo osservando un checkout.

Non stavamo osservando una query prodotti particolarmente pesante.

Non stavamo osservando una chiamata REST, un cron o una sincronizzazione.

Il loop avveniva durante la costruzione della navigazione prodotto precedente/successivo del tema Woodmart.

Come controllo indipendente, nel frattempo è stato abilitato anche lo slowlog di PHP-FPM.

Su 24 stack trace acquisiti, 24 contenevano woodmart_get_next_product.

Due strumenti differenti, basati su meccanismi differenti, arrivavano allo stesso identico punto.

Questa è la differenza tra una supposizione e una diagnosi.

Il codice che non riusciva più a uscire dal ciclo

Il punto critico era nel file:

wp-content/themes/woodmart/inc/integrations/woocommerce/modules/class-adjacent-products.php

La struttura rilevante del metodo era questa:

public function get_product() {
global $post;

```
$product               = false;
$this->current_product = $post->ID;

while ( $adjacent = $this->get_adjacent() ) {
    $product = wc_get_product( $adjacent->ID );

    if ( $product && $product->is_visible() ) {
        break;
    }

    $product               = false;
    $this->current_product = $adjacent->ID;
}

if ( $product ) {
    return $product;
}

return false;
```

}

L’intenzione del codice è comprensibile.

Partendo dal prodotto corrente si cerca il prodotto adiacente. Se il prodotto trovato è visibile, si restituisce e il ciclo termina. Se non è visibile, lo si scarta, si aggiorna il riferimento corrente e si continua a cercare.

Concettualmente:

prodotto corrente → adiacente → è visibile? → sì: break / no: continua

Il problema si manifestava però con una particolare configurazione dei dati: due prodotti consecutivi risultavano pubblicati ma esclusi dal catalogo.

In quella condizione il meccanismo di ricerca dei prodotti adiacenti poteva entrare in una situazione nella quale il ciclo non raggiungeva più una condizione di uscita valida.

La navigazione non riusciva quindi a convergere verso un prodotto visibile oppure verso la fine della sequenza.

Il metodo continuava a richiamare la logica di ricerca e a ricostruire gli oggetti coinvolti.

Non c’era un limite massimo alle iterazioni.

E un while senza un limite di sicurezza è corretto solo se possiamo dimostrare che a ogni iterazione lo stato avanza necessariamente verso una condizione terminale.

Nel caso osservato questa proprietà non era garantita.

Il bug non rendeva lenta la pagina: rendeva la richiesta non terminante

È importante utilizzare la terminologia corretta.

Dire che “Woodmart rallentava WooCommerce” sarebbe una descrizione tecnicamente debole.

Non stavamo misurando una funzione che impiegava due secondi invece di cento millisecondi.

Non stavamo neppure misurando una query SQL da ottimizzare.

Eravamo davanti a un problema di terminazione.

La richiesta poteva continuare fino al limite imposto esternamente da PHP-FPM.

Nel nostro ambiente quel limite era di 7.200 secondi.

Per questo il costo unitario di una singola richiesta patologica poteva arrivare a:

7.200 secondi di CPU.

Un core.

Per due ore.

E bastavano poche richieste di questo tipo, anche generate automaticamente a distanza di circa un minuto, per trasformare una macchina praticamente scarica in una macchina prossima alla saturazione.

Perché il traffico appariva innocente

Questa dinamica spiega anche un’altra caratteristica del problema che inizialmente poteva risultare controintuitiva.

Il cliente vedeva un server carico pur avendo pochissimo traffico.

Normalmente tendiamo a correlare il carico al numero di richieste:

più visite → più PHP → più query → più CPU.

Qui la correlazione era completamente differente.

Una richiesta sana poteva costare poche centinaia di millisecondi.

Una richiesta patologica poteva costarne 7.200.000.

A quel punto contare semplicemente le richieste per minuto perde quasi ogni significato.

Diecimila HIT da cache possono pesare meno di una singola richiesta che entra in CPU spin.

Ed è questo uno dei motivi per cui valutare un hosting soltanto sulla base di “quante visite regge” è spesso una metrica priva di significato tecnico.

Bisogna sapere quanto costa una richiesta.

La correzione: impedire che i prodotti non visibili entrino nel percorso e introdurre un limite

La soluzione adottata non è stata aumentare ulteriormente la potenza del server.

Non avrebbe avuto senso.

Abbiamo introdotto un mu-plugin che interviene sulla logica responsabile della selezione, con due principi di sicurezza.

Il primo consiste nell’evitare a monte che i prodotti non visibili che generano la condizione patologica vengano continuamente riproposti come candidati dalla ricerca SQL.

Il secondo consiste nell’introdurre comunque un limite massimo alle iterazioni.

Questo secondo punto è particolarmente importante dal punto di vista difensivo.

Anche se siamo convinti di avere corretto la condizione specifica che genera il loop, un codice che attraversa una sequenza di elementi esterni dovrebbe avere un limite ragionevole quando la terminazione non è matematicamente garantita.

Il concetto, in forma semplificata, è:

$iterations = 0;
$max_iterations = 100;

while ( $adjacent = get_adjacent_product() ) {

```
if ( ++$iterations > $max_iterations ) {
    return false;
}

$product = wc_get_product( $adjacent->ID );

if ( $product && $product->is_visible() ) {
    return $product;
}

// Avanzamento al candidato successivo.
```

}

Questo esempio illustra il principio di fail-safe: nessuna navigazione “prodotto precedente/successivo” dovrebbe poter consumare indefinitamente un worker PHP.

Il fix effettivamente applicato nel caso ha inoltre spostato parte della protezione a monte, facendo sì che la selezione SQL escludesse i prodotti non utilizzabili per quella navigazione.

In questo modo si correggono entrambi i livelli del problema:

si evita di alimentare il ciclo con candidati non validi e si impedisce comunque che un’anomalia futura possa trasformarsi in un ciclo senza limite.

Il risultato: da 7,70 di load a 0,28

Dopo l’applicazione della correzione il comportamento del sistema è cambiato immediatamente.

Il load average è passato da circa:

7,70 a 0,28.

Non abbiamo aggiunto CPU.

Non abbiamo aumentato la RAM.

Non abbiamo cambiato processore.

Non abbiamo sostituito MariaDB.

Abbiamo restituito alla macchina circa sette core che stavano eseguendo lavoro inutile.

Anche il TTFB misurato sul percorso interessato è migliorato sensibilmente, passando da circa 1,17 secondi a 0,36 secondi.

Dopo la correzione del percorso applicativo, il TTFB rilevato passa da circa 1,17 secondi a 0,36 secondi mentre il load scende da 7,70 a circa 0,28.

Questo dato deve però essere interpretato correttamente.

Il vero risultato non è il miglioramento di qualche centinaio di millisecondi sulla singola pagina.

Il vero risultato è che l’intero sistema ha smesso di perdere capacità computazionale nel tempo.

Prima della correzione, ogni nuova richiesta affetta poteva sottrarre un worker e praticamente un core al servizio per un periodo lunghissimo.

Dopo la correzione, quella richiesta tornava a essere una normale richiesta WooCommerce: iniziava, eseguiva il proprio lavoro e terminava.

La cosa più pericolosa dei bug applicativi è che spesso sembrano problemi di hosting

Questo caso rappresenta molto bene un problema che vediamo periodicamente nel nostro lavoro.

Un sito è lento e il primo indiziato diventa il server.

Si aumenta la RAM.

Si aumentano le CPU.

Si installa Redis.

Si aggiunge Varnish.

Si aumenta pm.max_children.

Si alzano i timeout.

Si passa a un server ancora più grande.

E qualche volta il sito sembra perfino andare meglio, perché abbiamo semplicemente aumentato la quantità di risorse che il difetto può consumare prima che l’utente percepisca la saturazione.

Ma questa non è ottimizzazione.

È occultamento tramite capacità.

Se un loop consuma un core, una macchina da 4 core collasserà prima di una macchina da 32 core. Questo non significa che sulla macchina da 32 core il codice sia diventato corretto.

Significa solo che può avviare più loop contemporaneamente prima di finire la CPU.

Anche noi eravamo partiti dall’ipotesi sbagliata

Vale la pena sottolinearlo perché in un case study tecnico raccontare soltanto la parte in cui si ha ragione serve a poco.

Quando il cliente è arrivato da Managed Server, anche noi ritenevamo ragionevole aspettarci che il cambio di infrastruttura avrebbe risolto gran parte dei problemi di performance.

Non era un’ipotesi assurda.

Molti siti WooCommerce che riceviamo provengono realmente da ambienti con PHP configurato male, storage inadatto, database non ottimizzati, assenza di object cache, web server generici, limiti troppo bassi o risorse condivise in modo aggressivo.

In quei casi la migrazione produce immediatamente un miglioramento evidente.

In questo caso no.

Il nuovo stack ha fatto invece qualcosa di altrettanto utile: ha tolto di mezzo una serie di variabili infrastrutturali e ha reso molto più evidente che il comportamento residuo non era normale.

Quando sai che il server può servire quel traffico senza difficoltà, ma continui a vedere sette core al 98%, la domanda cambia.

Non chiedi più:

come facciamo a rendere PHP più veloce?

Chiedi:

che cosa sta facendo esattamente PHP con quei sette core?

Ed è stata questa domanda a portarci alla soluzione.

Un hosting managed dovrebbe fermarsi al confine di PHP?

Qui entriamo anche in una questione di responsabilità operativa.

Formalmente, un hosting provider potrebbe fermarsi molto prima, anzi a detta di nostri rispettabilissimi colleghi, un Hosting Provider proprio non dovrebbe andare oltre.

Potrebbe mostrare al cliente i grafici, dimostrare che l’hardware funziona, evidenziare che il consumo proviene dai processi PHP del sito e rispondere:

È un problema applicativo, rivolgetevi allo sviluppatore.

In molti contratti sarebbe perfino una risposta legittima.

Ma un servizio che si definisce realmente managed, soprattutto quando lavora specificamente con WordPress e WooCommerce, deve almeno essere in grado di capire dove finisce l’infrastruttura e dove comincia l’applicazione.

E in determinati casi quel confine deve essere attraversato soprattutto se si ha l’esperienza e le competenze per farlo, consapevoli che ben pochi sviluppatori oggi hanno la capacità per farlo.

Non significa che un hosting debba diventare l’agenzia di sviluppo del cliente.

Significa che quando un’anomalia applicativa mette in crisi l’infrastruttura, la sistemistica e il debugging applicativo diventano due parti dello stesso problema.

Un processo PHP è contemporaneamente:

  • un processo del sistema operativo;
  • un worker di PHP-FPM;
  • una richiesta HTTP;
  • un’esecuzione WordPress;
  • un insieme di hook;
  • codice di plugin e tema;
  • query WooCommerce;
  • stato applicativo.

Fermarsi artificialmente a uno solo di questi livelli significa spesso non riuscire a spiegare ciò che sta realmente succedendo.

Woodmart era il colpevole !

È importante anche evitare generalizzazioni scorrette.

La diagnosi riguarda la versione Woodmart 8.1.2 presente nell’installazione analizzata e una specifica combinazione di codice, stato del catalogo e prodotti pubblicati ma esclusi dalla visibilità.

Non avrebbe senso concludere che “Woodmart è lento”, inteso come “Sempre lento” o che qualunque sito che utilizzi questo tema sia soggetto allo stesso comportamento.

Il valore tecnico del caso è un altro.

Un componente molto diffuso può contenere un percorso marginale che funziona perfettamente per milioni di richieste e fallisce soltanto quando incontra una particolare combinazione di dati.

È esattamente per questo che questi problemi sono difficili da riprodurre.

Non basta installare WooCommerce.

Non basta installare Woodmart.

Non basta aprire una pagina prodotto.

Serve la specifica sequenza di prodotti e condizioni che porta il codice a non convergere.

In produzione, però, quella combinazione esisteva.

E tanto basta.

Le lezioni sistemistiche che ci portiamo dietro

Questo incidente ci ha lasciato alcune conferme importanti.

Una cache HIT ratio eccellente non garantisce che l’applicazione sia sana

Una cache può mantenere veloce il frontend anche mentre sotto esiste un percorso applicativo patologico. Il problema emerge al primo MISS, alla prima invalidazione, alla prima rivalidazione o alla prima richiesta strutturalmente non cacheabile.

Il load average da solo non basta

Un load di 8 può significare cose completamente differenti.

Bisogna capire se il tempo viene speso in user CPU, system CPU, I/O, lock, processi runnable o attese.

Nel nostro caso 88,5% user e 0,0% iowait hanno indirizzato l’indagine molto più rapidamente di qualunque benchmark sintetico.

I timeout enormi non sono gratis

Un timeout di 7.200 secondi può essere necessario per determinate operazioni amministrative o batch, ma applicato indiscriminatamente a un pool web trasforma un loop applicativo in una risorsa sequestrata per due ore.

Un timeout non corregge il bug, ma determina quanto a lungo il bug può danneggiare il sistema.

Observability prima dell’emergenza

Non avere slowlog, status FPM e logging degli errori rende più difficile diagnosticare esattamente i casi nei quali quegli strumenti sarebbero più utili.

In questo intervento siamo riusciti a compensare costruendo strumenti temporanei, ma non dovrebbe essere la normalità.

Una richiesta senza END vale più di mille congetture

Correlare l’inizio e la fine delle richieste ai PID è stato sufficiente per trasformare un problema apparentemente casuale in un insieme ristretto di URL e processi riproducibili.

Il confronto con un workload sano sullo stesso host è potentissimo

Quando due siti condividono la stessa infrastruttura e soltanto uno presenta il problema, hai un controllo sperimentale naturale che permette di ridurre moltissimo lo spazio delle ipotesi.

Web Performance non significa soltanto Core Web Vitals

Quando si parla di Web Performance oggi si finisce spesso a discutere esclusivamente di Lighthouse, LCP, INP, CLS, JavaScript, immagini WebP o AVIF e caricamento dei font.

Sono metriche importanti.

Ma esiste un livello precedente.

Prima di ottimizzare il rendering del browser dobbiamo essere certi che l’origin sia computazionalmente sano.

Un TTFB elevato può dipendere da un server lento.

Può dipendere da query inefficienti.

Può dipendere da una cache che non funziona.

Può dipendere da chiamate HTTP esterne.

Oppure, come in questo caso, può dipendere dal fatto che una parte del rendering PHP non riesce proprio a terminare.

Ottimizzare immagini e JavaScript mentre sette core stanno girando in un loop dentro la navigazione prodotto sarebbe stato come alleggerire i sedili di un’automobile con il motore bloccato al limitatore.

La differenza tra un server veloce e un sistema veloce

Questa esperienza sintetizza molto bene anche la filosofia con cui in Managed Server affrontiamo le performance.

Un server veloce non garantisce automaticamente un sito veloce.

Il server è soltanto uno degli strati.

Possiamo avere CPU moderne, NVMe, moltissima RAM, MariaDB correttamente configurato, NGINX, Redis, Varnish, Brotli, HTTP/2 o HTTP/3 e un eccellente sistema di caching.

Ma se nel percorso di rendering esiste:

un loop infinito, una query con complessità sbagliata, una chiamata remota che non termina, un lock applicativo, un cron ricorsivo o una funzione che percorre milioni di elementi inutilmente, prima o poi quel comportamento emergerà.

L’infrastruttura può attenuarne le conseguenze.

Non può cambiare la semantica del codice.

Alla fine non abbiamo reso solo il server più potente: abbiamo fatto smettere l’applicazione di sprecare quello che aveva

Siamo partiti da una situazione in cui il cliente arrivava da un altro hosting convinto che un’infrastruttura più performante avrebbe risolto il problema.

Lo credevamo plausibile anche noi.

La migrazione ha effettivamente portato il sito su uno stack più adatto, ma ha anche dimostrato una cosa che nessun benchmark commerciale avrebbe potuto mostrare:

il problema fondamentale non era quanto velocemente il server eseguiva il codice. Era il fatto che, in una determinata condizione, quel codice non finiva mai.

Abbiamo seguito il carico dai grafici di sistema ai PID.

Dai PID alle richieste HTTP.

Dalle richieste agli hook WordPress.

Dagli hook allo stack PHP.

Dallo stack alla navigazione prodotto di Woodmart.

Dalla navigazione al ciclo che non terminava.

E dal ciclo alla particolare condizione del catalogo che lo innescava.

Solo a quel punto è stato possibile intervenire realmente.

Il risultato finale è stato un load passato da 7,70 a circa 0,28, un TTFB sceso da circa 1,17 a 0,36 secondi e, soprattutto, sette core restituiti al lavoro utile.

Non grazie a un server nuovo.

Non grazie a un’altra cache.

Non grazie a qualche parametro magico inserito in php.ini.

Grazie a un debug.

Ed è forse questo il punto più importante dell’intera storia.

Ci sono momenti in cui un hosting può limitarsi a ospitare un’applicazione.

Ce ne sono altri in cui, se vuole davvero capire perché quella applicazione non funziona, deve attraversare il confine tra sistemistica e sviluppo, seguire i dati fino al codice e sporcarsi le mani.

Questo è stato uno di quei momenti.

Hai dei dubbi? Non sai da dove iniziare? Contattaci !

Abbiamo tutte le risposte alle tue domande per aiutarti nella giusta scelta.

Chatta con noi

Chatta direttamente con il nostro supporto prevendita.

0256569681

Contattaci telefonicamente negli orari d’ufficio 9:30 – 19:30

Contattaci online

Apri una richiesta direttamente nell’area dei contatti.

DISCLAIMER, Note Legali e Copyright. Red Hat, Inc. detiene i diritti su Red Hat®, RHEL®, RedHat Linux®, e CentOS®; AlmaLinux™ è un marchio di AlmaLinux OS Foundation; Rocky Linux® è un marchio registrato di Rocky Linux Foundation; SUSE® è un marchio registrato di SUSE LLC; Canonical Ltd. detiene i diritti su Ubuntu®; Software in the Public Interest, Inc. detiene i diritti su Debian®; Linus Torvalds detiene i diritti su Linux®; FreeBSD® è un marchio registrato di The FreeBSD Foundation; NetBSD® è un marchio registrato di The NetBSD Foundation; OpenBSD® è un marchio registrato di Theo de Raadt; Oracle Corporation detiene i diritti su Oracle®, MySQL®, MyRocks®, VirtualBox® e ZFS®; Percona® è un marchio registrato di Percona LLC; MariaDB® è un marchio registrato di MariaDB Corporation Ab; PostgreSQL® è un marchio registrato di PostgreSQL Global Development Group; SQLite® è un marchio registrato di Hipp, Wyrick & Company, Inc.; KeyDB® è un marchio registrato di EQ Alpha Technology Ltd.; Typesense® è un marchio registrato di Typesense Inc.; REDIS® è un marchio registrato di Redis Labs Ltd; F5 Networks, Inc. detiene i diritti su NGINX® e NGINX Plus®; Varnish® è un marchio registrato di Varnish Software AB; HAProxy® è un marchio registrato di HAProxy Technologies LLC; Traefik® è un marchio registrato di Traefik Labs; Envoy® è un marchio registrato di CNCF; Adobe Inc. detiene i diritti su Magento®; PrestaShop® è un marchio registrato di PrestaShop SA; OpenCart® è un marchio registrato di OpenCart Limited; Automattic Inc. detiene i diritti su WordPress®, WooCommerce®, e JetPack®; Open Source Matters, Inc. detiene i diritti su Joomla®; Dries Buytaert detiene i diritti su Drupal®; Shopify® è un marchio registrato di Shopify Inc.; BigCommerce® è un marchio registrato di BigCommerce Pty. Ltd.; TYPO3® è un marchio registrato di TYPO3 Association; Ghost® è un marchio registrato di Ghost Foundation; Amazon Web Services, Inc. detiene i diritti su AWS® e Amazon SES®; Google LLC detiene i diritti su Google Cloud™, Chrome™, e Google Kubernetes Engine™; Alibaba Cloud® è un marchio registrato di Alibaba Group Holding Limited; DigitalOcean® è un marchio registrato di DigitalOcean, LLC; Linode® è un marchio registrato di Linode, LLC; Vultr® è un marchio registrato di The Constant Company, LLC; Akamai® è un marchio registrato di Akamai Technologies, Inc.; Fastly® è un marchio registrato di Fastly, Inc.; Let’s Encrypt® è un marchio registrato di Internet Security Research Group; Microsoft Corporation detiene i diritti su Microsoft®, Azure®, Windows®, Office®, e Internet Explorer®; Mozilla Foundation detiene i diritti su Firefox®; Apache® è un marchio registrato di The Apache Software Foundation; Apache Tomcat® è un marchio registrato di The Apache Software Foundation; PHP® è un marchio registrato del PHP Group; Docker® è un marchio registrato di Docker, Inc.; Kubernetes® è un marchio registrato di The Linux Foundation; OpenShift® è un marchio registrato di Red Hat, Inc.; Podman® è un marchio registrato di Red Hat, Inc.; Proxmox® è un marchio registrato di Proxmox Server Solutions GmbH; VMware® è un marchio registrato di Broadcom Inc.; CloudFlare® è un marchio registrato di Cloudflare, Inc.; NETSCOUT® è un marchio registrato di NETSCOUT Systems Inc.; ElasticSearch®, LogStash®, e Kibana® sono marchi registrati di Elastic N.V.; Grafana® è un marchio registrato di Grafana Labs; Prometheus® è un marchio registrato di The Linux Foundation; Zabbix® è un marchio registrato di Zabbix LLC; Datadog® è un marchio registrato di Datadog, Inc.; Ceph® è un marchio registrato di Red Hat, Inc.; MinIO® è un marchio registrato di MinIO, Inc.; Mailgun® è un marchio registrato di Mailgun Technologies, Inc.; SendGrid® è un marchio registrato di Twilio Inc.; Postmark® è un marchio registrato di ActiveCampaign, LLC; cPanel®, L.L.C. detiene i diritti su cPanel®; Plesk® è un marchio registrato di Plesk International GmbH; Hetzner® è un marchio registrato di Hetzner Online GmbH; OVHcloud® è un marchio registrato di OVH Groupe SAS; Terraform® è un marchio registrato di HashiCorp, Inc.; Ansible® è un marchio registrato di Red Hat, Inc.; cURL® è un marchio registrato di Daniel Stenberg; Facebook®, Inc. detiene i diritti su Facebook®, Messenger® e Instagram®. Questo sito non è affiliato, sponsorizzato o altrimenti associato a nessuna delle entità sopra menzionate e non rappresenta nessuna di queste entità in alcun modo. Tutti i diritti sui marchi e sui nomi di prodotto menzionati sono di proprietà dei rispettivi detentori di copyright. Ogni altro marchio citato appartiene ai propri registranti. MANAGED SERVER® è un marchio registrato a livello europeo da MANAGED SERVER SRL, con sede legale in Via Flavio Gioia, 6, 62012 Civitanova Marche (MC), Italia e sede operativa in Via Enzo Ferrari, 9, 62012 Civitanova Marche (MC), Italia.

SOLO UN ATTIMO !

Ti sei mai chiesto se il tuo Hosting faccia schifo ?

Scopri subito se il tuo hosting provider ti sta danneggiando con un sito lento degno del 1990 ! Risultato immediato.

Close the CTA
Torna in alto