Jun 072013
 
Artículo Administración de Servidores

Un sitio web cuyas páginas son generadas dinámicamente mediante un script CGI puede resultar lento. En este artículo se explica un mecanismo para guardar en una cache en disco las páginas preprocesadas, de manera que cuando vuelven a ser solicitadas, el servidor web las entrega leyéndolas directamente del disco, sin necesidad de volver a generar el contenido mediante consultas a una base de datos o realizando otro tipo de proceso.

Introducción

Cuando un cliente realiza una petición a un servidor web de un sitio dinámico, éste lanza la ejecución de un script CGI que genera el código HTML de la página. El servidor web recoge la salida del script CGI y se la entrega al cliente.

Para implementar una cache, la idea básica es redirigir la salida del script, que normalmente es enviada a standard output, a una variable. Después se envía el contenido de la variable al cliente, y en paralelo se guarda en un fichero cache.

En las sucesivas solicitudes que se reciban, el servidor web comprueba si la página existe en cache, y si es así la entrega al cliente leyéndola del disco. En caso contrario, el servidor web lanza la ejecución del script CGI, que genera la salida, la entrega al cliente y la guarda en cache.

Los ejemplos de código que se presentan en este artículo corresponden a un sitio web dinámico implementado mediante scripts CGI escritos en perl, sobre un servidor web apache que se ejecuta en una plataforma Linux Debian. Con ligeras modificaciones, pueden ser aplicables a otro tipo de servidor.

Redirigir a una variable el código HTML generado por el script

Al comienzo de su ejecución, un script CGI normalmente imprime una cabecera HTTP, y a continuación comienza a imprimir el codigo HTML.

Por ejemplo un script CGI en perl podría incluir las siguientes sentencias :

        # Imprimir la cabecera HTTP
        print "Content-type: text/html; charset=UTF-8\n\n";
        # imprimir el contenido HTML
        print "<html>\n";
        etc...

Para implementar nuestra cache, vamos a modificar este código de modo que el contenido html quede almacenado en una variable, en lugar de ser enviado directamente a standard output.

Para ello, vamos a redirigir la salida inmediatamente antes de empezar a generar el contenido HTML, despues de haber enviado la cabecera HTTP:

        # Variable que va a recibir el código HTML generado por el script
        my $output;
        # Handle de entrada/salida que apunta a la variable $output
        my $stdout;
        #
        # Imprimir la cabecera HTTP
        print "Content-type: text/html; charset=UTF-8\n\n";
        #
        # Abrir un handle de entrada/salida cuyo destino es la variable
        open ($stdout, '>', \$output) or die "No puedo abrir el handle: $!";
        #
        # Redirigir la salida a la variable $output, guardando el handle
        # original en la variable $old_stdout 
        $old_stdout = select $stdout;
        # generar el contenido HTML.
        # El resultado de las sentencias print quedará ahora guardado en la variable $output
        print "<html>\n";
        etc...

Guardar en cache la página preprocesada

Una vez que el script ha terminado de generar el código HTML de la página, debe enviarselo en primer lugar al cliente que lo solicitó. Para ello, se recupera el handle original que había quedado guardado en $old_stdout:

    # Recuperar el handle stdout original
    select $old_stdout;
    # Cerrar el nuevo handle
    close $stdout;
    # Enviar al cliente el código HTML
    print $output;

A continuación procedemos a guardar en un directorio “cache” situado bajo el DocumentRoot de nuestro servidor web el contenido de la página entregada. Como nombre de fichero se utiliza el URI con el que se ha accedido a la página, con extensión “.html”.

En el ejemplo, suponemos que el DocumentRoot es el directorio “/web”, y que hemos creado bajo el mismo un directorio “cache” para contener las páginas:

  use File::Path qw(make_path);

    # Guardar la página en cache
    my $cache_dir = "/web/cache";
    my $cache_filename = $cache_dir . $ENV{'REQUEST_URI'} . ".html";
    #
    # Crear el subdirectorio que va a contener el fichero, si es necesario
    if ($cache_filename =~ m/^(.*)\/[^\/]*$/) {
        $cache_dir = $1;
    }
    make_path($cache_dir);
    #
    # Escribir la página en el fichero cache
    open (my $cache, '>', $cache_filename) or die "Error abriendo el fichero cache $cache_filename: $!";
    print $cache $output;
    close $cache;

También guardamos una versión comprimida del fichero, que será utilizada si el cliente que solicita la página admite recibir datos comprimidos. El nombre del fichero comprimido es igual al nombre del fichero sin comprimir, con extensión “.html_gzip”:

  use Compress::Zlib;

    # Guardar también el fichero comprimido
    my $gz = gzopen($cache_filename . "_gzip", "wb")
              or die "Error abriendo el fichero cache comprimido: $gzerrno";
    $gz->gzwrite($output) or die "Error de escritura en fichero cache comprimido: $gzerrno";
    $gz->gzclose;

Configurar el servidor web para utilizar las páginas en cache

Ahora, debemos configurar el servidor web para hacer uso de la cache.

En el caso de un servidor apache, añadimos las siguientes sentencias al fichero de configuración (o bien, pueden ser incluidas en un fichero .htaccess, si utilizamos este tipo de fichero):

 

# BEGIN Page Cache
<IfModule mod_rewrite.c>
    AddEncoding x-gzip .html_gzip
    <FilesMatch _gzip$>
        ForceType 'text/html; charset=UTF-8'
    </FilesMatch>

    RewriteEngine On
    RewriteCond %{HTTP:Accept-Encoding} gzip
    RewriteRule .* - [E=PG_ENC:_gzip]
    RewriteCond %{REQUEST_METHOD} !=POST
    RewriteCond %{QUERY_STRING} =""
    RewriteCond "%{DOCUMENT_ROOT}/cache%{REQUEST_URI}.html%{ENV:PG_ENC}" -f
    RewriteRule .* "/cache%{REQUEST_URI}.html%{ENV:W3TC_ENC}" [L]

</IfModule>
# END Page Cache

Como vemos:

  • en la líneas 3 estamos indicandole al servidor web que los ficheros con extensión “.html_gzip” son ficheros comprimidos en formato gzip
  • en las líneas 4-6 indicamos el tipo de contenido del fichero, para que el servidor web lo incluya en el encabezado HTTP que envía al cliente
  • en las líneas 9 y 10, definimos una variable de entorno “PG_ENC” si el cliente admite recibir datos comprimidos en gzip
  • por último, en las líneas 11-14 entregamos al cliente el fichero en cache, si se cumplen tres condiciones
    • la solicitud recibida del cliente no es de tipo POST
    • la url no incluye argumentos (es decir no es de la forma “http://www.ejemplo.com/pagina?argumento=valor”)
    • el fichero existe en cache en su versión comprimida o no comprimida, según corresponda

Y con esto ya está implementada nuestra caché de páginas.

Consideraciones adicionales

Aunque la implementación descrita hasta ahora puede ser suficiente para un gran número de sitios web, en algunos casos puede ser necesario tener en cuenta algunas consideraciones adicionales:

Usuarios registrados

Algunos sitios web ofrecen servicios adicionales a los usuarios registrados. Normalmente, una vez que un usuario se ha validado introduciendo su nombre de usuario y contraseña, el sitio web le ofrece unos contenidos distintos y le da acceso a páginas adicionales.

Normalmente, el sitio web entrega al cliente un cookie en el momento en el que el usuario se valida, y utiliza el cookie recibido en los siguientes accesos para indentificar que proceden de un usuario registrado.

La manera más sencilla de tener esto en cuenta en nuestra implementación es impedir que a los accesos que realizan usuarios en sesión se les entreguen páginas de la cache. Para ello, incluimos una condición adicional en el fichero de configuración del servidor web:

RewriteCond %{HTTP_COOKIE} ! usuario_registrado [NC]

En el ejemplo, se supone que el nombre del cookie que el sitio web entrega al cliente cuando el usuario se valida es “usuario_registrado”.

De la misma forma, si el CGI se ejecuta en respuesta a un acceso de un usuario en sesión, no debe generar nuevos ficheros cache.

En perl, los cookies recibidos son accesible mediante la variable $ENV{‘HTTP_COOKIE’}. Una manera elemental de detectar la presencia del cookie  de usuario registrado y evitar generar el fichero cache sería:

if ($ENV{'HTTP_COOKIE'} !~ "usuario_registrado") {
    # generar los ficheros cache...
}

Caducidad de la cache

En la implementación que hemos descrito, una vez que se ha generado un fichero en la cache, el servidor web continuará entregándoselo a los clientes para cualquier nueva petición de la misma página.

Pero puede ocurrir que el contenido de la página deba ser actualizado como consecuencia de cambios en la base de datos, o por cualquier otra causa. El desarrollador del sitio debera implementar un mecanismo que detecte las páginas a las que  afecta un cambio, y elimine los ficheros cache correspondientes, de manera que vuelvan a ser generados en el próximo acceso.

La manera más elemental de implementar el refresco de la cache es mediante un proceso que se ejecute periódicamente (por ejemplo, añadiendo una entrada en el crontab en un sistema Linux) y elimine todos los ficheros de la cache.

Indice de artículos sobre programación en lenguaje Perl

 Publicado por en 11:04 am

 Deja un comentario

(requerido)

(requerido)