martes, 28 de diciembre de 2010

Cabeceras de cache

Hoy vamos a hablar de la cache.

No de la cache de ASP.NET, ni de la cache de JSP ni PHP, sino de la cache a secas porque primero hay que sembrar para después recoger.

Cuando un navegador solicita un recurso al servidor existen 3 distintas formas de llevar a cabo la petición.

De menos a más costosa:

  • Usar la versión local cacheada del recurso.
  • Enviar una petición HTTP condicional al servidor solicitando el recurso sólo si es distinta de la versión local cacheada.
  • Enviar una petición HTTP al servidor solicitando el recurso.
    • Esto puede suceder por distintas razones, pero a grandes rasgos se solicitará el recurso al servidor bien porque no se dispone de una versión local cacheada, bien porque aunque se disponga de ella, ésta ya ya ha expirado o bien porque hemos sido instruidos por el servidor a través de una petición HTTP condicional previa, para renovar el recurso que se tenía en la cache local.

Durante la solicitud del recurso al servidor por parte del cliente, algunas de las cabeceras HTTP más habituales que se puede enviar son:

  • Pragma: no-cache
    • Esta cabecera indica que cuando el cliente solicita al servidor una nueva copia del recurso, no espera recibir ninguna versión cacheada del recurso ya sea desde el propio servidor o desde alguno de los proxys que pudiera aparecer en su camino.
    • Esta cabecera está en desuso y no debería utilizarse ni confiar en ella. Si aun se soporta (y no todos los clientes y servidores lo hacen) es por mantener cierta retro compatibilidad.
  • If-Modified-since: <fecha/hora>
    • El servidor solo debería devolver el recurso solicitado si ha sido modificado después de la fecha y hora suministrada.
      • El formato de la fecha/hora es el de RFC822.
        Por ejemplo, Fri, 9 Aug 2002 21:12:00 GMT
  • If-None-Match: <Etag>
    • El servidor solo debería devolver el recurso solicitado si el ETag actual del recurso en el servidor es distinto del ETag suministrado por el cliente.

Un ETag (entity tag) es un identificador único asignado a una versión concreta de un recurso del servidor (normalmente su valor se obtiene a partir de una función de hash aplicada al recurso). Siendo así, cualquier cambio en el recurso supone un cambio en su ETag y entonces comparando ETags podemos validar si el recurso ha cambiado.


Si la petición del recurso devuelve un código de estado HTTP/304 Not Modified, el servidor le está comunicando al cliente que el recurso solicitado no ha cambiado y que puede utilizar su versión del recurso local cacheado (esto podría suceder si hemos realizado una petición condicional con If-Modified-Since o If-None-Match). En caso contrario el servidor devolverá una nueva copia del recurso y el navegador cliente deberá desechar su antigua copia de cache por la nueva copia que recibe. Además y en función de las cabeceras incluidas en la respuesta junto al recurso solicitado, el cliente podrá o no almacenar el nuevo recurso en su cache local o desecharlo automáticamente.

Una petición condicional es normalmente una excelente solución para optimizar nuestra aplicación. Sin embargo, no debemos olvidar que aunque la solicitud y respuesta sólo han incluido cabeceras (esto es que la respuesta no incluye ningún cuerpo), no deja de producirse un roundtrip al servidor (viaje de ida y vuelta). En cualquier caso, es más optimo que obtener siempre una nueva copia del recurso solicitado.

 
Un cliente sabe si tiene o no que almacenar una copia del recurso solicitado en cache local y durante cuánto tiempo, en función de las cabeceras incluidas en la respuesta por parte del servidor.

Las cabeceras de respuesta más habituales para el establecimiento de directivas de cache por parte del servidor, son las siguientes:

  • Expires: <fecha/hora>|<-1|0>
    • Si se especifica un valor de fecha y hora se indica el momento hasta el que se considera válido el recurso en la cache local.
    • Cualquier valor distinto de fecha y hora (normalmente 0 o -1) indica que el recurso expira automáticamente y no debe guardarse en ninguna cache.
    • Esta cabecera está obsoleta y debería no utilizarse.
  • Cache-Control:
    • public
      • La respuesta podría ser cacheada en cualquier cache, incluyendo caches compartidas de usuario (por ejemplo un proxy).
    • private
      • La respuesta podría ser cacheada en cualquier cache que sólo pertenezca a un usuario (por ejemplo un navegador).
    • no-cache
      • La respuesta no puede ser cacheada en ningún cache.
    • no-store
      • Igual que no-cache no puede ser cacheada en ningún cache y además no puede ser escrita en disco (normalmente por temas de seguridad).
    • max-age=segundos
      • Sólo durante los siguientes “segundos” se podría reutilizar la copia local cacheada.
    • must-revalidate
      • El recurso solicitado podría ser cacheado, pero siempre se debería contactar con el servidor para verificar que el recurso aún es válido.

Realmente Cache-Control es la cabecera principal si hablamos de caché. Su definición completa la puedes encontrar en http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

Cuando elegir Public y cuando Private depende del recurso servido. Si el recurso es algo que no específico a ningún usuario (por ejemplo la imagen del logo de la empresa) podría ser Public (es decir, lo pida quien lo pida es el mismo logo). Sin embargo, si lo que estamos cacheando es un recurso que contiene información relativa a un usuario en concreto (por ejemplo la página de inicio de nuestra aplicación que lleva su nombre para acceder a su perfil) pues entonces tendrá que ser cacheado como Private ¡no queremos que un usuario X reciba la página con la información del usuario Y!.

Cuando hablamos de proxys nos referimos a servidores que puede encontrase en el camino una petición, desde el cliente al servidor o viceversa, y que pueden inspeccionar y, opcionalmente transformar, la petición o la respuesta.

 

Un proxy puede ser de 2 tipos : forward proxy (cerca del cliente) o reverse proxy (cerca del servidor).

 

Un proxy de tipo forward es que ponen las empresas antes de que el tráfico de su intranet salga a internet. Las tareas más habituales de este tipo de proxy son:

  • Cachear recursos. Por ejemplo, si toda mi empresa necesita acceso a facebook, voy a cachear (si Cache-Control: Public) ciertos recursos de facebook para acelerar la navegación de mis empleados.
  • Control de acceso. No voy a dejar navegar a mis empleados a twitter, que se pasan el día allí…
  • Auditoría. Voy a registrar la navegación de mis empleados para luego tirarles de las orejas.
  • Eliminar información sensible. Por ejemplo la cabecera Referer que podría apuntar a recursos sensibles en la intranet.

Un proxy de tipo reverse los ponen las aplicaciones para liberar de carga al servidor de la aplicación. Por ejemplo, algunos tareas que podrían cumplir este tipo de proxys son:

  • Cachear recursos. Como resulta que mi aplicación tiene mucho éxito, para mejorar la salud y rendimiento del servidor principal voy a cachear .js, .css y cosas así en un servidor proxy y así no llegan ni al servidor principal, que tiene cosas más importantes que hacer…
  • Realizar tareas que aligeren la carga del servidor. Por ejemplo, si hay que comprimir en gzip, que lo haga el proxy, si hay que encriptar/desencriptar porque tenemos HTTPS, que lo haga el proxy, que de nuevo, el servidor principal tiene otras más importantes que hacer…
  • Balancear la carga de peticiones entre distintos frontales en función se su carga actual de trabajo.

Como ves, distintas características del protocolo HTTP puede ser arquitecturadas en capas gracias a la simpleza de los peticiones y respuestas (mensajes en texto plano fáciles de parsear y manipular). De este modo, los distintos servicios que puede ofrecer un proxy (sea del tipo que sea) son muy variopintos.


En cualquier caso, para que un recurso puede ser cacheado se debe encontrar una definición completa de cómo y hasta cuándo cachearlo.

Es decir, si recibimos una cabecera Cache-control sabremos “el cómo” pero si no va acompañado de una valor de expiración relativo al tiempo, por ejemplo Cache-control: max-age, Expires o similar, no sabremos “hasta cuándo” y tampoco podremos hacer una petición condicional si no recibimos una cabecera que lo permita (ETag o Last-Modified). Si esto sucede, estaríamos incurriendo en un problema de incoherencia con nuestras cabeceras de cache y el cómo cachear los recursos será entonces decisión del navegador de turno y hasta cuándo será también decisión del modulo heurístico del propio navegador (esta solución es jugártelo todo a blanco o negro y no nos gusta).

De hecho quiero hacer especial mención al módulo heurístico del navegador en lo relativo a la cache local. En este sentido, a nadie se le escapa que ahora mismo sufrimos una encarnizada batalla entre navegadores (IE, FF, Opera, Chrome, Safari, etc.) y como no podía ser de otra forma, el módulo heurístico por el que el navegador de turno decide cuando ha expirado un recurso (recuerda que esto sucede siempre que no se haya establecido una expiración concreta para ese recurso) es todo un misterio y distinto para cada uno de los navegadores del mercado. Incluso para directivas que suponen peticiones condicionales, un navegador no se comporta igual que otro (por ejemplo, no siempre que se envía desde el servidor un ETag o LastModified, siguientes peticiones del mismo recurso provocan peticiones condicionales). De este modo, mi recomendación (mía y sólo mía) es que para aquellos recursos sensibles se establezcan políticas explícitas de expiración concretas y para el resto no inviertas ni un solo minuto de tu tiempo asumiendo que se hará una petición condicional en determinado momento o que el módulo heurístico del navegador X será tan listo como para renovar el recurso cuando tú creas que debería haber sido.

¿Conoces la diferencia entre pulsar F5 y Ctrl+F5 en tu navegador?.

 

F5 viene a ser “por favor, actualiza todo el documento y todas sus dependencias, pero si hay recursos que no han cambiado, tampoco los descargues…”

 

Ctrl+F5 viene a ser “con favor y sin favor, actualiza todo el documento y no te preocupes de que algo no haya cambiando, el tráfico de red no es mi problema…”

 

En cualquier caso, esto es responsabilidad del navegador y distintos navegadores podrían tratar de distinto forma lo aquí expuesto.


Hasta ahora siempre hemos hablado de cabeceras en solicitud y respuesta para controlar la expiración de un recurso pero, y aunque no es lo más habitual, también es posible establecer el comportamiento de la cache a través de directivas META de HTML. Para más información ver la meta Cache-Control en Useful HTML Meta Tags.

Otra cabecera que está relacionada con la cache y que podría aparecer es Vary. Yo no la he trabajo mucho (francamente) pero parece que permite agregar más condiciones a la decisión de si el recurso debe o no ser servidor de nuevo por el servidor. Más información en http://stackoverflow.com/a/1975677 y http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44

Otras consideraciones al respecto de la cache son las siguientes:

  • Peticiones con el verbo POST nunca son cacheadas.
  • HTTPS se rige por las mismas cabeceras que HTTP. Más info en Top 7 Myths about HTTPS.

Cache en ASP.NET

Todo lo anteriormente expuesto es relativo a conceptos generales sobre la cache, pero en nuestro caso nos vamos a enfocar en como trabaja ASP.NET en lo relativo a la cache.

A grandes rasgos tenemos las siguientes posibilidades:

  • Response.Cache.
  • IIS
  • Manejadores para distintos tipos de archivo.

Response.Cache sólo es efectivo para la página .aspx y no para los recursos incluidos en la misma. Un artículo muy bueno que habla sobre ello puedes encontrarlo en http://dotnetperls.com/cache-aspnet

IIS permite establecer directivas (a través de cabeceras), bien sea a un sitio web entero, a una carpeta o incluso a un fichero concreto. Lo cierto es que si se tiene acceso al servidor y a IIS, es un buen sitio para especificar de forma precisa y rápida nuestras políticas de cache y expiración.

Otra opción para establecer cabeceras a recursos concretos (no a la página .aspx sino a los ficheros .jpg por ejemplo) es crear un nuevo manejador (clase que hereda de IHttpHandler) e instruir a IIS para que los ficheros .jpg en vez de servirlos él mismo lo haga ASP.NET. Un ejemplo se puede encontrar en http://www.codeproject.com/KB/aspnet/CachingImagesInASPNET.aspx

También comentar que existe la directiva OutputCache, que aunque en cierta medida puede intervenir en esta ida y vuelta de cabeceras de cache, su propósito está más orientado a cachear páginas (el resultado de la página o control de usuario) en el servidor para ahorrar coste de procesamiento. http://jimenezroda.wordpress.com/2009/08/19/using-outputcache-directive-utilizando-la-directiva-outputcache/

Por último, te dejo un enlace buenísimo que habla sobre la cache en su amplio espectro Caching Tutorial.

Un saludo!

4 comentarios:

  1. Fantástico. Por fin un artículo que me ha explicado todo el proceso completamente fácil y bien. Felicidades

    ResponderEliminar
  2. Gracias, Javier. Esto de la cache son las típicas cosas que si uno no escribe, parece que nunca terminas de cerrar el círculo y siempre estás dudando de como funcionaba ;-)
    Un saludo!

    ResponderEliminar
  3. sos el dios del caching... gracias por el articulo.

    ResponderEliminar
  4. Tremendo documento. No sé si desde 2011 hay algún documento mejor que explique así claramente el funcionamiento de la Caché, y con aplicación a ASP.NET.

    Gracias !!!

    ResponderEliminar