Objetos en Caché


System-Memory-icon[1]En los últimos años la memoria en caché se ha convertido en una clara ventaja en las aplicaciones, favoreciendo principalmente el performance de la aplicación, pero antes de entrar en detalle, me gustaría dar una pequeña introducción sobre que es la caché y las claras ventajas que ofrece su utilización.

Que es la Caché

Se le conoce como caché a toda la información que es cargada a la memoria RAM de la computadora. La memoria en caché ofrece como principal ventaja el acceso súper rápido a la información que se encuentra allí. Esto debido a que el acceso a la memoria RAM es muchas veces más rápido que el acceso a la información en disco. Sin embargo la cache tiene algunas inconveniencias de las cuales hablaremos más adelante, pero por hora cabe destacar que la memoria cache es volátil, lo que implica que podríamos perder cualquier cambio no persistido en un dispositivo físico. De momento hasta que lo dejamos y más adelante analizaremos todas las caras de la memoria en caché y sobre todo los Objetos en caché.

Que son los Objetos en Caché

Como ya vimos, la cache es toda la información previamente cargada del disco a la memoria RAM de nuestro servidor. Los Objetos en memoria siguen exactamente el mismo principio, sin embargo este tipo de cache está diseñado para albergar Objetos, permitiendo elevar el performance de la aplicación significativamente.

Básicamente la caché es creada a medida que la información es solicitada por la aplicación, cuando una aplicación busca un objeto este primero lo buscará en la memoria caché, si lo encuentra, lo regresa. En el caso de que el objeto no se encontrado en la cache, el sistema entonces sí tendrá que ir a buscarlo a la base de datos, el objeto será creado y puesto en la cache para estar disponible en el futuro y entonces sí, devuelto a la aplicación. Veamos que en el caso de que el objeto ya se encuentre en cache, se evita buscar la información en la base de datos.

Memoria en cache
Fig.1: Objeto encontrado en la memoria Caché.

En la imagen anterior se representa el escenario en donde la aplicación busca un objeto que ya se encuentra en memoria caché.

  1. La aplicación busca un objeto mediante un API de caché.
  2. El API busca el objeto en la memoria caché.
  3. El objeto es encontrado en la memoria cache y es regresado a la aplicación.
Memoria en cache
Fig. 2: Objeto no encontrado en memoria cache.

En la imagen anterior se representa el escenario en donde el objeto buscado por la aplicación no se encuentra en caché.

  1. La aplicación busca un objeto mediante el API de caché.
  2. El API busca el objeto en la memoria caché.
  3. El objeto buscado no es encontrado en la memoria caché, por lo que busca en la base de datos la información para crearlo.
  4. El API crea el objeto y lo carga en la memoria caché para futuras búsquedas.
  5. El API regresa el objeto a la aplicación.

Implementar una memoria en caché para Objetos puede ser atractivo, sin embargo existen diversas estrategias para implementarla y es necesario conocer algunas de las más utilizadas para elegir sabiamente cual utilizar, en las siguientes secciones analizaremos las principales estrategias y sus características.

Términos interesantes

Esta sección del articulo la dejare para nombrar algunas cosas que es interesante leer antes de iniciar con el artículo, en este se explican algunos términos y detalles importantes de la implementación de caché.

Estrategias de bloqueo

Las estrategias de bloqueo son características básicas que todo API de caché deberá tener para garantizar la integridad de los objetos, evitando la múltiple edición de un objeto por procesos corriendo al mismo tiempo.

Optimistic lock

El optimistic lock o bloqueo optimista, permite que varios procesos pueda utilizar el objeto al mismo tiempo, sin embargo, cuando el objeto es cargado, a este se le asigna un número de versión, de tal manera que cuando un proceso intenta actualizar el registro el API validará que la versión del objeto a actualizar tiene la misma versión que el objeto en caché, si ningún proceso actualizo antes el registro, este tendrá la misma versión y la actualización se aplicara, sin embargo, si otro proceso ya actualizo el registro, el objeto enviado ya no corresponderá con la versión en caché por lo que una excepción será lanzada.

Se conoce como optimista porque de alguna forma confiamos en que otro proceso no actualizara el objeto antes que nosotros, sin embargo para fines de proteger la integridad de la información esta estrategia agrega la versión como un sistema para asegurarse de que no paso este problema.

Pessimistic lock

Al contrario que optimistic lock el pessimistic lock o bloqueo pesimista, no permite la edición simultánea de un objeto, por lo que si un proceso solicita un objeto, el API bloqueara cualquier intento de lectura/escritura del objeto hasta que el proceso  que solicito el bloqueo lo libere. De esta forma se protege la integridad con mayor fuerza sin embargo detenemos al resto de procesos que intenta leer el objeto hasta se sea liberado, pudiendo crear un cuello de botella.

Se conoce como pesimista por que no confía en que solo un proceso actualizara al objeto, por lo que mejor lo bloquea y así se asegura que nadie más lo edite.

Peligros de la caché

Existe una regla de oro la cual jamás se deberá violar al utilizar objetos en caché, de lo contrario lo único que obtendremos es un verdadero caos, la cual es la siguiente:

La aplicación deberá ser dueña de la información que está en caché, garantizando que ABSOLUTAMENTE NADIE edite la información directamente sobre la base de datos directamente sin pasar por el API de caché. Si esta regla se infringe lo que pasara es que la información que está en caché estará desactualizada de lo que hay en la base de datos, ya que el API de caché no podrá saber que la información ha cambiado, llegando a una inconsistencia de la información. Si la aplicación no es dueña de la información o no estás seguro si otros proceso editan directamente la información sobre la base de datos te sugiero no utilizar caché.

Estrategias para implementar el Caché

Uno de los temas más importantes a la hora de elegir un proveedor de objetos en caché es necesario analizar las estrategias que soportan y en qué medida estas implementaciones pueden ayudar en el desempeño de nuestra aplicación. Seleccionar mal la estrategia de implementación podría llegar a ser incluso peor que no utilizar caché, ya que como analizaremos más adelante, se podría tener un desbordamiento de la memoria, un costo de sincronización elevado y elevar la latencia.

Uno de los temas más importantes cuando trabajamos con memoria cache es que esta puede ser Read-Only (Solo lectura) o Read-Write (Lectura y escritura), siendo la segunda la más complicada debido a la concurrencia, provocando que más de un proceso editen el mismo objeto al mismo tiempo,  dejando el objeto en un estado inconsistente o simplemente el último proceso sobrescribiría los cambios de los procesos anteriores.

A continuación se exponen las estrategias de implementación de objetos en memoria más utilizados.

Cache aside

Cache aside o Caché de lado en español, es la estrategia más simple de implementar y las más utilizada. Seguramente este tipo de estrategia ya la hemos utilizado sin darnos cuenta. Un ejemplo típico son las colecciones como Map o Dictionary las cuales nos permite almacenar objetos mediante una clave y valor. Este tipo de colecciones nos permite almacenar objetos e identificarlos de forma única mediante una clave, la cual no se podrá repetir en la colección. El proceso de acceso es el siguiente: el objeto es buscado en la colección, si el objeto existe lo regresamos, y si no existe lo podemos buscar y agregarlo a la colección, de esta manera estaríamos implementando un sistema de objetos en cache simple pero efectivo.

Memoria en cache
Fig. 3: Implementación de la estrategia Cache Aside.

En la imagen podemos ver que la aplicación mantiene una comunicación con la memoria caché y con la base de datos, de esta forma, la aplicación buscara los objetos sobre la memoria caché, si no encuentra el objeto entonces lo consultara en la base de datos y lo agregara al cache.

Ventajas

  1. La principal ventaja es que es muy simple de implementar, bastara con las librerías estándar del lenguaje para echarla andar.
  2. No requiere de ningún API o producto licenciado para ser implementado.
  3. Muy útil para cache Read-Only.

Desventajas

  1. Es fácil tener inconsistencias sobre todo cuando múltiples procesos editan el mismo objeto a la vez, provocando que el último cambio sobre escriba el de los procesos anteriores.
  2. No soporta la sincronización en cluster, por lo que cada nodo tendrá una versión única de los objetos.
  3. Cuando se escribe directamente sobre la base de datos, los problemas de concurrencia son atendidos por la base de datos. Al escribir sobre los objetos en cache es necesario implementar estrategias de bloqueo Optimistic o Pesimistic para evitar inconsistencias.

Cuando utilizar esta estrategia

Esta estrategia se podría utilizar para aplicaciones pequeñas y medianas, con un nivel de concurrencia moderado, sin embargo hay que tener especial ciudad cuando se trabaja en cluster. Una de preocupaciones de esta implementación es el desbordamiento de la memoria, ya que al ser nosotros mismo los que agregamos los objetos al caché, tendremos que asegurarnos de liberar aquellos objetos que consideremos no se utilizarán un un tiempo considerable, ya que si solo agregamos objetos a la cache pero nunca los retiramos llegara el momento en que la memoria se agote.


 

Through-Caching

Esta estrategia de caché trabaja un poco distinta a la estrategia Cache Aside, ya que en esta, la aplicación no lee directamente la base de datos ni la memoria cache, en su lugar utiliza un API de caché, de esta forma, nuestra aplicación se olvida de la base de datos y solo trabaja con la cache a través de un API.

La figura 2, representa perfectamente este escenario, en la cual el API de caché se encuentra como intermediario entre la aplicación y la base de datos, en la imagen podemos ver que la aplicación busca un objeto por medio del API, el API busca el objeto en la cache, si existe lo regresa, pero si no existe lo busca en la base de datos, lo carga en cache y luego lo regresa. Veamos la gran diferencia que tiene contra la estrategia Cache Aside, ya que en esta tenemos un API que se encargara de administrar la cache por nosotros, de la misma manera, cuando un cambio se realiza sobre un objeto del cache, el API persistirá los cambios en la base de datos por nosotros.

Ventajas

  • Tenemos un API que se encarga de la caché, de esta forma nos olvidamos de la base de datos.
  • Únicamente trabajamos con objetos en caché.
  • Es muy difícil encontrar inconsistencia en los objetos, debido a que el API de cache controla los bloqueos y el acceso a la base de datos, evitando que versiones distintas se persistidas sin que el API se entere.

Desventajas

  • Procesos que escriban directamente sobre la base de datos pueden ser perjudiciales, ya que el API de caché no los detectara y creara una inconsistencia en la información.
  • Debido a que toda la información pasa a través de la caché será necesario controlar la cantidad de objetos que mantenemos activos y probablemente nuestra aplicación requiere un poco más de memoria para funcionar.
  • Probablemente requeriríamos de un producto licenciado e infraestructura para su instalación.

Cuando utilizar esta estrategia

Esta implementación se puede implementar en aplicaciones de mediano a gran tamaño donde contamos con infraestructura suficiente para montar los componentes necesarios, esta estrategia es más recomendable que Caché Aside en la mayoría de los casos, sobre todo porque nos apoyamos del API el cual suelen ser componentes probados y robustos.


 

Write behind

Finalmente tenemos la estrategia Write behind la cual es una variante de la estrategia Throug Caching sin embargo presenta una característica que la distingue, veamos la siguiente imagen:

Memoria en cache
Fig. 4: Implementación de la estrategia Write behind.

Esta estrategia implementa un mecanismo que permite retrasar la actualización a la base de datos y programarlas para que se actualice cada cierto tiempo. Esta estrategia aporta todas las ventajas de la estrategia Through Caching más las ventajas que definiremos a continuación:

Ventajas

  1. Tiempo de respuesta de la base de datos se desacoplan de la aplicación.
  2. Permite seguir operando incluso si la base de datos falla.
  3. Todas las actualizaciones a la base de datos se pueden enviar en batch.
  4. Múltiples actualizaciones sobre un mismo objeto pueden ser agrupados, reduciendo el número de instrucciones a la base de datos.

Desventajas

  1. Puede requerir de productos licenciados que soporten esta estrategia.
  2. Se requiere de una infraestructura más robusta.
  3. No es recomendable si tenemos procesos que leen la información en tiempo real directamente sobre la base de datos, debido que abra un desfase entre lo que hay en la base de datos y la caché.

Cuando utilizar esta estrategia

Esta estrategia es implementada en aplicaciones medianas a grandes, donde se cuenta con los recursos suficientes para montar una infraestructura robusta. El factor clave para inclinarse por esta estrategia es cuando tenemos una gran cantidad de transacciones sobre la cache, debido a que esta estrategia aumenta el performance al agrupar actualizaciones sobre el mismo objeto y enviar las actualizaciones en Batch. Si la aplicación no cuenta con un gran número de transacciones sobre la cache podemos utilizar tranquilamente la estrategia Through Caching.


 Caché distribuida

Hasta el momento hemos hablado de las estrategias de implementación de caché, sin embargo no hemos abordado el cómo se comporta la caché en ambientes distribuidos o cluster. En este tipo de ambientes la administración de la cache se complica mucho más, ya que se empiezan a presentar algunos problemas como la redundancia, concurrencia y sincronización de la cache entre todo los nodos del cluster.

Topologias

Las topologías son las distintas maneras en las que podemos implementar una caché en ambientes distribuidos, estas topologías intentan resolver los problemas de sincronización de la caché entre todos los nodos.

Para comprender el problema de sincronización de la cache veamos la siguiente imagen:

Memoria en cache
Fig. 5: Cache distribuida no sincronizada.

Veamos la imagen anterior, existen dos nodos con la misma aplicación, cada aplicación tiene su propia cache, cada aplicación podría tener los mismo objetos cargados en la cache, por lo que si un nodo hace un cambio en los objetos de la cache, el resto de nodos no sabrán sobre el cambio y permanecerán con un objeto desactualizado en caché, cuando estos nodo realicen un cambio sobre el objeto desactualizado plancharon los datos guardados por el primer nodo, llegando a un estado de inconsistencia entre los nodos.

Para solventar este problema existen diversas topologías que nos permitirán hacer una sincronización sobre la información de tal manera que todos los nodos estén actualizados.

Replicated Cache service

Esta topología permite resolver el problema de sincronización de la cache mediante un servicio dedica exclusivamente en la sincronización de todos los nodos dentro del cluster, de tal manera que todos los nodos del cluster tendrán exactamente los mismo objetos en cache y en la misma versión, veamos la siguiente imagen:

Memoria en cache
Fig. 6: Replication cache service.

Veamos en la imagen que cada nodo tiene exactamente los mismo objetos en la cache, por lo que si es necesario consultar un objeto simplemente lo tendrá que buscar en su caché local y utilizarlo. Por otra parte tenemos la actualización de la caché, para lo cual veamos esta otra imagen:

Memoria en cache
Fig. 7: Replication cache service update

Veamos que la actualización es mucho más compleja, ya que cada actualización en un objeto forzara al API a sincronizar el cambio en todos los nodos del cluster.

Bajo este escenario, si uno de los nodos falla o se apaga, el resto de nodos sigue trabajando sin ningún problema, ya que el resto de nodos tiene una copia idéntica de la caché.

Ventajas

  1. Permite tener sincronizada la cache en todos los nodos.
  2. La consulta de la cache es muy rápida, ya que todos los objetos se encuentran en la caché local, evitando salir a otros nodos a buscar el objeto.
  3. El API se encarga de la sincronización por lo que para nuestra aplicación este procedimiento es transparente.

Desventajas

  1. Puede ocasionar problemas de performance por la latencia ocasionada de la sincronización que existe entre los nodos, sobre todo cuando existe gran cantidad de actualizaciones en todos los nodos.
  2. Se recomienda que todos los nodos tenga la misma cantidad de memoria, ya que si la cache empieza a crecer será necesario que todos los nodos soporten el mismo volumen de cache de lo contrario la sincronización fallara por espacio.
  3. Todos los nodos tendrán los mismos objetos por lo que el total de objetos en cache estará limitada al espacio en cache de un solo nodo.

Cuando utilizar

Esta topología la podemos aplicar en aplicaciones de gran tamaño, incluso con gran cantidad de transacciones, con reglas podríamos decir que es ideal en aplicaciones donde la información no está seccionada, es decir que muy posiblemente todos los nodos utilicen generalmente la misma información.

 

Partitional Cache Service

Otra de las topologías más utilizadas es la de cache particionado. La principal diferencia es que los objetos no se sincronizan entre los nodos, en su lugar cada nodo será dueño de una sección de la información, la siguiente imagen intenta representar la cache particionada:

Memoria en cache
Fig. 8; Cache particionada

Veamos que en la imagen tenemos 3 nodos, cada uno de ellos tiene un su caché objetos únicos, los cuales no se repiten en ningún otro nodo, ha esto se le llama cache particionada. La estrategia que emplea para la lectura y escricura es la siguiente: La aplicación solicitara el objeto al API, este por medio del Partioned Cache Service determinara donde es que este objeto vive, si el objeto se encuentre en la cache local, entonces simplemente lo regresara. Por otra parte, si el objeto vive en otro nodo entonces el API buscara el objeto en el nodo en el que se encuentra el objeto. La escritura también sigue este principio, cuando se edite cualquier objeto, este se modificara directamente sobre el nodo al que pertenece el objeto.

Bajo este escenario si uno de los nodos llegase a fallar entonces el cluster se ve afectado, ya que el resto de nodos necesitarán de los objetos que tiene el nodo que fallo, por lo cual el Partitioned Cache Service tendrá que cargar los objetos del nodo fallido y distribuirlos por el resto de los nodos activos en lo que el nodo fallido se reactiva.

Ventajas

  1. El tamaño de objetos cargados en cache se limita por la suma total de memoria de todos los nodos activos, ya que cada nodo carga objetos únicos, permitiendo que el tamaño de objetos cargados crezca a medida que más nodos son agregados. A diferencia de la cache replicada, en la cual si se agrega un nuevo nodo, este solo copia los mismos objetos que el resto de nodos tiene.
  2. Utilizado correctamente, aumenta drásticamente el performance, debido a que cada nodo tendrá de forma local los objetos más utilizados.
  3. El API de cache se encargar de buscar los objetos en los distintos nodos, evitando así saber exactamente en qué nodo vive cada objeto, haciendo para nosotros esto totalmente transparente.
  4. Permite montar aplicaciones de mayor envergadura, generalmente para aplicaciones multi región que dan soporte a varios países del mundo.

Desventajas

  1. Una mala segmentación de los objetos puede ser perjudicial el performance de la aplicación, sobre todo por la latencia generada en ir a buscar muchos objetos fuera del nodo.
  2.  Requerirá de una configuración mucho más delicada, ya que se tendrá que planear con cuidado la distribución de todos los objetos.
  3. Se sacrifica un poco de performance para obtener un mayor número de objetos en caché, a diferencia de la caché replicada, la cual evita tener que salir del nodo a buscar el objeto.

Cuando utilizar

Esta topología sin duda, es que nos servirá para soportar las aplicaciones más grandes de todas, en las que hay que tener muchos nodos atendiendo gran volumen de transacciones y usuario repartido por varios países del mundo. Esta topología sigue el lema de “Divide y vencerás” ya que al segmentar la información nos permitirá atender con mayor eficiencia a grupos identificados de transacciones. Imagina un sistema multi empresa, multi región o multi unidad de negocio, en estos tipo de sistemas rara vez una empresa obtiene información de otra, o una región de otra, etc., desde luego que esta comunicación se da, pero es solo un pequeños porcentaje del resto de transacciones.

Near Cache

Finalmente la topología Near Cache o Cache cercano en español, este es una topología que hibrido la topología de cache sincronizada y cache distribuida, esta combina las características de las dos para dar una solución mucho mejor, veamos la siguiente imagen para entender este concepto:

Memoria en cache
Fig. 9: Near cache.

Esta topología es la más compleja de todas las expuestas aquí. Para comprender mejor la imagen analicemos lo siguiente: Cada nodo tendrá dos secciones de caché, la primaria seria la caché de abajo la cual tiene los objetos pertenecientes a nodo (cache distribuido), en esta cache cada nodo tendrá objetos únicos y no deberá haber uno igual en otra cache del resto de nodos. La segunda cache, es la caché local, esta cache es la que se encuentra arriba de la imagen, esta caché funcionara como caché sincronizada. El funcionamiento es el siguiente: Cuando el cluster inicia, cada nodo cargará únicamente la cache primaria, luego la secundaria es cargada a medida que un objeto de otro nodo es solicitado, por ejemplo, veamos la cache secundaria del nodo 2, este tiene cargado el objeto A, debido a que fue solicitado una vez, este objeto permanecerá en la memoria secundaria hasta que sea retirado o que dure un tiempo sin usarse. Otra de las características es que cuando el objeto A se actualizado por cualquier nodo, este cambio se replicara en la cache secundaria de todos los nodos (siempre y cuando el objeto este allí).

Mediante esta topología podemos tener distribuida la cache, pero también podemos tener en caché local los objetos más utilizados del resto de nodos.

Ventajas

  1. Permite combinar las características de la cache sincronizada y la caché distribuida.
  2. Tiene un mayor performance que la cache distribuida.
  3. El API se encarga de administrar las dos secciones de caché, de tal manera que es transparente para la aplicación la ubicación real del objeto.

Desventajas

  1. La cantidad de objetos cargados en memoria se reduce, debido a que es necesario tener objetos redundantes en todos los nodos.
  2. Puede presentar problemas de latencia, al sincronizar los objetos en todo el cluster.
  3. Al igual que la cache distribuida, requiere de la configuración para determinar la ubicación de los objetos.

Cuando utilizar

Esta topología es empleado por lo general en aplicaciones  grandes a muy grandes, sin embargo esta no es la principal regla, en lo general yo recomiendo esta estrategia cuando el performance es un cache distribuido es el principal objetivo, de lo contrario podríamos utilizar tranquilamente la cache distribuida.

Conclusiones

Una vez analizados los aspectos más importantes de los objetos en caché podemos decir que ofrecen una gran ventaja en performance para las aplicaciones, también hemos visto que existe estrategias y topologías bastante solidas que nos permitirán montar una infraestructura en cache robusta y escalable, por lo que lo único que nos queda por hacer es identificar basado en lo planteado en este artículo cuando debemos utilizar objetos en cache y cuando no, también es importante pensar muy bien qué estrategia implementaremos para sacar el mayor provecho.

* Si te gusto este articulo coméntalo, compártelo y te invito a que te suscribas para recibir notificaciones cada vez que cree un nuevo artículo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *