Construye un sistema de cache web en tu servidor usando PHP

Con la finalidad de reducir la carga de los servidores y agilizar al mismo tiempo la descarga de nuestras Webs vengo pensando en la idea de implementar algún tipo de caché Web, con la flexibilidad suficiente como para aplicarlo a determinados archivos, y cuya implementación no me obligue a rehacer nuestro CMS. Finalmente he encontrado una manera muy sencilla y flexible a la vez, por medio del uso del buffer de salida de PHP y el almacenamiento de archivos en HTML identificados por nombres codificados en MD5. En realidad es tan sencilla que voy a explicar el funcionamiento básico para aquellas personas que como yo (hace unas horas) piensen que todo esto se soluciona solo con hard o herramientas altamente sofisticadas y caras. Antes de empezar he de decir que desconozco la técnica usada por otros sistemas de caché web, pero la solución que propongo me parece una buena opción para empezar, así que ahí voy. Si álguien conoce un método mejor y que proporcione la misma flexibilidad estaré encantado de conocerlo. 1. El escenario Imaginemos un servidor en el que alojamos Webs dinámicas en PHP, y cuyo contenido se genera al vuelo para cada usuario que las solicita por medio de distintas consultas a una base de datos. Si el número de usuarios conectados es relativamente bajo el servidor tendrá una carga suficiente como para afrontar las peticiones y las ejecutará en un intervalo de tiempo corto, casi igual que si sirviera páginas en HTML. El problema vendría cuando el número de usuarios conectados creciera y aumentaran las peticiones al servidor, lo que terminaría disparando el número de consultas a la base de datos, cálculos y demás y se crearía una carga en el servidor que comprometería su rendimiento. Y poco más o menos es lo que termina ocurriendo tarde o temprano. 2. Caché Web Lo que buscamos es un sistema que permita almacenar en un archivo HTML el resultado final de realizar dichas consultas a la base de datos, y los cálculos de los distintos scripts en PHP que se realizan cuando servimos una Web, de modo que al próximo usuario que solicite la misma página podamos entregarle ese archivo HTML sin necesidad de volver a realizar los cálculos de nuevo, con el consiguiente ahorro de carga en el servidor y el incremento de velocidad de respuesta hacia nuestro usuario. Este sistema es lo que se conoce como caché Web. 3. Modo de operación Cuando un usuario solicite una página del servidor Web comprobaremos si tenemos almacenada esta en un archivo HTML dentro de una carpeta que llamaremos "cache". Si es así le enviaremos el contenido de este archivo sin más, y nuestro trabajo habrá terminado. De lo contrario, si la página solicitada no existe como archivo HTML dentro de la carpeta "cache", ejecutaremos todo el código necesario para generarla, como veníamos haciendo hasta hoy, se la serviremos y nos la guardaremos en formato HTML dentro del directorio "cache", para que si álguien más nos pide por la misma página ya no sea necesario volver a generarla. 4. Caducidad de la caché Un archivo que guardemos en la caché nos valdrá para dárselo a los siguiente usuarios, pero ¿durante cuanto tiempo?. En algún momento actualizaremos nuestra Web, y la caché debería también actualizarse. Para esta tarea hay dos opciones claras: La primera sería eliminar los archivos de la caché que van quedado obsoletos a medida que vamos actualizando la Web, manualmente (no recomendado) o por medio de algún script. La segunda podría ser eliminando todos aquellos archivos de la caché que lleven más de cierto tiempo (horas, días...). En ambos casos al eliminar los archivos obligaremos al servidor a generarlos de nuevo cuando un usuario vuelva a solicitar los contenidos de la Web. La segunda opción puede ser la más sencilla de implementar por medio de una tarea en el cron o un script que corra en segundo plano, pero nunca optimizaremos el rendimiento al máximo, ya que si el tiempo de vida de la caché lo establecemos bajo (minutos u horas) podemos estar eliminando la caché antes de que realmente hayamos realizado cambios, por lo que estaríamos desaprovechando recursos del servidor. Si por el contrario establecemos un tiempo de vida alto (varias horas y días) podríamos estar sirviendo información desactualizada a los usuarios durante mucho tiempo. Está claro que lo más eficiente es refrescar la caché justo en el momento en que realizamos un cambio en alguna de las páginas, y por supuesto solo la parte relacionada con ese cambio, no toda la caché. ¿Cómo hacer esto? En mi caso aun lo estoy pensando, pero se me presenta fácil ya que las modificaciones en nuestras Webs se hacen por medio de un CMS, y siempre podemos reprogramarlo para que elimine los archivos de la caché que están relacionados con la parte de la Web actualizamos. Si esto no fuera así seguramente optaría por crear distintos scripts a los que acudiría tras cada actualización para indicarles qué apartados han cambiado y que en base a eso borrara los archivos correspondientes de la caché. En cualquier caso lo más importante es saber que hemos de refrescar la caché ¡o nuestra web no envejecerá nunca! 5. Manos a la obra: PRIMERA PARTE Supongamos el siguiente script en PHP que bien podría corresponder a la sección de una web convencional:

<?php

echo "Bienvenido a mi Web.\n";

// Cientos de operaciones
...

// Decenas de consultas MySQL
...

// Diversas comprobaciones de archivos y permisos
...

// Multitud de requires e includes (css, jscripts, funciones...)
...

echo "Tardo mucho en cargar pero te acostumbrarás\n";
echo "Bye!";

?>
Ahora vamos a imaginar un efecto meneame o slashdot y ya tenemos el servidor temblando. Así que vamos a aplicarnos la primera parte del caché web que tanto he explicado y que aún no he mostrado.
<?php
echo "Bienvenido a mi Web.\n";

// Habilitamos el buffer de salida
ob_start();

// Cientos de operaciones
...

// Decenas de consultas MySQL
...

// Diversas comprobaciones de archivos y permisos
...

// Multitud de requires e includes (css, jscripts, funciones...)
...

// Recogemos el resultado del buffer y cerramos
$resultado= ob_get_contents();
ob_end_flush();

// Creamos una cadena con el nombre del script que estamos ejecutando
$filename = $_SERVER["SCRIPT_NAME"];
$break = Explode(´/´, $filename);
$filename = $break[count($break) - 1];

// Añadimos a la cadena todas las variables $_POST que se han podido pasar al script
foreach($_POST as $variable => $valor)
	$filename.=$variable.$valor;

// Añadimos a la cadena todas las variables $_GET que se han podido pasar al script
foreach($_GET as $variable => $valor)
	$filename.=$variable.$valor;

// Generamos un nómbre único para referirnos a la cadena que acabamos de crear
$filename=md5($filename).".htm";

// Guardamos el archivo con el nombre generado en la carpeta "cache"
$f = fopen ("cache/".$filename, ´w´);
fwrite ($f, $resultado);
fclose ($f);

// Mostramos el resultado en pantalla
echo $resultado;
echo "Tardo mucho en cargar pero te acostumbrarás\n";
echo "Bye!";

Vamos a explicarlo. Como puede verse antes de realizar el trabajo pesado en el servidor habilitamos el buffer de salida con ob_start(), lo que quiere decir que no se imprimirá nada en pantalla y que todo quedará guardado en memoria para luego poder recuperar el resultado. Al terminar las operaciones leemos el resultado que ha quedado almacenado en ob_end_flush() y deshabilitamos el buffer. En este momento todo el resultado ha quedado guardado en la variable $resultado y procedemos guardarla como un archivo html en la carpeta "cache". El nombre del archivo es importante que sea único y distinto para cada página, pero incluso más, es importante que sea distinto incluso si la página ha sido llamada pasándole unas variables u otras, pues el contenido podría variar en función de ello, y es por eso por lo que añadimos al nombre del script todas las variables y sus valores que se han pasado tanto por medio de POST como por medio de GET. El script podría completarse incluyendo además cookies y otros tipos de variables de entrada. La cadena obtenida por este proceso es como una huella dactilar que identifica muy bien el contenido de la variable $resultado, ya que solo indicando el nombre del mismo script y pasándole las mismas variables obtendríamos un resultado igual. Pero guardar el archivo usando como nombre esta cadena sería arriesgado ya que puede llegar a ocupar varios centenares de caracteres e incluso no ser válida como nombre de archivo. Así es como pensé en el MD5 como una opción de convertir la cadena a una menor de 32 caracteres, única y que además todos ellos son válidos y permitidos como nombre de archivo. Por último creamos el archivo en la carpeta "cache", mostramos el resultado en pantalla y terminamos el script. Antes de continuar cabe decir que el script podría mejorarse mucho pero no es la finalidad de este post. Por ejemplo las variables convendría incorporarlas a la cadena $filename en orden alfabético, ya que sino se generarían archivos distintos en la caché solo porque el orden de las variables cambiara de posición. Por ejemplo "index.php?a=1&b=2" e "index.php?b=2&a=1" se tratarían como dos archivos distintos cuando realmente no lo serían. 6. Manos a la obra: SEGUNDA PARTE Ahora modificaremos el mismo script para que contemple la segunda parte. Es decir, que busque en la caché el archivo que contiene lo que el usuario busca, y que si es así se lo muestre, ahorrándose tener que realizar todos los cálculos que venía haciendo hasta ahora para cada petición.
<?php

echo "Bienvenido a mi Web.\n";

// Creamos una cadena con el nombre del script que estamos ejecutando
$filename = $_SERVER["SCRIPT_NAME"];
$break = Explode(´/´, $filename);
$filename = $break[count($break) - 1];

// Añadimos a la cadena todas las variables $_POST que se han podido pasar al script
foreach($_POST as $variable => $valor)
	$filename.=$variable.$valor;

// Añadimos a la cadena todas las variables $_GET que se han podido pasar al script
foreach($_GET as $variable => $valor)
	$filename.=$variable.$valor;

// Generamos un nómbre único para referirnos a la cadena que acabamos de crear
$filename=md5($filename).".htm";

// Buscamos en la caché si existe un archivo con este nombre
if (is_file("cache/".$filename)){
	require "cache/".$filename;
	}
else {
// Habilitamos el buffer de salida
ob_start();

// Cientos de operaciones
...

// Decenas de consultas MySQL
...

// Diversas comprobaciones de archivos y permisos
...

// Multitud de requires e includes (css, jscripts, funciones...)
...

// Recogemos el resultado del buffer y cerramos
$resultado= ob_get_contents();
ob_end_flush();

// Guardamos el archivo con el nombre generado en la carpeta "cache"
$f = fopen ("cache/".$filename, ´w´);
fwrite ($f, $resultado);
fclose ($f);

// Mostramos el resultado en pantalla
echo $resultado;}

echo "Tardo mucho en cargar pero te acostumbrarás\n";
echo "Bye!";
?>
Como veis se ha movido al principio el bloque que genera el nombre del archivo. Ahora lo necesitamos para poder buscarlo en la carpeta "cache". Si lo encuentra un simple require cargará su contenido y lo sacará por pantalla, terminando ahí el script. De lo contrario realiza todo el proceso de cálculo y guarda el archivo resultante en la cache para futuras ocasiones. Como veis el proceso es bien simple y permite muchas mejoras a partir de este punto. Dejo para el que quiera continuar a partir de aquí el aplicar un sistema de caducidad de la caché según las necesidades de cada uno.

02-05-2008


Comentarios:

Pues yo pienso que:


Suscribirse