In questo articolo verranno evidenziati alcuni dei principi più comuni da tenere in considerazione per raggiungere ottimi livelli di performance nelle proprie applicazioni web. Parleremo nello specifico di tutto ciò che riguarda l’applicazione web lato server (il back-end) ma questo non significa che nelle vostre applicazioni dovrete tralasciare l’aspetto performance nella parte di interfaccia (il front-end). La maggior parte dei concetti che tratteremo possono essere applicati a qualsiasi linguaggio e framework anche se, data le mia esperienza specifica, farò spesso riferimento ad esempi, design patterns, convenzioni e strumenti che sono piuttosto comuni nel mondo PHP.

Questo post é stato originariamente pubblicato in lingua inglese sul mio blog personale: 6 Rules of thumb to build blazing fast web server applications.

IN BREVE, queste sono le regole che tratteremo:

  • Regola 1: Evita l’ottimizzazione prematura
  • Regola 2: Non perdere tempo nel fare cose inutili
  • Regola 3: Mamma, lo faccio domani!
  • Regola 4: Utilizza la cache, giovane padawan!
  • Regola 5: Evita il dannato problema delle N+1 query
  • Regola 6: Predisponi la tua app alla scalabilità orizzontale

Il tuo codice dovrebbe essere veloce come Flash

Regola 1: Evita l’ottimizzazione prematura

In una delle sue citazioni più famose, Donald Knuth, uno dei padri dell’informatica, disse:

“L’ottimizzazione prematura è la radice di tutti i mali”
— Donald Knuth

Knuth aveva notato che molti sviluppatori spendono una quantità di tempo spropositata preoccupandosi delle performance di parti “non critiche” del proprio codice. Questo spesso accade perché questi sviluppatori non sanno esattamente come riconoscere le parti “critiche” del proprio codice, ovvero quelle che hanno maggiore impatto sull’utente e su cui ha più senso investire tempo e risorse. Quante volte ti è capitato di trovare in rete discussioni e domande futili del tipo: “Ma le stringhe delimitate da doppio apice sono più lente di quelle delimitate da apice singolo?“…

Per evitare di cascare nella trappola dell’ottimizzazione prematura dovresti scrivere la prima versione della tua applicazione senza preoccuparti troppo delle performance. Una volta fatto ciò puoi installare un profiler e utilizzarlo sul tuo codice per scoprire dove stanno i principali colli di bottiglia. In questo modo puoi concentrare i tuoi sforzi nel migliorare le parti che richiedono maggiormente la tua attenzione.

Nota bene che la citazione di Knuth non va intesa come un invito a non preoccuparsi affatto di performance e ottimizzazione e non deve essere utilizzata come scusa per scrivere codice “indecente”. Piuttosto va letta come un invito ad imparare ad ottimizzare in modo “intelligente”.

Se lavori in PHP ci sono diversi tool che possono essere utilizzati per eseguire la “profilazione” del codice:

  • xdebug: è probabilmente il più famoso dei debugger disponibili per PHP, può essere installato come estensione PHP e può facilmente essere integrato con la maggior parte degli ambienti di sviluppo. Oltre ad essere un debugger offre anche funzionalità specifiche per la profilazione.
  • xhprof: un profiler PHP che offre una semplice navigazione tramite interfaccia web e la possibilità di comparare diverse varianti di codice per determinare qual è quella maggiormente performante.
  • Symfony profiler: si tratta del profiler fornito dal framework Symfony. Permette di analizzare il tempo di esecuzione di ogni richiesta, mostra una timeline dettagliata che permette facilmente di individuare quali parti di codice consumano più tempo. È installato di default nella modalità development quando si sviluppa un app con il framework Symfony completo, ma può essere anche integrato come componente separato in altri progetti PHP che non utilizzano l’intero framework (o altri framework). Va sottolineato che questa soluzione non richiede l’installazione di alcuna estensione PHP.
  • Il componente Stopwatch: è la libreria PHP che a basso livello viene utilizzata dal profiler Symfony. Può essere facilmente integrato in qualsiasi applicazione PHP per misurare il tempo di esecuzione di una porzione di codice. Anch’esso non richiede alcuna estensione PHP.
  • Blackfire.io: si tratta di un profiler ottimizzato per PHP offerto come software as a service. È fornito di una interfaccia web molto potente in grado di mostrare visualmente un grafico ti tutte le funzioni eseguite in una richiesta ed evidenziare il percorso critico in termini di performance. Permette inoltre di comparare diverse versioni del proprio codice e di analizzare anche le performance di rete e delle query al database.
  • Tideways: una alternativa promettente a Blackfire che offre una serie di strumenti visuali (timeline, grafo di chiamate, etc.) per semplificare l’individuazione di colli di bottiglia. È stato progettato per essere eseguito costantemente, anche negli ambienti di produzione, avendo un impatto estremamente trascurabile sulle performance dell’intera applicazione.

Se vuoi approfondire questo specifico argomento ti consiglio di leggere i seguenti articoli e papers (in inglese):

Regola 2: Non perdere tempo nel fare cose inutili

Joker meme I Just Do Things (performance tips)

Molto spesso il codice fa più cose di quante ne dovrebbe fare per calcolare l’output atteso. Questo potrebbe succedere in particolar modo quando si utilizzano librerie complesse e framework o quando il codice della propria applicazione non è organizzato in modo ideale. Giusto per fare un esempio potrebbe capitare di caricare molte più classi di quelle che vengono effettivamente utilizzare durante la richiesta o potresti aprire una connessione ad un database o leggere un file di configurazione ad ogni richiesta, anche quando queste risorse non sono necessarie per produrre l’output della pagina corrente.

In generale una buona conoscenza dei framework e delle librerie utilizzate aiuta nell’evitare questi problemi, ma ci sono diversi design pattern che possono tornarci utili per evitare queste situazioni ed aiutarci ad ottenere performance migliori.

Autoloading

È una feature di PHP che permette di includere automaticamente i files che contengono le definizioni delle classi che sono effettivamente utilizzate durante l’esecuzione di uno script PHP (solo quando queste classi stanno effettivamente per essere istanziate). Ti permette di non preoccuparti di “quali file includere” in ogni script e di non includerne erroneamente più del necessario. Ogni file incluso infatti deve essere letto ed interpretato dall’interprete PHP, operazioni che potrebbero occupare il disco e il processore per una quantità di tempo non indifferente, specialmente se in presenza di molti files o di files di grosse dimensioni.

Configurare l’autoloader è stata in passato un’operazione piuttosto complessa, anche perchè ogni libreria utilizzava un approccio non standardizzato nell’organizzazione delle classi all’interno del codice. Per fortuna oggi possiamo contare sugli standard PSR-0 e PSR-4 ormai consolidati e su strumenti come Composer che rendono l’utilizzo dell’autoloading una funzionalità cosí semplice da potersi considerare scontata.

Dependency Injection

È un design pattern piuttosto comune nel mondo Java ma che di recente sta prendendo piede anche nel mondo PHP grazie anche agli sforzi di framework come Symfony, Zend e Laravel che ne fanno ampiamente uso e ne hanno promosso la divulgazione.

In pratica questo design pattern suggerisce di “iniettare le dipendenze” di un componente (una classe) attraverso il costruttore o un metodo setter. Cosí facendo si obbliga lo sviluppatore a ragionare in termini di dipendenze e a creare componenti piccoli ed isolati che fanno una cosa sola e la fanno bene, collaborando con altri componenti quando necessario solo attraverso interfacce ben definite.

Utilizzare questo pattern migliora la qualità del proprio codice e permette di evitare l’inclusione di classi inutili durante l’esecuzione di uno script.

Lazy Loading

Si tratta di un altro importante design pattern utilizzato per rimandare l’inizializzazione di oggetti o di risorse “pesanti” fino al momento in cui sono necessari. Può essere ad esempio utilizzato per effettuare la connessione ad un database solo nel momento in cui sta per essere eseguita la prima query, evitando cosí di stabilire connessioni inutili in tutte le pagine in cui non vengono effettuate query.

Regola 3: Mamma, lo faccio domani!

Tomorrow definition mystical land for human productivity

Quante volte hai avuto bisogno di automatizzare l’invio di una mail per mandare una notifica ad un utente che ha appena compiuto una specifica azione sul tuo sito? Immagina casi come il cambio di una password, una transazione eseguita con successo per il pagamento di un ordine, etc. Oppure immagina quante volte hai dovuto ridimensionare un’immagine dopo che un utente l’ha caricata sul tuo server? Beh è piuttosto comune eseguire queste operazioni non proprio leggere “al volo” in fase di elaborazione della pagina, ovvero prima di aver mostrato un messaggio di successo all’utente che ha appena compiuto la specifica azione. L’utente si aspetta di vedere qualcosa nel proprio browser prima possibile ed è compito di noi sviluppatori assicurarci che nessuna operazione addizionale venga fatta mentre l’utente attende una risposta. Bisogna rispondere il più velocemente possibile e rimandare ad un momento successivo qualsiasi operazione non strettamente correlata alla generazione del messaggio di successo.

Il modo più comune di affrontare queste situazione è quello di utilizzare le job queues, le quali permettono di memorizzare i dati inviati dall’utente in una coda di esecuzione. L’idea è quella di memorizzare i dati importanti e dimenticarcene il più in fretta possibile per tornare a “servire” il nostro utente! Le job queues permettono di distribuire poi questi dati a dei processi (workers) scritti appositamente per eseguire computazioni in background, in modo indipendente dal processo di generazione della pagina visitata dall’utente.

Una job queue può essere facilmente implementata utilizzando qualsiasi tipo di base di dati (spesso vengono utilizzati Redis o MongoDB) o utilizzando dei message broker come RabbitMQ o ActiveMQ. Ci sono diverse implementazioni interessanti nel mondo PHP:

  • Resque: una libreria che implementa una job queue utilizzando Redis come base di dati
  • Laravel Queues: la soluzione di default offerta dai framework Laravel e Lumen per gestire code di lavoro. Può essere configurata per utilizzare una buona varietà di basi di dati come data storage.
  • Gearman: Un job server generico che permette di scrivere i propri workers in diversi linguaggi di programmazione (PHP tra i tanti).
  • Beanstalkd: Un altro veloce job server (originariamente sviluppato per Facebook) che offre librerie per i principali linguaggi di programmazione (Ruby, PHP, etc.)

Regola 4: Usa la cache, giovane padawan!

Comic strip about the cloud and the cache

Al giorno d’oggi le applicazioni web sono diventate piuttosto complesse. Per generare una risposta ad una richiesta capita spesso di dover fare alcune cosette non proprio leggere: connettersi ad uno o più database, chiamare delle API esterne, leggere dei files di configurazione, calcolare ed aggregare dei dati, serializzare dei dati in un formato particolare (Xml, Json, etc.), utilizzare un sistema di templating per renderizzare l’HTML, etc.

Seguendo un approccio naïf, possiamo lasciare il nostro server a fare queste operazioni noiose ad ogni richiesta dell’utente, lui non si stancherà mai di fare sempre le stesse cose, ne si lamenterà… Ma se vogliamo essere clementi nei confronti del nostro server e soprattutto dei nostri utenti, possiamo usare un approccio più intelligente (e performante!): signore e signori ecco a voi “la cache”!

La cache, pronunciata “cash” (non “catch” o “cashay”), memorizza informazioni calcolate di recente, cosí che possano essere riutilizzate velocemente in un secondo momento.

Il concetto di cache è piuttosto diffuso in computer science ed è possibile trovarlo praticamente ovunque. Ad esempio la RAM stessa è utilizzata per memorizzare (“mettere in cache” appunto) il codice dei programmi in esecuzione per evitare al processore di doverlo leggere dal disco (molto molto più lento della RAM) milioni e milioni di volte durante il corso d’esecuzione dello stesso.

Per quanto riguarda la programmazione web ci possiamo concentrare su diversi livelli di cache:

Byte Code Cache

È una features presente nella maggior parte dei linguaggi interpretati (PHP, Python, Ruby, etc.) e permette di evitare al processore di interpretare nuovamente il codice sorgente di un file che era già stato precedentemente interpretato e non è stato più modificato.

Alcuni linguaggi hanno questa funzionalità integrata nel cuore dell’interprete (python), altri hanno bisogno di specifiche estensioni che devono essere installate e abilitate (PHP). Le estensioni più comuni in assoluto per questo scopo sono APCeAccelerator, Xcache. A partire da PHP 5.5 è preferibile utilizzare l’estensione OpCache che viene generalmente distribuita insieme all’interprete.

Application Cache

Da non confondere con la application cache di HTML5, è la logica di caching che riguarda la specifica applicazione che si sta sviluppando ed è probabilmente la più importante in termini di performance.

Stai calcolando il 1264575esimo numero nella sequenza di Fibonacci di continuo? Bene, perchè non memorizzi il risultato alla prima chiamata e lo recuperi dalla cache per tutte le chiamate successive invece di continuare a ricalcolarlo? O, per fare un esempio un po’ più realistico, stai facendo sempre la stessa query “costosa” per caricare i contenuti da mostrare nella tua home page? Si? Allora metti in cache il risultato della query (o anche l’intero output della pagina se possibile) ed evita di connetterti al database ed eseguire la query ogni volta che devi servire la tua pagina.

In questi casi è una buona idea utilizzare un server di cache come Memcached, RedisGibson.

Cache HTTP

Ricevere dati attraverso la rete è un’operazione lenta: solo per scaricare i dati necessari a mostrare una pagina web il browser deve effettuare diverse richieste e verosimilmente viene speso molto tempo prima che sia possibile mostrare la pagina. Non sarebbe utile avere un modo per dire al browser di riutilizzare dei contenuti che ha già scaricato in precedenza dal nostro sito invece di scaricarli nuovamente ad ogni accesso? Ebbene si, è possibile fare proprio questo grazie agli header della cache HTTP come  Etag e  Cache-control. Questa risulta essere una delle modalità più economiche (in termine di costi dei server) di utilizzare la cache, infatti tutto viene memorizzato all’interno del browser dell’utente. L’unica complessità da gestire è quella di assicurarsi di utilizzare questi header in modo corretto per evitare di mostrare contenuto obsoleto.

Proxy Cache

Questa tecnica di caching si riferisce all’utilizzo di uno o più server dedicati (chiamati reverse proxy) a ricevere tutto il traffico HTTP e a mantenere copied delle pagine elaborate in precedenza. Quando un utente richiede nuovamente una di queste pagine, questa viene restituita immediatamente, senza la necessità di inizializzare linguaggi di scripting di backend come PHP ed effettuare computazioni più o meno complesse. In siti che non richiedono la generazione di pagine personalizzate per gli utenti questa risulta essere una delle ottimizzazioni più semplici ed efficaci con un bassissimo impatto sulla logica della nostra applicazione web. I proxy server più famosi sul mercato sono Varnish, Nginx e Squid. Anche Apache può essere configurato per essere utilizzato come reverse proxy.

Ad ogni modo, non appena hai acquisito il concetto di cache ti risulterà veramente semplice utilizzarlo. Il problema però è quella di capire quando qualcosa è cambiata ed il valore che hai memorizzato nella cache non è più valido. In questi casi è necessario cancellare il valore obsoleto dalla cache cosí che possa venire correttamente ricalcolato alla prossima richiesta. Questo processo è detto invalidazione della cache (“cache invalidation”) ed è costante fonte di frustrazione tra gli sviluppatori al punto che viene menzionato in una delle citazioni più famose del mondo dell’informatica:

Ci sono solo due cose difficili in Computer Science: invalidare la cache e dare nomi alle cose.
— Phil Karlton

Se sei stato nel mondo dello sviluppo software per un po’ sono sicuro che ti sarai già imbattuto almeno una volta in questa citazione!

Purtroppo non esiste alcuna formula magica per rendere l’invalidazione della cache un processo semplice o addirittura automatizzato. Dipende molto dall’architettura del tuo codice e dai requisiti della tua applicazione. In generale meno livelli di cache sono presenti, più è facile riuscire ad individuare delle buone regole di invalidazione.

Qui di seguito trovi alcuni articoli utili che riguardano la cache applicata al mondo del web (in inglese):

Regola 5: Evita il dannato problema delle N+1 query

Il problema delle N+1 query (the “N+1 Query Problem“) è un anti-pattern piuttosto comune che si verifica specialmente quando si ha a che fare con i database relazionali. Consiste nel leggere N record dal database dopo aver eseguito N+1 query (tipicamente 1 per ottenere gli ID degli N record in question e una query aggiuntiva per arricchire ogni specifico record). Per capire meglio di cosa si tratta diamo un’occhiata a questo specifico esempio:

Questo codice carica una lista di utenti con una prima query e poi, per ogni utente nella lista, carica ulteriori informazioni eseguendo una query addizionale per ogni utente. In totale questo codice produce N+1 query del tipo:

Questa soluzione è palesemente inefficiente ma può accadere piuttosto spesso se non viene prestata la dovuta cura al modo in cui costruiamo le nostre query o se utilizziamo qualche magica libreria (come un ORM) per farlo ma non la conosciamo abbastanza da poterla utilizzare correttamente.

In generale è possibile risolvere questo specifico problema delle N+1 query eseguendo 2 query (quindi un numero costante di query non più dipendente dalla grandezza dell’input) come nel seguente esempio:

o utilizzando una clausola  JOIN quando possibile.

Questo problema può essere controllato solo quando è possibile cambiare la logica con cui sono costruite le query o quando si ha una ottima conoscenza della libreria ORM che si sta utilizzando (nel caso in cui se ne stia utilizzando qualcuna). Molti profiler permettono di collezionare tutte le query che vengono eseguite durante l’esecuzione di una pagine e questo può essere un ottimo strumento per rendersi conto se si sta cascando o meno nella trappola delle N+1 query.

Come nota collaterale, parlando di databases, assicurati di tenere una sola connessione attiva con il tuo database ed evita di riconnetterti ad ogni richiesta.

Ovviamente non aspettarti che questo paragrafo sia esaustivo, si tratta di un problema complesso e sfaccettato sul quale sono stati scritti persino degli interi libri. Se vuoi approfondire la questione ecco una reading list tematica (in inglese):

Regola 6: Predisponi la tua app alla scalabilità orizzontale

Horizontal scalability is hard

Il termine “Scalabilità” non è un sinonimo di “performance”, ma i due termini sono strettamente collegati. Se mi concedi una definizione personale, “scalabilità” è l’abilità di un sistema di adattarsi e rimanere funzionale senza rallentamenti evidenti anche quando il numero di utenti (e quindi di richieste) e di dati memorizzati cresce notevolmente.

Anche questo è un problema molto complesso e ampio e non ho la pretesa di poter essere esaustivo in questa sede. Ma per quanto riguarda le performance è importante evidenziare e tenere in mente alcuni dettagli tecnici che puoi facilmente adottare nelle tue applicazioni per “predisporle” ad essere “scalabili orizzontalmente”. La scalabilità orizzontale è una particolare strategia di scalabilità in cui è possibile aggiungere più server ad un sistema man mano che aumentano le esigenze computazionali. Il carico viene cosí diviso tra tutti i server disponibili in modo che nessuno di essi venga sovraccaricato da troppe richieste simultanee. Nel caso di un’applicazione web i server che vengono maggiormente replicati sono quelli che contengono ed eseguono il codice dell’applicazione web (ovvero il codice PHP se l’applicazione è scritta in PHP), ma anche i server che contengono le basi di dati e i server di cache possono seguire lo stesso approccio di scalabilità quando si gestiscono grosse moli di dati.

I due principali problemi che bisogna affrontare per predisporre un’applicazione web alla scalabilità orizzontale sono in genere relativi alle sessioni e alla consistenza dei files degli utenti.

Sessioni

Molto spesso (specialmente nelle applicazioni PHP in cui avviene di default) la sessione utente è memorizzata nel file system locale sotto forma di file.

Utilizzando questa strategia (tra l’altro lenta in quanto richiede l’accesso continuo all’hard disk) ogni server avrà localmente i suoi files di sessione e quindi un utente che inizierà una sessione su un server non troverà gli stessi dati qualora una sua richiesta venisse servita da un’altro server nel cluster. Questo può ovviamente portare ad errori imprevisti, perdite di dati e logout improvviso degli utenti.

Una soluzione migliore è quella di utilizzare un qualche tipo di base di dati esterna per memorizzare i dati di sessione. Molti framework permettono di gestire facilmente la scrittura dei dati di sessione in una base di dati esterna cambiando solo poche righe di configurazione. All’inizio, quando la tua applicazione è piccola e non hai molti utenti puoi tenere la base di dati per le sessioni nello stesso server che fa da web server, successivamente, qualora la tua applicazione avesse necessità di scalare, ti basterà spostare il database delle sessioni su una o più macchine dedicate ed aggiungere nuove macchine nello stack di web server. I web server potranno cosí facilmente accedere ad una sessione esterna e soprattutto “condivisa”.

Consistenza dei file utente

Un problema simile a quello descritto per le sessioni si ha quando gli utenti possono memorizzare dei files sulle macchine server (ad esempio un’immagine di profilo o un documento PDF). È necessario anche in questo caso memorizzare questi files in uno storage condiviso, esterno alla logica della nostra applicazione web. Per questi fini si può utilizzare un servizio di storage esterno alla nostra infrastruttura (come Amazon S3Rackspace Cloudfiles), oppure si possono tenere i files nelle macchine locali e sincronizzare i file system dei vari web server con software come NFSGlusterFS.

Ecco una lista di risorse e approfondimenti interessanti nell’ambito della scalabilità di applicazioni web (in inglese):

Conclusione

Spero che questo luuuuungo post sulle performance di applicazioni web sia stato utile e interessante. Il mio obiettivo era quello di dare un’idea generale su alcuni dei principali problemi che si affrontano in materia di performance quando si sviluppa un’applicazione web. Come ho detto nella prima regola non bisogna cadere nella trappola della ottimizzazione prematura e bisogna inizialmente focalizzarsi nello scrivere codice corretto per le funzionalità che si vogliono sviluppare. In ogni caso se hai maturato molti di questi concetti e sei uno sviluppatore abbastanza esperto sono sicuro che sarà per te quasi naturale utilizzare delle buone soluzioni architetturali fin dalla prima iterazione di un nuovo sito web, potrai poi raffinare la tua soluzione analizzandola nel dettaglio attraverso un profiler ed applicando delle ottimizzazioni specifiche.

La mia lista di regole non aveva la pretesa di essere esaustiva, ma se credi che abbia dimenticato qualche regola fondamentale lascia pure un commento, spero cosí di conoscere le vostre opinioni sulle performance web e che possa nascere qualche discussione interessante :)

Alla prossima!

The following two tabs change content below.
Luciano é un ingenere del software e un programmatore web nato nel 1987, lo stesso anno in cui “Super Mario Bros” é stato rilasciato in Europa, che, non a caso é il suo gioco preferito!
É appassionato di programmazione, del web, dalle app e da tutto ció che é creativitá come la musica, l’arte e il design. Come sviluppatore web ha maturato la propria esperienza principalmente come backend developer PHP e Symfony, anche se di recente si é innamorato di Javascript e NodeJS.
Nel suo tempo libero ama scrivere sul suo blog personale loige.co.

Ultimi post di Luciano Mammino (vedi tutti)