PHP 5.3 su Red Hat Enterprise Linux (RHEL) 4

Un cliente che utilizza ancora RHEL 4 ci ha chiesto di predisporre il server web per ospitare, oltre all’applicazione attuale, anche una nuova applicazione basata su Drupal 7.x e quindi PHP 5.3.
La necessità di supportare contemporaneamente PHP 4.3 (utilizzato dalla web application attuale) e 5.3 (per Drupal) ci ha imposto delle notevoli restrizioni: non era possibile installare i pacchetti dai repository legacy esistenti perché avrebbero sovrascritto le precedenti versioni, quindi le possibilità erano due: ricompilare Apache e PHP, oppure estrarre il contenuto dei pacchetti esistenti e creare un’installazione parallela che sfruttasse quanto più possibile il sistema già installato, divergendo solo ove necessario. Abbiamo optato per questa seconda soluzione.
Non disponendo il cliente di un ambiente di test, la prima mossa è stata creare una virtual machine CentOS 4 i386 su cui effettuare tutte le prove del caso. Abbiamo installato un sistema minimale ed aggiunto i pacchetti php, php-gd e php-mysql che sapevamo presenti in staging e produzione.
A questo punto abbiamo scaricato sulla nostra VM il repo file di un repository che disponeva dei pacchetti PHP 5.3 per RHEL 4 e l’abbiamo utilizzato per ottenere la lista degli rpm da scaricare:

# wget http://rpms.famillecollet.com/enterprise/remi.repo
# mv remi.repo /etc/yum.repos.d/
# wget rpms.famillecollet.com/RPM-GPG-KEY-remi
# rpm --import RPM-GPG-KEY-remi
# yum --enablerepo=remi install php php-gd php-mysql php-pdo
[...]

Abbiamo preso nota dei pacchetti che venivano installati dal repository e li abbiamo scaricati in una directory per estrarne i contenuti:

# mkdir php5-rpms
# cd php5-rpms
# wget http://repo.famillecollet.com/enterprise/4 [...]
# mkdir contents
# cd contents
# for file in ../*.rpm ; do rpm2cpio $file | cpio -id ; done
[...]
# ls
etc/  lib/  usr/

Disponevamo dei binari e dei file di configurazione base necessari, che altro ci serviva?

  • Uno script di init per la seconda istanza di Apache e relativo file di configurazione in /etc/sysconfig/
  • Una ServerRoot separata per la seconda istanza di Apache in cui mettere tutti i file di configurazione
  • Un php.ini e una directory php.d separate per la configurazione di PHP
  • Una directory separata per le estensioni PHP in modo che non sovrascrivessero le vecchie

Partiamo impostando un riferimento alla directory in cui abbiamo estratto gli RPM, poi facciamo delle copie dei file attuali ed aggiorniamo la ServerRoot di Apache con i file estratti e ci assicuriamo che la directory dei log sia diversa da quella dell’istanza di default:

# FILES_REPO=/root/php5-rpms/contents
# cp /etc/init.d/httpd /etc/init.d/httpd-php53
# cp /etc/sysconfig/httpd /etc/sysconfig/httpd-php53
# cp -a /etc/httpd /etc/httpd-php53
# cp -a ${FILES_REPO}/etc/httpd/* /etc/httpd-php53/
# mkdir -p /var/log/httpd-php53
# chmod 700 /var/log/httpd-php53
# cd /etc/httpd-php53
# rm logs
# ln -s ../../var/log/httpd-php53 logs

Poi è la volta di PHP. Copiamo il modulo Apache, i relativi file di configurazione (in una nuova directory creata ad-hoc) ed estensioni (anch’esse in una nuova directory) e qualche binario che non dovrebbe sovrascrivere quanto abbiamo già installato:

# cp ${FILES_REPO}/usr/lib/httpd/modules/libphp5.so /usr/lib/httpd/modules/
# mkdir -p /etc/php5
# cp -a ${FILES_REPO}/etc/php.ini ${FILES_REPO}/etc/php.d /etc/php5/
# cp -a ${FILES_REPO}/usr/lib/php /usr/lib/php5
# cp -i ${FILES_REPO}/usr/bin/phar* /usr/bin/
# cp -i ${FILES_REPO}/usr/bin/php-cgi ${FILES_REPO}/usr/bin/phpize /usr/bin/

Manca poco, dobbiamo aggiungere un paio di librerie necessarie ai moduli.

# cp ${FILES_REPO}/usr/lib/libedit.so.0.0.27 /usr/lib/
# cd /usr/lib
# ln -s libedit.so.0.0.27 libedit.so.0

# cp -i ${FILES_REPO}/usr/lib/mysql/libmysqlclient.so.18.0.0 /usr/lib/mysql/
# cd /usr/lib/mysql
# ln -s libmysqlclient.so.18.0.0 libmysqlclient.so.18

# ldconfig

Gli ingredienti ci sono tutti, dobbiamo solo farli “parlare” tra loro. La prima cosa che modificheremo è lo script di init (riporto solo le parti in cui è stato modificato rispetto al default):

if [ -f /etc/sysconfig/httpd ]; then
        . /etc/sysconfig/httpd-php53
fi
[...]
prog=httpd-php53
pidfile=${PIDFILE-/var/run/httpd-php53.pid}
lockfile=${LOCKFILE-/var/lock/subsys/httpd-php53}
[...]
start() {
        echo -n $"Starting $prog: "
        check13 || exit 1
        LANG=$HTTPD_LANG PHP_INI_SCAN_DIR=/etc/php5/php.d daemon $httpd $OPTIONS
[...]

Come vedete le righe modificate sono veramente poche. Abbiamo cambiato il nome del file di configurazione da includere, fatto in modo di creare nomi univoci per il pidfile ed il lockfile e poi abbiamo modificato la linea di comando per includere una variabile che specifica a PHP dove reperire i file .ini relativi alle estensioni.

Nel file di configurazione /etc/sysconfig/httpd-php53 abbiamo cambiato solo le opzioni in modo da specificare una ServerRoot diversa:

OPTIONS="-d /etc/httpd-php53"

A questo punto siamo passati a configurare Apache, prima il file /etc/httpd-php53/conf/httpd.conf cambiando solo ServerRoot e PidFile in modo coerente con quanto avevamo fatto nello script di init e nel sysconfig:

ServerRoot "/etc/httpd-php53"
[...]
PidFile run/httpd-php53.pid

Poi siamo passati a fare un ritocco alla configurazione di PHP in /etc/httpd-php53/conf.d/php.conf, aggiungendo la direttiva PHPINIDir che specifica dove reperire il php.ini:

LoadModule php5_module modules/libphp5.so
PHPINIDir /etc/php5/

Il passo successivo riguarda proprio il php.ini (quello nuovo che abbiamo copiato in /etc/php5/php.ini), in cui dovremo cambiare la direttiva extension_dir:

extension_dir = "/usr/lib/php5/modules/"

A questo punto creiamo uno script php di test e riavviamo il servizio per verificare che tutto sia a posto:

# echo '<?php phpinfo() ?>' > /var/www/html/stardata-test-php53.php
# service httpd-php53 restart
# curl http://localhost/stardata-test-php53.php
[...]

Nell’output dovreste vedere che la versione di PHP è la 5.3 e le estensioni vengono caricate da /usr/lib/php5

Passaggio efficiente di record tra client e server

In un protocollo client-server, quando viene eseguita una query, il server ha due modi per inviare i record trovati al client: uno alla volta o tutti insieme. Il modo effettivamente usato dipende dalle richieste del client.

Programmando MySQL con un linguaggio evoluto come Perl o PHP, la differenza non si coglie, perché il modo di ricezione attivato per default è il “tutti insieme”, cioè il server invia i record in un unico array che viene quindi trasferito alla memoria del client.

Chi si è preso la briga di guardare dietro le quinte del driver usato da Perl e PHP, però, ha visto che questi linguaggi si appoggiano alla libreria di funzioni scritta in C, che ha la capacità di differenziare il tipo di invio dei record. La libreria delle API (Application Programming Interface) di MySQL ha due funzioni per questo compito: mysql_use_result e mysql_store_result.

mysql_store_result è il comportamento più comodo per il programmatore, perché trasporta tutti i record ritrovati in una sola mandata. In questo modo il programma ha a disposizione due funzionalità che di solito si danno per scotate, ma non lo sono: il numero di record ritrovati e la possibilità di saltare avanti e indietro nella lista dei record.

mysql_use_result invece istruisce il server perché invii i record uno alla volta. Questo comportamento, che a prima vista potrebbe sembrare illogico e indesiderabile, trova fondamento nei casi in cui si debbano leggere grandi quantità di record, quando è meglio prendere i record uno per uno, farci quel che ci si deve fare (per esempio stamparlo o inviarlo a un file) e passare a quello successivo.

Poiché mandare i record tutti insieme richiede il costo aggiuntivo dell’occupazione di memoria per un array (la cui creazione richiede più tempo della semplice occupazione dello spazio di un singolo record) quando si manipolano grandi quantità di record (parlo di decine di MB), allora la gestione uno-alla-volta si dimostra più veloce.

Per contro, mysql_use_result ha lo svantaggio di non poter dire quanti record sono stati trovati finché tutti sono stati trasferiti dal server al client.

Comunque, per quantità di record limitate (dipende anche dalle risorse della macchina) che coprono la maggior parte delle applicazioni, mysql_store_result è il metodo da preferirsi.

Ma, mi sento dire, se l’uso di queste funzioni si stabilisce in base al presunto numero di record e il programmatore non può conoscere il numero prima dell’esecuzione della query, com’è possibile prendere una decisione?

Potrei dire che i programmatori bravi sanno a priori se la query che stanno per eseguire è di quelle da tremila record o da tre milioni, ma a parte quelli che hanno queste intuizioni difficilmente trasferibili ad altri, se il vostro database ha una gran quantità di record e avete paura che un risultato ve ne porti qualche milione sul client, esaurendo tutta la memoria se usate mysql_store_result, potete sempre sondare il database con una query di conteggio.

Per esempio, prima di eseguire


SELECT autore, titolo FROM libridiamazon WHERE autore LIKE "A%";

potete sondare il database con questa:


SELECT COUNT(*) FROM libridiamazon WHERE autore LIKE "A%";

Se il conteggio è nell’ordine delle migliaia, usate mysql_store_result. Se invece si va oltre i diecimila, forse è il caso di scegliere mysql_use_result. Volete sapere come gestire questo doppio comportamento da Perl e PHP?