Member of The Internet Defense League Últimos cambios
Últimos Cambios
Blog personal: El hilo del laberinto Geocaching

¿Por qué y cómo crear un espacio web "cache friendly"?

Última Actualización: 18 de Abril de 2.000 - Martes

Prácticamente todos los navegadores del mercado van "capturando" en memoria y en disco las páginas HTML, elementos gráficos, etc., que van descargando de la red a medida que el usuario va recorriendo enlaces. Ello es bueno, ya que si el usuario regresa a la página en otro momento encontrará que, por un lado, se carga muy rápido y, por otro, el proveedor de contenidos se verá menos saturado.

En resumen, las ventajas de la tecnología caché son, fundamentalmente:

  • Para el usuario final:
    • Una velocidad de navegación mucho mayor

  • Para el proveedor de contenidos:
    • Una carga menor

  • Para Internet:
    • Menos tráfico y congestión

Como no todo pueden ser ventajas, el empleo de cachés tiene también algunos problemas:

  • Los contenidos pueden ser visualizados por más gente que la que podría parecer, juzgando por los logs del servidor web

  • Cuando un usuario visualiza un objeto obtenido desde la caché, existe la posibilidad de que no esté actualizado.

En este documento vamos a desentrañar algunos de los secretos y trucos necesarios para procurar, en lo posible, que un sitio web sea lo más cache friendly posible.

El protocolo HTTP (Protocolo de Transferencia HiperTexto)

El protocolo que interconecta los clientes y los servidores web se llama HTTP (HyperText Transfer Protocol), y su entendimiento es crucial para poder entender los mecanismos "caché".

Para poder seguir el resto del documento se requieren unas nociones y unas herramientas mínimas. Los lectores famirializados con el idioma Inglés deberían curiosear la especificación HTTP, cuya URL aparece al final de este documento.

Veamos, por ejemplo, qué envía mi servidor web cuando pido las cabeceras asociadas a un documento cualquiera de mi página (lo que yo escribo lo pongo en negrita):

# telnet www.argo.es 80
Trying 195.53.30.2...
Connected to corinto.argo.es.
Escape character is '^]'.
HEAD /~jcea/ HTTP/1.0

HTTP/1.1 200 OK
Date: Fri, 24 Mar 2000 15:24:45 GMT
Server: Apache/1.3.12 (Unix) AuthMySQL/2.20 PHP/3.0.15
Last-Modified: Tue, 15 Feb 2000 19:59:56 GMT
ETag: "30c14-193f-38a9b03c"
Accept-Ranges: bytes
Content-Length: 6463
Connection: close
Content-Type: text/html; charset=iso-8859-1

Connection closed by foreign host.
# 

Las cabeceras que nos responde el servidor son más o menos obvias; "Date" nos dice el momento en que se ha generado la respuesta (típicamente "ahora", a menos que tengamos cachés intermedias). "Last-Modified" nos indica la fecha de la última modificación del documento, y "Etag" es un identificador único de objeto (más sobre esto luego).

Cuando un usuario visualiza mi página, su navegador conserva una copia de la misma en el disco duro del usuario (el tamaño de caché es configurable; a medida que la caché se llena se van eliminado los objetos más antiguos). Si un tiempo más tarde el usuario vuelve a visitar mis mismas páginas, y su caché todavía conserva la copia, el navegador consultará a mi servidor para preguntarle si las páginas han cambiado o no. Si han cambiado, visualiza la versión nueva (y la guarda en la caché otra vez); si no han cambiado, visualiza la copia de la caché, que todavía está actualizada.

Físicamente el procedimiento se implementa utilizando una petición condicional:

# telnet www.argo.es 80
Trying 195.53.30.2...
Connected to corinto.argo.es.
Escape character is '^]'.
GET /~jcea/ HTTP/1.0
If-Modified-Since: Tue, 15 Feb 2000 19:59:56 GMT

HTTP/1.1 304 Not Modified
Date: Fri, 24 Mar 2000 15:39:07 GMT
Server: Apache/1.3.12 (Unix) AuthMySQL/2.20 PHP/3.0.15
Connection: close
ETag: "30c14-193f-38a9b03c"

Connection closed by foreign host.
# 

En este caso el servidor me indica que el objeto no ha sido modificado, por lo que puedo seguir utilizando la versión disponible en mi caché, y cierra la conexión. Si la página hubiera sido modificada, el servidor me la enviaría de nuevo completa, otra vez.

Por eso muchas veces cuando volvemos a una página que hemos visitado recientemente, los gráficos nos van apareciendo "de golpe", no poco a poco como cuando la visitamos por primera vez. Ello es debido a que el gráfico ya lo tenemos, no hay que cargarlo de nuevo, pero el navegador no lo visualiza hasta que haya contrastado con el servidor original que no ha habido ningún cambio.

¿No es genial?.

Lo ideal sería poder evitar, incluso, esas conexiones de "verificación", ya que suponen carga en el servidor y retardan innecesariamente la visualización de los datos. Y ello es perfectamente posible si podemos garantizar que un objeto web no ha sido modificado.

Supongamos que cuando el servidor nos envía un objeto web nos indica también su período de validez. Si volvemos a acceder a ese recurso antes de que haya "caducado" no necesitamos realizar una petición condicional al servidor, ya que sabemos que no ha habido cambios:

# telnet www 80
Trying 195.53.30.2...
Connected to corinto.argo.es.
Escape character is '^]'.
HEAD /cgi-bin/conexiones HTTP/1.0

HTTP/1.1 200 OK
Date: Fri, 24 Mar 2000 16:05:38 GMT
Server: Apache/1.3.12 (Unix) AuthMySQL/2.20 PHP/3.0.15
Cache-Control: private
Expires: Fri Mar 28 16:05:39 2000 GMT
Last-Modified: Fri, 20 Mar 2000 16:05:39 GMT
Connection: close
Content-Type: text/html; charset=iso-8859-1

Connection closed by foreign host.
# 

Aquí nos aparecen dos cabeceras nuevas: "Cache-Control", que veremos más adelante, y "Expires".

En el ejemplo propuesto, se nos indica que la página fue modificada por última vez el día 20, y que es válida hasta el día 28. Si el usuario vuelve a visualizar esa página antes del día 28, su navegador le mostrará directamente la copia de la caché local, si está disponible, sin necesidad de consultar primero con el servidor origen.

Vemos, por tanto, que un uso inteligente de la cabecera "Expires" puede reducir el número de conexiones y la carga en los servidores web, al mismo tiempo que hace que la navegación del usuario sea instantanea. ¡¡La panacea!!.

No Todo es Tan Fácil

Las cosas nunca son sencillas, y menos cuando hablamos de estos temas. En la sección anterior, por ejemplo, se habla de que si el servidor original no envía un "Expires" explícito, realizaremos siempre una conexión para comprobar si el objeto ha sido modificado.

En la práctica el tema es bastante más complicado.

Para reducir el tráfico y aumentar la velocidad de navegación, además de que la mayoría de los objetos web no tienen una cabecera "Expires" explícita, la mayoría de los sistemas de caché realizan un cálculo implícito sobre la "frescura" de un objeto dado. Aunque suele ser algo configurable, suele ser típico asignar una "frescura" en función de cuánto tiempo haya transcurrido desde la última modificación.

Por ejemplo, si tenemos un objeto en caché desde hace 10 días, y cuando se introdujo en la caché se sabía que había sido modificado 150 días antes, es razonable pensar que la página no ha cambiado en los 10 días transcurridos. El sistema caché puede decidir arriesgarse y entregar el objeto sin haber verificado con el servidor original. Si el objeto, en cambio, lleva en caché media hora, y cuando se capturó hacía 10 minutos que se había modificado, la caché verificará que el objeto es "fresco" antes de enviar una copia al usuario.

Obviamente esta cálculo implícito no se realiza si el objeto incluye una cabecera "Expires", por razones obvias. Pero, claro, el 99% de las páginas de Internet no incluyen una cabecera así.

Pueden verse ejemplos y sugerencias de cálculo de "frescura" en el propio documento que define el estándar HTTP/1.1 (enlace en el apéndice), sección 13.

La Cabecera Cache-Control

Existe una cabecera que ya ha surgido, pero que no se ha explicado aún: "Cache-Control".

"Cache-Control" es una cabecera que puede emplearse tanto hacia el servidor como en su respuesta, con significados distintos.

Navegador -> Servidor Servidor -> Navegador
  • no-cache
    El objeto debe validarse completamente antes de ser enviado al usuario.

  • no-store
    Evita el almacenaje del objeto en disco para, por ejemplo, que no quede capturado en un backup.

  • max-age
    El cliente está dispuesto a aceptar objetos en caché cuya edad no supere el tiempo especificado. Si se ha superado dicho tiempo, la caché debe revalidar el objeto a menos que también se haya especificado "max-stale"

  • max-stale
    Si se indica esta cabecera, el cliente está dispuesto a aceptar objetos expirados. Si se especifica un tiempo, ese valor indica "cómo de expirado" puede estar el objeto.

  • min-fresh
    El cliente demanda un objeto cuya "frescura" sea, al menos, la indicada. Es decir, que tenga una garantía de validez durante un tiempo mínimo indicado.

  • no-transform
    No debe transformar el objeto (por ejemplo, comprimirlo)

  • only-if-cached
    Devuelve el objeto sólo si está en la caché, sin intentar revalidarlo. Si el objeto no está en caché, dá un error.
  • public
    Se puede hacer caché, aunque el objeto normal fuese normalmente "no cache" o para almacenar exclusivamente en cachés no compartidas.

  • private
    El objeto o parte de sus cabeceras no deben almacenarse en una caché compartida (por ejemplo, no deben almacenarse las cookies, pero sí el objeto propiamente dicho).

  • no-cache
    El objeto o parte de sus cabeceras no deben emplearse para responder peticiones subsiguientes, sin una revalidación completa.

  • no-store
    Evita el almacenaje del objeto en disco para, por ejemplo, que no quede capturado en un backup.

  • no-transform
    No debe transformar el objeto (por ejemplo, comprimirlo)

  • must-revalidate
    La caché debe revalidar el objeto si no considera que sea "fresco", aunque se haya configurado para trabajar con objetos expirados. Es decir, si el objeto se considera expirado en función de "Expires" o "Max-Age", la caché debe revalidarlo, sin hacer un cálculo "implícito" de frescura.

  • proxy-revalidate
    Como la cabecera anterior, pero sólo es aplicable a cachés compartidas.

  • max-age
    Similar a la cabecera "Expires".

  • s-maxage
    Como "max-age", pero esta cabecera sólo es aplicable a cachés compartidas.

La directiva "Cache-Control" es válida para aquellos sistemas compatibles con HTTP/1.1. Los sistemas HTTP/1.0, por ejemplo, no disponen de esta directiva. Se puede emplear la directiva "Pragma: no-cache", por ejemplo.

La directiva "Cache-Control" es importante y compleja, y se remite al lector interesado al propio estándar HTTP/1.1, sección 14.9.

Por ejemplo, cuando un cliente desea actualizar una página web que cree que puede haber cambiado y piensa que existe una caché intermedia que está entregando una versión anticuada, puede pulsar sobre el botón de recarga teniendo pulsada la tecla "SHIFT". Ello hace que su navegador inicie una petición con "Cache-Control: no-cache" y/o "Pragma: no-cache" (por compatibilidad), de forma tal que la caché deba recargar el objeto del servidor original.

Si no queremos que el Internet Explorer haga caché de un objeto, hay que añadirle una cabecera "Expires: -1". Lo bueno es que esta cabecera parece funcionar perfectamente también sobre Netscape y cualquier caché conocida.

Generación de Cabeceras

Hemos visto que el rendimiento de la navegación puede mejorarse enormemente empleando de forma inteligente sistemas de caché, para lo cual es preciso una cierta cooperación entre el servidor original, y los usuarios y cachés intermedias que puedan existir. Dicha cooperación se especifica mediante el intercambio de cabeceras, como se ha indicado con anterioridad.

La cuestión ahora es ¿Cómo generar dichas cabeceras?.

  • Objetos dinámicos

    Estos objetos se generan a través de algún tipo de CGI, PHP, ASP, etc., por lo que ya tenemos un punto de anclaje donde introducir las cabeceras. Es decir, si el objeto es devuelto a través de un programa, sólo tenemos que modificar dicho programa para que incluya las cabeceras que deseemos al principio del objeto.

    Es importante recordar que la mayoría de los servidores web no incluyen una cabecera "Last-Modified" cuando el objeto se genera de forma dinámica, lo que casi siempre implica que el objeto no es apto para caché a menos que incluya una cabecera "Expires" explícita, por ejemplo. Asimismo, el objeto no se podrá revalidar.

  • Objetos estáticos

    La forma más sencilla, a priori, consiste en transmitir dichos objetos a través de un contenedor dinámico que introduzca las cabeceras correspondientes. Este sistema es perfectamente usable y resulta muy portable.

    Pero la mayoría de los servidores web disponen de algún tipo de configuración para introducir cabeceras adicionales en la respuesta. Por ejemplo, en UNIX es muy típico el fichero ".htaccess", en el que se pueden poner cabeceras extra, entre otras muchas cosas (ver la documentación de cada servidor web; por ejemplo, Apache).

    Apache tiene, por ejemplo, una configuración global para las expiraciones, sin usar el fichero ".htaccess". Se trata del módulo "mod_expires". Este módulo permite devolver una cabecera "Expires" explícita en función del tipo de objeto de que se trate. La expiración puede tomar como base tanto la fecha de la última modificación del objeto como el instante de petición. Puede consultarse la documentación correspondiente en los enlaces al final de este documento.

Información Adicional

Historia

  • 18/Abr/00: Cálculo implícito de "frescura", cabecera "cache-control" y generación de cabeceras. Añadidos un montón de enlaces.

  • 24/Mar/00: Primera versión de este documento.



Python Zope ©2000 jcea@jcea.es

Más información sobre los OpenBadges

Donación BitCoin: 19niBN42ac2pqDQFx6GJZxry2JQSFvwAfS