Acelerando APIs con Grape, Entities y Redis

Óscar de Arriba
The Cocktail Engineering
4 min readJun 16, 2016

--

Una de las arquitecturas más populares actualmente es la de centralizar los datos en APIs, separándolos de su representación y haciéndolos disponibles para todo tipo de clientes.

Si bien en ocasiones los contenidos de dichas APIs no necesitan ser consultados en tiempo real, también nos encontraremos casos en los que la respuesta tiene que ser lo más rápida posible y actualizada a los últimos datos disponibles. Para este último caso se puede llegar a una solución intermedia empleando las posibilidades de caché que nos aporta Ruby on Rails junto con Grape, Grape-Entity y Redis.

Grape y Entities para nuestras APIs

Si bien no es estrictamente necesario, hay bastantes posibilidades de que en una API desarrollada con Ruby on Rails se utilice grape para encargarse de la creación de los endpoints, el versionado, autenticación, etc.

En caso de que sea así, existe una gema que nos permite modelar cómo se van a ver nuestros objetos una vez que los devolvamos en la respuesta. Su nombre es grape-entity.

Gracias a esta gema podemos definir los campos que queremos publicar (y cómo) para cada modelo de nuestra aplicación. Por poner un ejemplo, suponiendo que tengamos un objeto User, podemos crear un entity que exponga los campos del modelo que queramos:

Entity del modelo User

Esto nos permite generar un hash de cualquier objeto User con los campos que le hemos especificado. Al ser un hash, su conversión a JSON o XML es trivial (además, al usar Grape, ni siquiera tenemos que preocuparnos por ello).

Un ejemplo de cómo usar este entity podría ser:

Ejemplo básico de API Grape

La caché puede ser nuestra amiga

La caché puede ser nuestra amiga, hasta en una API en la que se actualicen los datos en tiempo real.

Uno de los mayores cuellos de botella, especialmente cuando se tratan muchos datos, es la instanciación de objetos. Si además tenemos en cuenta que hay que añadirle el procesamiento necesario para generar el hash de cada uno de esos objetos a través de los entities, el cuello de botella puede ser aún peor.

Aquí es donde llega la caché, y la solución por defecto aportada por Rails, es genial para actualizar los datos en tiempo real: la caché de bloques basada en timestamps. Esto permite invalidar la caché de un objeto si este ha cambiado en la base de datos y, en ese caso, regenerarla. Esta técnica es la usada por defecto en Rails a la hora de generar el cache_key de los objetos de ActiveRecord.

Si además queremos que nuestra caché vaya lo más rápido posible, Redis es el aliado perfecto (gracias a redis-rails). Usando Redis como almacenamiento de la caché podemos usar la acción fetch_multi para pedir múltiples bloques en una sola petición, minimizando los retardos de comunicación y haciendo todo el trabajo de una vez. Si aplicamos todo esto a nuestro código podríamos tener la función apply_entity tal que:

Modificación para cachear la representación JSON de los objetos.

Podemos ver como ahora nuestro código es más eficiente, cacheando la generación del hash del objeto (y, de paso, convirtiéndolo a un objeto JSON, lo que ahorrará bastante tiempo).

En este momento nuestro endpoint de Users contestará, en el peor de los casos, igual que al principio. Una vez que la primera petición se complete, los objetos estarán almacenados en Redis en su forma final y no hará falta regenerarlos, ahorrando mucho tiempo (más ahorro cuantos más objetos que devolvamos estén cacheados).

Reduciendo aún más los tiempos

Si aún así queremos reducir más los tiempos de respuesta, podemos reducir la cantidad de objetos completos que obtenemos de la base de datos.

Esto lo podemos conseguir dividiendo en dos la obtención de datos desde nuestro modelo User: primero obtendremos los datos necesarios para saber qué hay en caché y qué no. Sabiendo qué objetos necesitan ser procesados, podremos obtener todos sus campos:

Obtención de Users en dos pasos para optimizar el tiempo de respuesta.

De esta manera inicialmente sólo obtenemos los campos id y updated_at de los usuarios (ya que son necesarios para generar la cache_key) para verificar qué usuarios nos falta y poder obtener en una segunda consulta sus datos, para posteriormente procesarlos y añadirlos a nuestra caché.

Es importante tener en cuenta que esta última parte puede mejorar los tiempos o empeorarlos, dependiendo de ciertos factores como la latencia de red con el servidor de base de datos, la cantidad de objetos que manejemos o la volatilidad de la caché, ya que mejora los tiempos cuantos más objetos necesitemos y más respuestas válidas obtengamos de caché.

Bola extra: optimizar la generación JSON con Oj

Si bien es cierto que hace ya varias versiones de Rails que la generación de JSON ha mejorado notablemente, es posible conseguir unos milisegundos más utilizando una gema que use extensiones nativas para formatear nuestras respuestas. Una gema que permite hacer esto es Oj.

Oj nos permite parsear y formatear JSON utilizando una extensión nativa en C, lo que será bastante más rápido. Sin embargo, al encargarse Grape de dicha conversión, no es trivial reemplazar al generador JSON de Rails por Oj. Para ello, podemos indicarle a ActiveSupport que use Oj:

Configurar ActiveSupport para que use Oj

Esto mejorará el tiempo de respuesta especialmente cuando se trate de documentos JSON muy grandes o complejos.

--

--