Creo que, después de tanto artículo sobre Scrum, los lectores de mi blog agradecerán una entrada que no tenga nada que ver con las metodologías ágiles. Sí, sí, yo también estoy un poco saturado del tema, pero de todos modos, no dudéis que vamos a seguir hablando de ello en este blog. Pero antes de meterme en harina, permitidme que os presente www.scrumweek.com, una semana enterita de formación sobre metodologías ágiles.
Pero como decía hoy no vamos a hablar de metodologías, sino de arquitectura y diseño de software. De hecho vamos a hablar de uno de los elementos claves de toda arquitectura: la caché.
Seguro que ninguno de los lectores ignora los beneficios que una caché bien diseñada proporciona a casi cualquier aplicación. Se puede asegurar que sea cual sea el dominio de nuestra aplicación el rendimiento de la misma se verá beneficiado por el principio general de almacenar datos en lugares donde su acceso sea más rápido (memoria frente a disco, memoria frente a base de datos, capas más cercanas de la aplicación frente a capas más lejanas…).
Este principio general se cumple casi siempre, pero no es el motivo de este post hablar de los ya conocidos efectos beneficiosos del cacheo, sino de los usos aberrantes que a menudo he visto de la cache. Y es que, si bien una cache bien utilizada es un catalizar claro del buen rendimiento, una mal uso de la cache es más a menudo de lo que muchos desarrolladores sospechan fuente de serios problemas de rendimiento. Es la tan temida cacheitis. La cacheitis es una enfermedad que sufren muchísimas aplicaciones, consiste en hacer una mal uso de la cache que provoca una dolorosa inflamación de la misma que la hace totalmente inservible. La cacheitis se manifiestas con muchos síntomas diferentes siendo los más significativos un altísimo consumo de memoria y un mal rendimiento de la aplicación.
La cacheitis tiene sus orígenes en diversos malos usos de la caché casi siempre relacionados con olvidar alguna de las siguiente buenas prácticas:
Asume que no puedes cachearlo todo: Como arquitecto, cuando te enfrentas a la difícil decisión de diseñar tu modelo de caché debes tener en cuenta que no puedes cachear todo. La cache por definición es finita. Si puedes contar con una caché infinita, simplemente no necesitas una caché. El lugar en el que suelen acabar todo los datos es la memoria, si todos los datos que va a manejar tu aplicación caben en memoria por definición y tienes la seguridad de que esto siempre va a ser así, simplemente almacénalos directamente en memoria (ignoro aquí la necesidad de persistencia). Por ejemplo supongamos una aplicación que utiliza una tablas de cálculo, cargar directamente esas tablas en memoria puede ser una opción interesante. De todos modos, hemos de reconocer, que a menudo las aplicaciones no puede asumir está premisa. En ese caso es una labor sumamente difícil decidir cual es el balance justo entre consumo de memoria y uso de recursos más lentos como acceso a disco y petición de datos por la red, pero ese no es le punto ahora. El punto ahora es que la memoria es finita y además un recurso compartido y por tanto tu cache tiene que estar explícitamente limitada. Diseñar si tener en cuenta que la cache es finita y cachearlo todo es un error de base, una cache que cachea todo nunca va a ser efectiva. Una frase que en el Debugging and Optmization Team de Plain Concepts hemos oído a menudo es: ‘Cómo puede ser lenta la aplicación si lo cacheamos todo’… cachear todo es dañino por que la cache es finita. Si en una cache finita, cacheas todo el resultado será que algo tendrá que salir de la caché para que entre lo último que has cacheado y que tu cache por lo tanto siempre contendrá lo último cacheado, pero no lo que más importante sea cachear. La cache debe almacenar lo recursos costosos que se acceden con más frecuencia, no los últimos accedidos.
El cacheo temprano es el origen de todos los males: Parafraseando la mítica frase de Donald Knuth , ‘la optimización temprana es el origen de todos los males’, tomar decisiones de cacheo demasiado temprano es dañino en la mayoría de las ocasiones. A menudo cuando diseñamos una arquitectura establecemos mecanismos de cacheo que luego son utilizados hasta el abuso. El peligro subyacente al cacheo temprano es que comencemos a cachear sistemáticamente elementos que nos parecen costosos sin tener evidencia de que realmente son los mejores candidatos para su cacheo. Por ejemplo, llegamos a una historia de usuario que requiere cargar datos de provincias y como suponemos que van a cambiar poco decidimos cachearlos ignorando que el factor más importante a la hora de cachear es el coste de obtener la información multiplicado por el número de accesos que esa información va a ser accedida. Cachear toda la información que cambia poco simplemente por que es fácil de cachear es un error habitual. Además los patrones de acceso a la información en aplicaciones complejas son difícilmente predecibles, es muy difícil sin analizar el rendimiento de tu aplicación mediante unas adecuadas pruebas de carga que modelen el uso que va a tener, tomar las decisiones correctas sobre que cachear. Diseña para hacer el cacheo posible, pero no decidas que cachear hasta que no te quede más remedio.
No solo existe una caché: Otro error habitual es olvidar que no solo existe una cache en el sistema. El gestor de bases de datos que utilizamos implementa mecanismos de cache, el servidor web que utilizamos implementa mecanismos de cache, nuestra aplicación implementa mecanismos de caché… ¡y todos usan la memoria!. Hace un tiempo nos llamaron para ver que estaba ocurriendo en una aplicación que hacia un buen uso de la caché, de hecho el comportamiento de la aplicación era adecuado la gran mayoría de las ocasiones… excepto cuando el ‘servidor’ tenía menos de dos bigas de memoria situación en la que todo se volvía escandalosamente lento. Lo más sorprendente es que en la última versión de la aplicación habían añadido una caché que mejoraba mucho el rendimiento en la mayoría de los casos. Sin embargo la versión anterior sin caché se comportaba aceptablemente en servidores con poca memoria. Tras estudiar el comportamiento de la aplicación, sorprendentemente, en situaciones con poca memoria, elementos costosos que habitualmente debían estar en la caché sistemáticamente tenían que ser recuperados desde la base de datos. Y lo que es peor, cuando observábamos el comportamiento de la base de datos ¡también veíamos tiempos de respuesta muy elevados!. Tras tirar del hilo nos dimos cuenta de lo que estaba ocurriendo: SQL Server y ASP.Net/IIS se estaban pegando por la memoria para colocar sus respectivas cachés. Vaya, nos habíamos olvidado de que ¡la memoria es finita!. La solución, sencilla, limitar la memoria máxima a utilizar por parte de SQL Server y de ASP.Net y listo… problema solucionado.
Una colección estática o global no es una caché: Aunque parezca sorprendente, no es extraño encontrar desarrolladores que implementa su propio sistema de caché. Esto siempre es una aberración. Hoy por hoy todas las tecnologías que podemos usar en una arquitectura .Net en las que el cacheo puede jugar un papel tiene un mecanismo de caché. ¡He visto cachés implementadas como colecciones estáticas! Una mecanismo de caché que no cuenta con un mecanismo de invalidación no es un mecanismo de caché. Tan importante es mantener en caché los elementos que se acceden con más frecuencia como liberar la memoria utilizada por aquellos elementos en cache que no son accedidos. Igualmente es importante, siempre que se decide cachear un elemento establecer la política de invalidación de dicho elemento.
La cacheítis, como cualquier enfermedad tiene síntomas que nos pueden poner sobre su pista. Casi todos estos síntomas se manifiestan con contadores de rendimiento fuera de control. El el Performance Monitor (Perfmon.exe) el termómetro que nos indicará que podemos estar sufriendo de cacheitis. Hay un parámetro que todo mecanismo de caché suele publicar en sus contadores rendimiento y que es fundamental a la hora de diagnosticar una cacheítis:
Cache Hit Ratio:
Cuando el cliente de un sistema de caché pide el acceso a un dato que es susceptible de encontrarse en la caché y lo encuentra se produce un ‘hit’ de la cache. La relación entre las veces que se hacen peticiones a la cache y las veces que el dato buscado en la caché se encuentra en esta es el Cache Hit Ratio. Un porcentaje bajo en este indicador es un síntoma claro de un mal uso de la caché. Se suele considerar un valor adecuado un ratio de alrededor del 90%.
En la siguiente imagen se pueden ver diferentes contadores de rendimiento de componentes típicos de una aplicación .net:
¡Ojo con la cacheitis!