Migliorare le performance del proprio sito PHP con l’estensione APC

php-logoSe avete un sito web realizzato in PHP e volete raddoppiarne le performance con un semplice ma significativo passo, allora la soluzione giusta è l’estensione PECL APC.

Come molti sapranno PHP è un linguaggio interpretato, questo fa si che ad ogni singola richiesta di una pagina web, l’interprete PHP debba leggere il codice nei files memorizzati su disco, valutarne la correttezza, trasformarlo in istruzioni eseguibili, trasformare i nomi di variabili e funzioni in indirizzi di memoria, ed eseguire le istruzioni specificate.

Inoltre per chi fa uso di framework o librerie particolarmente corpose, c’è l’overhead dovuto alle dozzine di files contenuti in queste librerie. Per esempio Zend Framework è composto da più di 2500 files, e spesso e volentieri per istanziare un singolo oggetto di una data classe è necessario che l’interprete legga decine di files.

Files caricati da Zend Framework

Files caricati da Zend Framework

L’applicazione di prova dell’articolo su ZFDebug necessita di 110 files (circa 1MB) solamente per caricare l’home page.

Proprio in casi come questi ci viene incontro APC, infatti questa estensione crea un file mappato in memoria (praticamente una porzione di RAM salvata su filesystem) che viene condivisa tra tutte le richieste al webserver, dove mantiene una copia cache di tutti i files caricati durante l’esecuzione degli script.

In questo modo, fatta eccezione per la prima richiesta, i tempi di risposta avranno uno speedup notevole in quanto l’interprete non dovrà più leggere i files da disco ma se li ritroverà automaticamente caricati in memoria.

Installazione e configurazione

Per installare questa estensione su una macchina Debian/Ubuntu è sufficiente eseguire il comando

aptitude install php-apc

In alternativa è possibile installarla utilizzando PECL oppure compilando a mano l’estensione e caricandola con il file php.ini con la riga

extension=apc.so

Per quanto riguarda la configurazione, in linea di massima va bene quella di default che prevede 1 segmento di memoria condivisa di dimensione pari a 30MB, ed abilita la cache di tutti i files php ed inc di dimensione inferiore al Megabyte.

Su una macchina in produzione può essere molto producente l’opzione apc.stat con la quale si indica all’estensione di non controllare per i files in cache se sono cambiati su disco. Questa infatti è un’operazione abbastanza onerosa, in quanto richiede per ogni file un controllo sul filesystem. Chiaramente questo settaggio va usato con molta cautela, in quanto la cache non viene svuotata sino ad un riavvio del webserver, per cui eventuali modifiche effettuate ad uno script non saranno visibili se questo si trova in cache.

Un’altra opzione che può tornare utile cambiare è apc.enable_cli, che rende disponibile la cache dei files anche negli script a riga di comando. Per default questa opzione è disattivata, quindi non si ha la cache attiva negli script a riga di comando.

Per una trattazione completa delle opzioni, del loro significato e dei valori di default si può vedere la pagina relativa.

Benchmark

Per misurare l’aumento di prestazioni che si ha installando sul proprio webserver l’estensione APC, ho utilizzato il tool Apache Benchmark. Questo strumento misura i tempi di risposta di un sito web e determina il numero massimo di richieste al secondo che la vostra installazione di Apache può gestire. Per ottenere un risultato indipendente da eventuali congestioni di rete le prove sono state eseguite in locale come spesso viene consigliato, per cui la singola sessione di benchmark è stata lanciata eseguendo il comando

ab -c 30 -n 1000 http://localhost/zfqs/

Ovvero ho detto ad ab di eseguire 1000 richieste lanciandone al più 30 in parallelo sulla home page del progetto dell’articolo di ZFDebug, che come detto in precedenza carica 110 files per mostrare la home page.

Il benchmark è stato eseguito in tre situazioni distinte:

  1. Estensione apc non caricata
  2. Estensione apc caricata con opzione stat=1 (default)
  3. Estensione apc caricata con opzione stat=0

E questi sono i risultati ottenuti:

This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)

Server Software:        Apache/2.2.9
Server Hostname:        localhost
Server Port:            80

Document Path:          /zfqs/
Document Length:        24269 bytes

Concurrency Level:      30
Time taken for tests:   67.103 seconds
Complete requests:      1000
Failed requests:        182
   (Connect: 0, Receive: 0, Length: 182, Exceptions: 0)
Write errors:           0
Total transferred:      24717294 bytes
HTML transferred:       24268294 bytes
Requests per second:    14.90 [#/sec] (mean)
Time per request:       2013.098 [ms] (mean)
Time per request:       67.103 [ms] (mean, across all concurrent requests)
Transfer rate:          359.71 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0       2
Processing:   984 2004 228.3   2001    3384
Waiting:      957 1874 221.8   1865    3209
Total:        984 2004 228.3   2001    3386

Percentage of the requests served within a certain time (ms)
  50%   2001
  66%   2078
  75%   2125
  80%   2159
  90%   2239
  95%   2321
  98%   2412
  99%   2740
 100%   3386 (longest request)
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)

Server Software:        Apache/2.2.9
Server Hostname:        localhost
Server Port:            80

Document Path:          /zfqs/
Document Length:        24263 bytes

Concurrency Level:      30
Time taken for tests:   22.696 seconds
Complete requests:      1000
Failed requests:        753
   (Connect: 0, Receive: 0, Length: 753, Exceptions: 0)
Write errors:           0
Total transferred:      24714238 bytes
HTML transferred:       24265238 bytes
Requests per second:    44.06 [#/sec] (mean)
Time per request:       680.882 [ms] (mean)
Time per request:       22.696 [ms] (mean, across all concurrent requests)
Transfer rate:          1063.40 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0       2
Processing:   198  678 144.2    673    1449
Waiting:      184  639 142.5    640    1444
Total:        198  678 144.1    673    1449

Percentage of the requests served within a certain time (ms)
  50%    673
  66%    722
  75%    752
  80%    778
  90%    846
  95%    916
  98%   1001
  99%   1101
 100%   1449 (longest request)
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)

Server Software:        Apache/2.2.9
Server Hostname:        localhost
Server Port:            80

Document Path:          /zfqs/
Document Length:        24267 bytes

Concurrency Level:      30
Time taken for tests:   18.690 seconds
Complete requests:      1000
Failed requests:        866
   (Connect: 0, Receive: 0, Length: 866, Exceptions: 0)
Write errors:           0
Total transferred:      24713251 bytes
HTML transferred:       24264251 bytes
Requests per second:    53.50 [#/sec] (mean)
Time per request:       560.712 [ms] (mean)
Time per request:       18.690 [ms] (mean, across all concurrent requests)
Transfer rate:          1291.25 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2  11.5      0      77
Processing:    80  555 152.3    549    1247
Waiting:       75  519 150.7    516    1223
Total:         80  557 153.6    550    1247

Percentage of the requests served within a certain time (ms)
  50%    550
  66%    588
  75%    621
  80%    648
  90%    742
  95%    821
  98%    946
  99%   1091
 100%   1247 (longest request)

I risultati parlano da soli e sono veramente notevoli, in ogni caso ho evidenziato i valori più importanti

  • Numero di richieste gestite al secondo da Apache
  • Tempo medio per l’esecuzione di una richiesta (in concorrenza)
  • Tempo massimo per servire una richiesta

Questi tre valori cambiano nel seguente modo nei tre casi analizzati

Richieste al secondoTempo medio di richiestaTempo richiesta più lunga
Caso 114,9067,103ms3386ms
Caso 244,0622,696ms1449ms
Caso 353,5018,690ms1247ms

Uso di APC come cache a livello utente

Un altro uso che può tornare molto utile di questa estensione è il caching dei dati applicazione. In alcune operazioni l’uso della cache può portare notevoli benefici in termini di velocità, ad esempio quando si eseguono query molto complesse su un database il cui risultato cambia molto raramente nel tempo, oppure quando si caricano dati da un webservice esterno. In tali casi si può memorizzare il dato calcolato in cache attraverso l’uso delle funzioni apposite, e, alla successiva esecuzione dello script verificare se il dato è presente in cache; in caso affermativo, si può restituire il dato preso dalla cache senza il bisogno di rieseguire il calcolo.

Apc può essere usata anche come  backend del componente Zend_Cache. Nei miei progetti in ZF utilizzo sempre il seguente codice in produzione, per evitare di ricaricare i metadati relativi alla struttura delle tabelle nelle classi che fanno uso/derivano da Zend_Db_Table

$frontendOptions = array ('lifetime' => 7200, 'automatic_serialization' => true);
$backend = extension_loaded('apc') ? 'Apc' : 'File';
// getting a Zend_Cache_Core object
$cache = Zend_Cache::factory('Core', $backend, $frontendOptions);
Zend_Db_Table::setDefaultMetadataCache($cache);

In questo modo il componente Zend_Db_Table caricherà i metadati per la tabella che rappresenta (nomi delle colonne, tipi, etc) una sola volta ogni 2 ore per ogni tabella, invece che ad ogni esecuzione. L’incremento di prestazioni che si ottiene con queste poche righe è veramente considerevole.

Gli usi possibili di un’estensione del genere spaziano moltissimo, chiaramente per l’utilizzo all’interno di un applicazione è necessario studiare bene l’impatto che può avere il caching di dati a livello logico, ma se implementata correttamente nei punti chiave di un applicazione, il guadagno che se ne ottiene giustifica ampiamente l’impiego di questo meccanismo.

Un buon metodo per capire su quali operazioni implementare la cache è sicuramente il profiling del proprio codice. Quando avrò un po di tempo scriverò un articolo su come applicare questa strategia.

You can leave a response, or trackback from your own site.

Leave a Reply

Subscribe to RSS Feed