¿Problemas de rendimiento en tu API? Serializa con pluck y Arel

Rubén Sierra
5 min readJun 20, 2016

--

El rendimiento es uno de los problemas más molestos con el que nos solemos encontrar los desarrolladores. Un ‘memory leak’, un algoritmo con elevada complejidad, una consulta ‘N+1’… todo funciona correctamente pero alguno puede estar escondido esperando para hacerse notar cuando menos te lo esperes (muchas veces cuando la aplicación ya está en un entorno real). En uno de nuestros últimos proyectos, surgió un problema de rendimiento que no esperábamos encontrarnos y esta es la historia de cómo conseguimos solucionarlo.

La arquitectura de este proyecto está orientada a servicios. Una aplicación core donde centralizar el almacenamiento y gestión de información, y una serie de aplicaciones cliente que consultan los servicios que la API del core provee para consultar información.

Todo esto está montado sobre un stack de Ruby on Rails y usando ActiveModelSerializers para conformar la salida de las peticiones de la API (librería recomendada por Rails para tal fin). Con ActiveModelSerializers, por convención de nombres se relaciona un modelo de Rails con un serializer en el que se configura la información que queremos mostrar (el qué) y se elige un adapter para determinar el formato esa información (el cómo). En el momento en el que la API tiene que devolver datos de un modelo en particular, por defecto irá a buscar el serializer asociado y aplicará el formato del adapter para devolver el JSON adecuado.

Hasta aquí todo era fantástico y maravilloso, la base era sólida y todo funcionaba a la perfección. ¿Pero… que pasó cuando usamos datos reales? El modelado de datos de la aplicación no es trivial. Un gran número de modelos con múltiples relaciones entre sí y la necesidad de representarlos de distinta forma en función de quién y cómo los solicite. Esto se tradujo en que algunas peticiones a la API empezaban a tardar demasiado de cara a las aplicaciones cliente, incluso habiéndonos preocupado por evitar consultas ‘N+1’. ¿Que estaba pasando? En este caso, la instanciación de objetos.

Cada petición a la API conllevaba la instanciación de los objetos de Active Record y de los serializers correspondientes, junto con los de todas las relaciones indicadas, algunas anidadas en forma de árbol. Una vez estaba todo instanciado, los adapters tenían que conformar el JSON para enviarlo por la red y que los clientes volviesen a instanciar los objetos correspondientes al otro lado. Algunas pruebas y mediciones confirmaron que el rendimiento que estaba dando Rails y ActiveModelSerializers con tantos objetos y relaciones era muy bajo para tener unos tiempos aceptables en las respuestas al usuario final.

En este punto la API ya estaba completamente funcional, por lo que nuestro primer objetivo fue intentar reducir la instanciación de objetos manteniendo la estructura de la aplicación. La solución: Pluck y Arel.

Pluck y Arel

Pluck es un método de Active Record introducido en Rails 3.2 que permite consultar columnas específicas de un modelo sin necesidad de instanciar el objeto. Se puede utilizar junto con los scopes normales, pero siempre como último elemento en la cadena, ya que el resultado es un array.

>> Page.all.pluck(:id, :published)
(0.5ms) SELECT `pages`.`id`, `pages`.`published` FROM `pages`
=> [[1, true], [2, true], [3, false]]

Por otra parte, Arel es una librería de gestión de árboles SQL de sintaxis abstracta (SQL AST) que permite simplificar la creación de consultas SQL a distintas bases de datos. Fue introducida en Rails 3 y es la base sobre la que se soporta Active Record para generar las consultas SQL. Realmente, cada vez que pasamos un hash a un método de Active Record, Arel esta trabajando por debajo. Arel no cambiará nuestra vida (respecto a las bases de datos) pero nos permitirá hacer consultas de una forma mas afinada, elegante, reutilizable y en algunos casos, más eficiente.

Como pequeña introducción, todo modelo Active Record, dispone de un método ‘arel_table’ que devuelve un objeto Arel::Table del podemos obtener los Arel::Nodes correspondientes a cada una de las columnas de la tabla de datos del modelo. Sobre estos nodos podemos aplicar una serie de predicados, más amplios que los disponibles con Active Record, por ejemplo:

  • Comparaciones: eq, not_eq, lt, gt, lteq, gteq, in, matches
  • Agregación: having, sum, average, maximum, minimum, count, group
  • SQL: project, join.on, take, skip, and, or
Ejemplos de scopes básicos con Arel

Estos son algunos ejemplos sencillos de consultas que con Active Record habría que recurrir a algo de SQL, pero pueden llegar a hacerse cosas más complejas como:

Los Pluckers

Con estas dos herramientas y un poco de metaprogramación, acuñamos el concepto de Pluckers, objetos orientados a sustituir a los serializers para obtener los datos directamente serializados sin necesidad de instanciar objetos.

El plucker básico recibe una relación Active Record y una especificación de campos y relaciones. Mediante metaprogramación se infieren los distintos tipos de relaciones (has_many, belongs_to...) y con ayuda de Arel se componen las distintas consultas para poder extraer los datos de las relaciones según su tipo.

Ejemplo de inferencia de una relación has_and_belongs_to_many

Finalmente con pluck, se van obteniendo los atributos especificados de cada modelo y se compone el JSON que, sin más transformación podrá devolver nuestra API.

Además, hay casos en los que se necesita un tratamiento específico de la información a devolver. Esta lógica que antes podía estar en un método del serializer o del modelo (algo tan sencillo como concatenar dos campos para devolver sólo uno), ahora ha de ser transferida a los pluckers para aplicarla directamente sobre la información ‘cruda’ que obtenemos de la base de datos, con algoritmos algo más funcionales que los que estamos acostumbrados en objetos, pero que también darán algo mas de rendimiento al resultado final.

Este sería un ficticio serializado con pluckers de las páginas del anterior ejemplo, con información de SEO y varios bloques de contenido con imágenes que necesitan un tratamiento específico de algún dato:

Ejemplo de plucker

Y con esto llega el final feliz, introduciendo poco a poco los pluckers en la API, pudimos cambiar progresivamente la funcionalidad sin afectar a la respuesta y finalmente los tiempos son considerablemente mejores, con números hasta diez veces más bajos en los mejores casos. Todas las llamadas responden ahora en tiempos adecuados para el usuario final y todo vuelve a ser fantástico y maravillo ;-).

¡Nos veremos en el futuro!

--

--