De la ejecución secuencial a la paralela

La optimización del Importador

Eduard Fabra
Building the Wine&Spirits Marketplace
8 min readJun 4, 2021

--

Uno de los mayores retos al que nos enfrentamos tras la compra de Uvinum por parte de Pernod Ricard y la correspondiente transición a Drinks&Co, fue preparar nuestro “Importador” para permitir procesar un mayor número de catálogos sin aumentar, si a caso reduciendo, el tiempo que se tarda en procesar y tener listas las ofertas diarias de cada afiliado en nuestro Marketplace.

Llamamos Importador a la herramienta que se encarga a diario, de recoger los productos de los catálogos de cada una de nuestras tiendas afiliadas. Su responsabilidad es procesar e incorporar las diferentes ofertas con nuestro catálogo de productos a la venta del Marketplace. Es lo que se llama un ETL y en este caso es parte crítica en nuestro modelo de negocio.

Un poco de historia

En los casi 11 años que llevo en la compañía (y como padre de la criatura 🤗), este proceso siempre ha sido un proceso largo y costoso. Para hacernos una idea:

  • Allá por el diciembre de 2010, empezamos procesando 4 catálogos, insertando las ofertas en Magento. Por aquel entonces con 4 tiendas y unas 10k ofertas, la importación ¡ya tardaba una hora!.
  • Más tarde, una vez validamos que las conversiones eran mucho mejores si las compras se hacían en la propia plataforma, sin necesidad de redirigir al comprador a la web del Afiliado, decidimos crear el código del proceso la compra como propio de Uvinum. Año tras año, procesamos cada vez más catálogos de Afiliados:
  • 2015, ya procesamos 99 Afiliados y unas 900k ofertas. Tardaba 40 minutos.
  • 2016: 131 Afiliados y unas 1000k ofertas. Duración: 50 minutos.
  • 2017, procesaba 169 Afiliados y 1400k ofertas. Superamos la barrera psicológica de la hora: 1 h y 7 minutos.
  • 2018: 167 Afiliados y 1120k ofertas. Duración: 1 h y 17 minutos.
  • 2019: 218 Afiliados y 600k ofertas. Duración: 1 h y 52 minutos.
  • Finales de Enero 2020: procesamos 282 Afiliados, y unas 580k ofertas. Habíamos superado ampliamente la barrera de las dos horas, tardando unas 2 h y 25 minutos. Récord absoluto de consumo de tiempo.
Ejemplo de salida del Importador cuando su ejecución era secuencial fase por fase
Ejemplo de salida del Importador cuando se ejecutaba de forma secuencial, en el que se puede apreciar como se procesaba linealmente los distintos Afiliados.

Era evidente que teníamos un problema encima de la mesa. El aumento de tiempo era proporcional al número de catálogos de Afiliados y ofertas que se procesaban. Se nos presentaba un desafío mayúsculo y teníamos que hacer algo. Así que desde el equipo de Affiliates, nos pusimos en marcha para tratar de buscar una solución a este problema.

Nuestra principal meta era conseguir escalar nuestro proceso de importación, para que el número de catálogos/tiendas a procesar no determinase la duración del proceso. En nuestras mentes estaba volver a aproximarnos al máximo a la barrera psicológica de 1 hora.

Manos a la obra y primeros pasos: divide et impera…

Primero estudiamos cómo dividir el Importador en distintas fases, de modo que nos permitiera trocear los distintos pasos del proceso en pasos más pequeños. Ejemplos:

  • Descarga de los feeds
  • Normalización de los feeds
  • Validación de los feeds
  • Procesado de los feeds

Era trabajar con las sensaciones similares a la de un estudio de arquitectos que recibe el encargo de hacer nuevo por dentro un edificio, respetando la fachada… Todo esto sin perder de vista que el importador debía seguir funcionando para seguir proporcionando las ofertas de los Afiliados al Marketplace.

La idea que perseguimos era implementar la ejecución de las fases de la importación en paralelo para así ganar tiempo, aprovechar recursos utilizando diferentes hilos y dejar de procesar estas fases secuencialmente por cada feed.

¿Todas las fases eran paralelizables?

Todas las fases de la importación se podían ejecutar en paralelo menos una: la fase de linking.

Esta fase es responsable de asociar ofertas de un Afiliado a un producto existente, en caso de no encontrar producto en el marketplace, procede a su creación. Este paso debía seguir siendo completamente lineal, ya que al ejecutarse en paralelo, podría crear productos duplicados en nuestra plataforma aparte de añadir complejas condiciones de carrera.

La fase de “Linking” se tenía que mantener como secuencial, ya que su comportamiento esperado, es que si en la importación de la tienda A, se crea un producto, al importar la siguiente tienda, B, este ya encuentre el producto en nuestro marketplace y le asocie una nueva oferta.

Afortunadamente, no es un proceso que tenga una duración muy extensa, ya que no se suelen crear muchos productos nuevos en una importación diaria.

Handicap: trabajar sin romper nada en un entorno sin tests

El importador es un producto que va evolucionando con el paso de los años, pero su punto de partida es de 2010, con lo que teníamos una base de código con 10 años de antigüedad y que no tenía tests, ni unitarios ni funcionales en la mayoría de sus líneas de código.

Únicamente podíamos ir comparando la entrada de datos (feeds de los Afiliados) con la salida de datos esperada. Este era nuestro rudimentario sistema para asegurar que lo que se hacía antes y lo que estábamos reescribiendo producían el mismo resultado.

Fueron tiempos de horas invertidas en comparadores de ficheros para verificar que obtenemos el mismo resultado en una ejecución con el código antiguo contra una ejecución con el nuevo código.

En todo el código nuevo que íbamos abstrayendo y separando del script inicial, le empezamos a hacer test unitarios, para aplicar buenas prácticas a lo que estábamos haciendo y estar más seguros de lo que se desarrollaba.

Trazabilidad y control de errores

Otro factor a tener en cuenta en este punto, fue el darnos cuenta de que si queríamos paralelizar procesos en segundo plano, debíamos conocer en todo momento en que estado se encontraba cada importación de cada catálogo. Para ello añadimos 3 diferentes eventos a cada etapa.

  • Nada más empezar a ejecutarse una fase, dispara un evento que notifica que la fase ha comenzado para un afiliado
  • Si la ejecución de la fase termina correctamente, dispara un evento notificando que la fase ha terminado correctamente
  • Si ocurre algún error, se dispara un evento que contiene dicho error en dicho afiliado

Mediante estos eventos empezamos a sacar logs y benchmarks de los propios scripts de cada fase a Event Listeners, además que los propios eventos nos permiten automatizar acciones como re-encolar un afiliado a un paso concreto, notificar a un afiliado si hay algún error con su catálogo… y todo un nuevo mundo de posibilidades que se abría ante nosotros.

Eventos, colas, máquinas de estados

Una vez terminamos de separar todas las fases existentes en la importación en scripts independientes con sus correspondientes eventos de inicio, fin satisfactorio o fin inesperado, era el turno de crear un script orquestador que se encargue de lanzar la importación inicial de cada tienda añadiendo un ”job” en la cola correspondiente para que se fueran ejecutando las distintas fases.

Creamos máquinas de estados adecuadas a cada fase, que controlaban en qué estados se podía lanzar la siguiente fase. De este modo, conocíamos en qué fase se encontraba cada tienda que se procesaba y podíamos saber si todas las tiendas ya se habían procesado o no, para poder empezar la siguiente fase.

Almacenamiento intermedio entre fases: se acabó usar ficheros en local

Trabajar en paralelo en varios hilos o incluso en varias máquinas, te obliga a que todo esté disponible desde un sistema de almacenamiento intermedio, ya que no puedes almacenar nada en local.

Inicialmente, pensamos que S3 era una buena solución para almacenar los feeds y descargarlos al inicio de cada fase, pero nos encontramos que se incrementaban los tiempos debido a la descarga del feed en cada fase, sobretodo con feeds grandes, de varios MB (que son la mayoría).

Teníamos que pensar y testear alternativas y fue así como llegamos a encontrar la que fue la solución que más nos convenció: usar REDIS como almacenamiento intermedio entre las distintas fases. Optamos por reescribir el código usando REDIS como sistema de almacenamiento en memoria ya que permitía acceder a los datos desde varias máquinas con una velocidad aceptable.

De este modo, establecimos que en la fase inicial de la descarga se usaría S3 y en el resto de fases usaríamos REDIS como capa de almacenamiento intermedio.

Software listo. Es la hora del Hardware

Ya teníamos todo el código preparado, hasta este momento seguíamos ejecutando todos los pasos de forma secuencial en una sola máquina, al mismo tiempo estábamos cerca de las dos horas y media por importación.

Así que había llegado el momento de preparar cuál iba a ser nuestro esquema de máquinas.

Optamos por tener esta configuración:

  • 1 Máquina (Jiman alias Heman, pero en cañí 😜) con 2 procesadores y 1 worker escuchando que es la que se encarga de orquestar el inicio de la importación de los catálogos y la que se encarga también de la fase de linking que es la que se tiene que ejecutar en secuencial.
  • 2 Máquinas (Mcclane) con 2 procesadores y 2 workers escuchando, por cada fase. En estas máquinas, se ejecutan el resto de fases de la importación en paralelo al tener en las colas dos workers escuchando los trabajos que les llegan.

La hora de la verdad. La primera ejecución del Importador en paralelo

Había llegado el momento de la verdad, de pasar de la teoría a la práctica. De probar los resultados de más de 1 año de trabajo desde que empezamos a separar las partes del importador. Era el momento de ver los frutos del trabajo de un equipo de 5 personas que, si bien, no siempre habíamos estado todos trabajando a la vez en el proyecto, sí que habíamos estado trabajando en una fase o en otra.

La verdad es que el éxito fue BRUTAL y dejamos una importación en la impresionante duración de ¡33 minutos en procesar 291 catálogos de Afiliados! Habíamos superado con creces nuestras mejores expectativas de intentar reducir la duración en 1 hora.

Gráfica de tiempos de la Importación con las distintas disminuciones de tiempos que se fueron consiguiendo al ir pasando las distintas fases a ejecutarse en paralelo.

Y lo que es más importante todavía, habíamos convertido un proceso secuencial en un proceso escalable en el que el número de catálogos a procesar ya no condiciona la duración del proceso, sino que, lo que lo condiciona es el hardware. Si el número de catálogos a procesar se incrementa y la duración se alarga, es tan sencillo como asignar más recursos a las máquinas Mcclane para que tengan más capacidad de proceso en paralelo o bien añadir un nuevo nodo Mcclane y la duración de la importación se mantenga en esos magníficos tiempos.

Ejemplo de consola de ejecución de los distintos jobs de las colas en una máquina de tipo Mcclane
La parelización del Importador ejecutándose en las distintas máquinas

Mi más sincero reconocimiento a todos los que hemos hecho posible (Albert Colom, Koldo Pikaza, Christian Puga, Ramón Serra y Eduard Fabra) este importante paso 💪🏻.

A fecha de la escritura de este artículo (mayo 2021), la importación sigue ejecutándose en la misma configuración de máquinas y sigue estando en torno a los 30 minutos procesando más de 300 Afiliados y 500k ofertas 👏🏻.

--

--

Eduard Fabra
Building the Wine&Spirits Marketplace

Analista programador especializado en e-commerce. Desde 1998 batallando con PHP. Trabajando en Uvinum-Drinks&Co desde 2010 como Affiliates Lead Developer.