Costruire un archivio con PHAR per farne una copia statica, o usare il dbms?

Mi è stato chiesto di fare una versione statica di una parte di sito (una sottocartella), così ho preso wget e scaricato i file (wget -nc -r http://host.domain/directory).

La parte in questione è una applicazione PHP/MySql che usa mod_rewrite, così il numero di file è effettivamente molto alto.

Ora il problema sarebbe quello di gestire tutti questi file rimettendoli nel sito: di fatto i server ftp limitano il numero di file visualizzabili tramite il comando list, così potrebbe non essere accessibile parte del filestestem, quando effettivamente è solo non visibile.

Altro punto è che avere accesso ai file statici (la copia) non è di nessuna utilità, quindi è inutile che siano visibili.

Ho quindi usato la poco conosciuta estenzione PHAR del php.

Di fatto non si trovano molte informazioni, la conoscevo in quanto all’esame si deve accennare all’esistenza, ma non ci sono domande a riguardo.

Ho trovato questo articolo della IBM: http://www.ibm.com/developerworks/library/os-php-5.3new4/index.html?S_TACT=105AGY75

Il codice che ho usato per creare l’archivio è:

<?php

$phar = new Phar('forum.phar', 0,'forum');

$phar->buildFromDirectory(dirname(__FILE__). '/forum');

 

Quindi questo pacchetto va usato in qualche modo, ed ora vengono le perdite di tempo per capire come usarlo.

Cercare su internet ed arrivare a questo articolo, https://stackoverflow.com/questions/17648948/how-does-mod-rewrite-work-with-phar , è fuorviante non è esattamente questo l’uso di cui ho bisogno.

Per usare un archivio nella maniera utile a me (a ‘mo di archivio, appunto), è sufficiente definire in .htaccess (o qualsiasi cosa abbiate) una rules semplicissima:

RewriteRule ^ index.php [QSA]

(la sola in .htaccess di questa cartella, manca anche il flag Last perché non se ne ha bisogno), e definire index.php così:

<?php

$requri = $_SERVER['REQUEST_URI'];
$wanted = str_replace('/forum','',$requri);

include 'phar://forum.phar'.$wanted;

Avendo nella cartella i soli file:

.htaccess
index.php
forum.phar

Ora le cose possono essere più immense di quanto ci si potrebbe aspettare, e il file essere veramente ingestibile (no caricabile). L’opzione è quella di comprimerlo, no problem, non bisogna ripetere la creazione di nuovo, il constructor accetta il nome del file .phar e lo apre se già esiste, cosicché basta fare:

<?php

$phar = new Phar('forum.phar', 0,'forum');

$phar->compress(Phar::BZ2);

 

per così veder creato il file forum.phar.bz2

Resta comunque il fatto che se i file sono molti si arriva ad un archivio di qualche centianaia di mega, considerato che il php spesso è un modulo che gira sopra apache, considerato che è interpretato, considerato che la memoria è limitata (sia da php, sia da apache, sia dal sistema operativo), non è affatto agevole far gestire questa quantità di dati per poter servire velocemente i contenuti.

Non resta quindi che affidarsi ad una tecnologia ben rodata: il DBMS.

Bene, visto che le url sono sia per pagine html, sia per le immagini, è meglio usare un blob per mettere tutto nel db:

<?php

$directory = dirname(__FILE__);
$directory.= '/pathwheredownloaded/forum';
$add_todir = '/forum';

$iter = new RecursiveIteratorIterator(
				      new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
				      RecursiveIteratorIterator::SELF_FIRST,
				      RecursiveIteratorIterator::CATCH_GET_CHILD // Ignore "Permission denied"
);

require('db.class.php');

$db = DB::getInstance();
function db_insert($key,$text) {
  global $db;
  $db->query("INSERT INTO thestatic_great_table(pathuri,content) VALUES(:pathuri,:content)",
	     array(':pathuri'=>$key,':content'=>$text));
}

foreach($iter as $path => $fileInfo) {
  $pathEp = str_replace($directory,'',$path);
  $pathEp = $add_todir . $pathEp;
  if($fileInfo->isFile()) {
    $c = file_get_contents($path);
    db_insert($pathEp,$c);
  }
}

Questo mette tutto nel db facendo un loop ricorsivo nella cartella directory ($add_todir è una aggiunta al path perché nel server viene servita come sotto cartella, cioè la request uri contiene anche ‘/forum’).

Importante

La definizione della tabella dovrebbe avere un indice su pathuri così da rendere la ricerca veloce, questo però rende anche l’inserimento nella tabella molto lento, così è sicuramente meglio definire l’indice dopo aver eseguito il loop iterativo sulle directory per inserire i dati. Per riferimenti: https://stackoverflow.com/questions/3688731/is-it-better-to-create-an-index-before-filling-a-table-with-data-or-after-the-d

Altra questione fondamentale, e comunque il relazione alla creazione dell’indice, è far si che le url siano univoche. Non conosco le restrinzioni w3c riguardo le url, e probabilmente il limite standard dei varchar (255 caratteri), è sufficiente, ma per esserne certi è bene eseguire preventivamente qualcosa del tipo:

find . | awk '{ if(length($0)>255) print "NOuOuO", $0;}'

questo da farsi nella cartella …/…./pathwheredownloaded/ tira fuori un bel NOuOuO e la url solo nel caso la path relativa superi i 255 caratteri, a dir la verità c’è anche un punto davanti, sarebbe ./forum/… quindi la cosa corretta è 256, ma il test io preferisco farlo addirittura con 230, ed è passato. Bene.

Per poi servire le pagine così (index.php):

<?php
require_once('db.class.php');

$url = $_SERVER['REQUEST_URI'];

$db = DB::getInstance();

if(preg_match('/jpeg$/',$url)) {
  header('Content-type: image/jpeg');
}
if(preg_match('/jpg$/',$url)) {
  header('Content-type: image/jpeg');
}

$r = $db->query("SELECT pathuri,content FROM thestatic_great_table WHERE pathuri=:uri",array(':uri'=>$url));
$r->bindColumn(1,$returi);
$r->bindColumn(2,$content);
$r->fetch();
print $content;
// remember ... http://www.php.net/manual/it/pdostatement.fetch.php#84321

una nota riguardo il fetching di dati pdo è nel link sopra. (db.class.php è una classe singleton che uso io perché pdo per me non è così rapido per alcune best practices).

Sul content type può capitare che non basti solo controllare l’estenzione, nel mio caso ad esempio ho una url di questo tipo “.*posted_img_thumbnail.php\?.*” che rappresenta sempre una immagine, quindi devo aggiungerlo.

Inoltre i fogli di stile vanno forniti con content type text/css. E ci saranno comunque aggiustamenti da fare.

E così si finisce per avere un bel db da 2giga da buttar su, una chiave su pathuri, e se non fosse abbastanza efficiente si possono definire anche delle partizioni, ma a me sembra ok.

p.s.: non mi va di rivedere tutto, effettivamente ho iniziato a scrivere questo articolo quando pensavo di aver fatto il grab correttamente ed aver bisogno di 2giga di dati, ma il grab non era corretto, mancavano parecchi dati. Risultato: 9 giga! ma va bene lo stesso.

Postum p.s.

Un db di 11 giga rende palesi delle problematiche che solitamente non si considerano, e così è stato in questo caso. Il formato di storage, o storage engine, del db, di default ultimamente per i server mysql è InnoDB che ha indubbi vantaggi per quanti riguarda le features a disposizione, come il supporto di transazioni, triggers, stored procedures, cursori (ci sono davvero?), etc. Funzionalità ormai richieste da chiunque usi un database, ma tutto ciò ha un costo dal punto di vista delle performance ed delle ottimizzazioni che non sono più disponibili. È bastato cambiare lo storage engine in formato MyISAM e l’occupazione nel filesystem è passata da 12 Giga a 8 Giga.

Inoltre è possibile ottenere ancora di più: se la tabella non deve essere modificata si può utilizzare il comando myisampack che permette di occupare ancora meno spazio mantenendo le performance. E questo è decisamente il caso.

Peripezie

Il trasferimento verso l’hosting è sicuramente qualcosa di problematico se si ha un managed hosting, cioè una macchina gestita da altri, che sicuramente gestiscono un certo numero di macchine e quindi avranno un carico di lavoro elevato, e spesso, visto l’ignoranza diffusa in campo informatico, hanno a che fare con gente poco preparata a cui rispondono malvolentieri. Cosa è trasferire una tabella db da 10 giga erso un server? praticamente è fare l’esportazione dei dati tramite mysqldump e fare l’importazione successiva tramite mysql, oppure, sfruttando il fatto di utilizzare uno storage engine piuttosto povero, semplicemente trasferire 3 file, tabella.frm, tabella.MYD e tabella.MYI, come descritto in https://stackoverflow.com/questions/1960845/mysql-backup-can-i-copying-individual-myisam-table-files-to-another-server-with (con note riguardo la compatibilità di versione)

Conclusioni

È piuttosto comune pensare che la cosa più veloce per servire pagine web sia il filesystem, effettivamente vedendo girare questo sito le performance raggiunte grazie al supporto del DBMS sono eccezionali. L’idea che il filesystem sia la cosa migliore tiene conto del numero di sottosistemi coinvolti ma non della mole di lavoro che essi svolgono:

  1. Caso DBMS: apache -> php -> mysql -> filesystem con seek statica ( e ritorno <- )
  2. Caso fs: apache -> filesystem (e ritorno <- )

Infatti nel caso 1 il server dbms riesce ad ottenere il dato molto velocemente solo mantenendo un indice di 25 Mbyte in ram, rendendo la richiesta verso il filesystem praticamente una seek ad un indirizzo specifico, mentre una richiesta al filesystem richiede un attraversamento delle cartelle, cioè l’individuazione di un inode (la cartella), lettura del suo contenuto, individuazione del file, lettura del suo contenuto, e così via, ripetendo fino al file oggetto della richiesta. Nel caso 2. sono tutte richieste di I/O, non pesanti dal punto di vista operazionale, ma pesanti dal punto di vista di tempi di risposta.

Con gli SSD le differenze si mitigano, ma l’occupazione del bus di I/O è già di suo un elemento a sfavore della scelta 2.