Backend más rápido con tu propia caché
Introducción
En muchas ocasiones hemos escuchado el término “caché” aplicado a numerosos ámbitos, normalmente relacionados con la tecnología. Su origen está en los microprocesadores. En ellos existe una memoria especial que se encuentra entre la CPU y la RAM, más pequeña y de acceso más rápido que la memoria principal. Su objetivo es almacenar datos previamente procesados por la CPU, que normalmente se utilizan con frecuencia, reduciendo así los tiempos de acceso a los datos, ya que será más rápido acceder a esta memoria caché que a la memoria principal.
Cuando una persona accede a cualquier página de Internet, sin él saberlo, puede estar provocando que se estén realizando numerosas operaciones de caché. Podemos decir que existen principalmente los siguientes niveles:
- Cliente. Los navegadores, por ejemplo, cachean contenido web para acelerar la carga de los mismos.
- DNS. Los servidores de DNS suelen cachear la resolución de los dominios a IPs.
- Web. Relacionado con el cacheo de cabeceras HTTP, en CDNs o en proxys inversos, por ejemplo.
- Aplicaciones. Normalmente cachés locales creadas por las aplicaciones.
- Bases de datos. Permiten reducir los tiempos en las solicitudes a las bases de datos.
Son numerosos las soluciones que existen hoy en día en el mercado, entre las que se encuentran las más conocidas como Varnish, Memcache o Redis. Son excelentes, y hacen que el rendimiento de nuestra aplicación mejore notablemente. Sin embargo, todas ellas tienen sus propósitos y no son necesariamente excluyentes con la solución cuestión de este artículo, que es crear nuestra propia capa de caché en nuestra aplicación.
Contexto
Se pueden dar muchos escenarios por los que resulta interesante aplicar nuestra propia caché. Refiriéndome a mi propia experiencia, los escenarios más comunes a los que me he enfrentado han sido:
- Respuestas de APIs poco variables en el tiempo
- Conexiones con APIs con límite de peticiones
- Tareas con un elevado y exigente procesamiento
Estos casos, y otros, incluso pueden ir de la mano. En definitiva, todos tienen en común que el rendimiento es importante. Y la medida de rendimiento más importante siempre suele ser la misma: rapidez. Y en este aspecto, implementar una capa de caché en nuestro backend puede ser determinante. De hecho, es directamente proporcional: a más carga que tenga nuestro backend, más beneficio nos proporciona.
Soluciones
En mi caso, el desarrollo backend de mis aplicaciones está realizado con el framework Symfony, es decir, está programado con PHP. Aunque, realmente, el uso de Symfony va a importar poco para las siguientes soluciones.
PSR-6: Caching Interface
Este estándar fue presentado por el PHP FIG en diciembre de 2015 (https://www.php-fig.org/psr/psr-6/). Nace con el objetivo principal de proveer un conjunto de interfaces para cubrir las necesidades básicas que cualquier desarrollador PHP pueda tener a la hora de implementar caching en su aplicación. Nos dan una forma sencilla y normalizada de tener nuestra propia capa de caché. Cuenta con una parte de manejo de errores y, dado que no dejan de ser una serie de interfaces, son perfectamente extensibles. De hecho, algunos de los paquetes PHP más utilizados como “symfony/cache” (https://packagist.org/packages/symfony/cache) o “illuminate/cache” (https://packagist.org/packages/illuminate/cache) las extienden.
PSR-6 nos deja dos conceptos claves:
- Pool se referirá a nuestro repositorio de los elementos que almacenará nuestra caché
- Item será aquel elemento que forme parte de nuestra caché
Para cada uno de estos conceptos, PSR-6 nos deja las siguientes principales interfaces:
Para tener nuestro propio pool de caché, implementaremos la interfaz CacheItemPoolInterface. Como vemos, su comportamiento es bastante sencillo: nos permite obtener, guardar y eliminar uno o varios elementos. Todo ello a partir de unas claves que podemos definir nosotros cómo deben ser. Una cosa importante es que siempre nuestro pool devolverá un elemento CacheItemInterface, exista o no.
Estos elementos CacheItemInterface tienen la forma que vemos, donde hay que destacar la función isHit(), la cual nos indicará si existe o no tal elemento en caché.
Un flujo típico sería el siguiente:
La idea siempre será la misma: a partir de una clave que identifica el valor que necesitamos, hacemos una petición a nuestro pool. Este siempre nos devolverá un item, que en caso de haber tenido éxito (a partir de la comprobación con la función isHit(), nos proporcionará el valor que estamos requiriendo. En caso contrario, ejecutaremos la funcionalidad necesaria para obtener dicho valor, y la guardamos en el item obtenido previamente, el cual estará listo para ser guardado a través de nuestro pool.
PSR-16: Common Interface for Caching Libraries
Con PSR-6 se puede cubrir prácticamente casi todos los casos posibles que se pueden dar a la hora de implementar una capa de caché. Sin embargo, se puede optar por una solución más simple, y de ahí surge otro estándar: PSR-16 (https://www.php-fig.org/psr/psr-16/).
Este estándar se desentiende del concepto item que existía en PSR-6 ya que solo cuenta con una única interfaz clave, que correspondería a lo que conocemos como pool en PSR-6.
Como vemos, es mucho más simple, pero sigue contando con las operaciones básicas de lectura, escritura y borrado. Pero en este caso atacando directamente a los valores en caché, al eliminarse el concepto item que los contenía.
Lo más interesante está en que está más orientada a cuando necesitamos realizar numerosas operaciones de lectura, escritura y borrado en caché. De ahí la existencia de funciones multiple, para cada tipo de operación. De esta manera nos centramos en hacer el menor número de viajes a nuestra caché, lo cual es lo más óptimo en términos de rendimiento.
Symfony Cache Component
En este caso no hablamos de un estándar como tal, sino de un paquete listo para usar elaborado por Symfony, que pese a ello, podemos utilizarlo en cualquier proyecto. Se trata de un componente que consiste en una implementación de tanto del estándar PSR-6 como del PSR-16. Al ser un componente ya creado para su uso, cuenta con una API definida y robusta (https://symfony.com/doc/current/components/cache.html), que cuenta con una serie de funcionalidades listas para su uso, por lo que no necesitamos estar implementando nada por nuestra parte. Además, cuenta con su propio manejo de errores.
El componente se basa en el uso de lo que llaman Cache Contracts. Están basados en el uso único de dos funciones:
La función delete es más que evidente lo que realiza. Por otro lado, la función get es bastante interesante, ya que es más compleja de lo que parece.
Su segundo parámetro, como vemos, se trata de un callback que definiremos para que sea ejecutado en caso de que el elemento que intentemos buscar no se encuentre o haya expirado. Por tanto, esta función tiene forma de getter y de setter a su vez.
El último parámetro, beta, es una de las características más interesantes del componente Symfony Cache. Existe un concepto llamado Cache Stampede, el cual es un caso problemático que se puede dar. Se explica mejor con el siguiente ejemplo:
Imaginemos que recibimos 10 peticiones a la vez. Estas 10 van a hacer uso del mismo recurso, el cual vamos a obtener de nuestra caché, pero se produce un fallo de caché (es decir, que no existe o ha expirado). Por tanto, todas estas peticiones necesitarán realizar la tarea correspondiente para volver a generar la caché, lo cual no es óptimo. Existen dos soluciones que nos da ya el componente:
- Utilizando un sistema de locking.
- Recalcular el elemento antes de su expiración. La idea es que, antes de que se produzca la expiración, se provocará en una determinada petición recibida un fallo de caché, dando así a un recálculo del elemento. Ese tercer parámetro de la función, beta, servirá para calcular la probabilidad de que en una petición se de tal fallo de caché. Por defecto es 1.0, y según aumente, más temprana será la recalculación.
Conclusión
Como hemos podido ver, son diferentes las soluciones que podemos aplicar para tener caché en nuestra aplicación. Son unas de las tantas posibles, que van desde crear nuestra propia herramienta de manera personalizada a utilizar un componente ya existente. Como en casi todo, utilizar una u otra dependerá de la naturaleza del proyecto: si queremos algo rápido podemos utilizar un componente ya trabajado como Symfony Cache, pero si queremos algo totalmente personalizable, a nuestro gusto, podemos crearla a partir de los estándares presentados.
Crear tu propio componente, pese a pueden tener cierto grado de personalización, te permitirá reutilizarlo en tus diferentes proyectos, así que los costos de implementación en cuanto a tiempo solo existirán la primera vez que lo desarrolles.
En definitiva, cualquiera de ellas son bastantes simples, tanto de entender como de implementar, así que no hay por qué tener miedo a añadirlo a algún proyecto, ya que los beneficios van en aumento según mayor sea la cantidad de procesamiento que haya en tu backend.