Contenido

Cuándo usar una caché

Discusión sobre cuándo se debería aañadir un sistema de caché a una aplicación. Incluye una calculadora para ayudar a los desarrolladores que lleguen aquí.

Respuesta corta

Debería usar un sistema de Caché para resolver mi problema actual?

No, pero esta respuesta podría ser incorrecta porque está cacheada ;)

Calculadora de Optimización de Cache


Respuesta larga

Cuando alguien habla sobre añadir una caché para hacer que un servicio sea mas rápido, a mí sólo se me ocurre preguntar: “¿Cómo de rápido debería ser?”. A menudo no recibo respuesta, y la gente sólo se enfada conmigo.

Pero eso sólo es porque no han leído este artículo o simplemente no se han parado a pensar sobre ello.

Digamos que se deseamos bucar en una base de datos. Luego lo primero que se deberiamos hacer es medir cuánto tiempo require realizar esa consulta, como media.

Quizá nos demos cuenta de que eso es suficientemente rápido y no necesita de una caché.

Incluso si creemos que la caché no merece la pena, es importante medir:

  • Cuánto tiempo requiere leer datos de la Cache.
  • Cuánto tiempo requiere escribir datos en la Caché.
  • La tasa de éxito, o cuántas veces se encontrarán los datos en la caché.

Es importante añadir todas esas métricas a nuestro sistema de monitorización.

Entonces se puede aplicar esta fórmula: El tiempo empleado por nuestro sistema inicial fue el tiempo necesario para leer un valor de la base de datos, pero con una caché sería:

$$ Cache_{read} + ((1 - S_R) * (Op + Cache_{write})) $$

Donde:

  • \(S_R\) Es la Tasa de Éxito. Es una propoerción entre 0 y 1.
  • \(Cache_{read}\) Es el tiempo de lectura de la caché.
  • \(Cache_{write}\) Es el tiempo de escritura en la caché.
  • \(Op\) Es el tiempo que dura la operación que queremos mejorar. Puede ser un acceso a base de datos, una llamada a función, …

Como código

Quizá leer el algoritmo habitual es más fácil de entener la fórmula anterior:

1
2
3
4
value = retrieve_from_cache(key)        # Cache Read
if value is None:                       # Will enter (1 - SuccessRate) times
    value = operation()                 # Main operation to be performed
    store_in_cache(key)                 # Cache Write

El prcio de una Caché

Un servicio adicional

Tener una caché normalmente requiere el uso de un servicio adicional, como Redis, Memcached, …

Este servicio necesita configurarse con Alta Disponibilidad (HA), con varios hosts, lo que cuesta tiempo y dinero. Y requiere mantenimiento, actualizaciones, …

Algún lector dirá: “Eso no es cierto, porque podemos reutilizar la instalación que ya tenemos”. Quizá, pero se tendrán que tener en cuenta los nuevos requisitos de CPU, RAM y Disco para el nuevo servicio y cómo podría afectar a los otros servicios existentes.

Invalidación de Cache

Cuando se usa un sistema de caché para datos que pueden variar con el tiempo aparece un nuevo problema: La invalidación de la caché. Es realmente difícil o incluso imposible tratar algo así sin errores.

No basta con enviar un mensage de invalidación. Si nuestro sistema de Caché se compone de varias instancias, necesitaremos asegurarnos de que:

  • Todos ellos se han notificado.
  • Ninguna otra petición contra la misma caché se está ejecutando en paralelo, lo que podría obtener un valor antiguo y re-insertarlo en la Caché.

De acuerdo a Martin Fowler:

Sólo hay dos cosas realmente complejas en Informática: la invalidación de la Caché y poner nombre a las cosas.

Monitorización

Cuando se usa una caché los requisitos de monitorización se incrementan porque necesitamos asegurarnos de lo bien que funciona la caché.

Por lo tanto, el código anterior se transforma en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
with measure_time("cache_read"):
    value = run_retrieve_from_cache(key)    # Lectura de Cache
if value is None:                           # Entrará (1 - SuccessRate) veces
    increase_measure("cache_failure")
    with measure_time("cache_operation"):
        value = operation()                 # Operación principal a ejecutar
    with measure_time("cache_write"):
        store_in_cache(key)                 # Escritura en Cache
else:
    increase_measure("cache_success")

Lo que es más complejo de leer.

Esto incluirá dashboards y alertas en nuestro sistema de monitorización para poder comprobar de forma contínua si es útil o ya no lo es.

Conclusiones

A veces usamos un sistema de caché sólo porque pensamos que las cachés son rápidas, pero no siempre son necesarias.

Otras veces se pueden mejorar juntando operaciones. Por ejemplo, en lugar de cachear separadamente dos peticiones a una base de datos podríamos juntarlas en una función y cachear la propia llamada a la función, usando los argumentos de la función como parte de la clave de la caché. Esto podría incrementar el tiempo de \{Operation\}, incrementando el porcentaje de mejora esperado, pero quizá podría afectar a la Tasa de Éxito (\{S_R\}) si es más difícil encontrarlo cacheado. En una ocasión vi cachear todo el HTML resultante, lo que es óptimo porque requería muy pocos parámetros (únicamente el usuario), se solicitaba muchas veces y requería muchas consultas.

En mi opinión, no se debería usar una caché cuando el porcentaje de mejora esperado es menor al 30%, subiendo hasta el 50% si se necesita invalidación de caché. Si no es así creo que la caché no merece los costes de mantenimiento, cambios de código y de operación y, en general, los dolores de cabeza que trae.

Por lo tanto, respondiendo de nuevo…

Debería usar un sistema de Caché para resolver mi problema actual?

, pero sólo cuando marque la diferencia y esté correctamente monitorizada.