Muchas empresas tecnológicas, como DoorDash, Amazon y Netflix, reciben a los usuarios con una página de exploración para ayudarles a inspirar su experiencia de compra. Estas páginas de exploración suelen presentar una gran cantidad de contenido, por lo que es un reto para el sistema backend servirlas a escala.
La página de exploración de DoorDash muestra una combinación de restaurantes y alimentos que recomendamos a cada usuario en función de su actividad anterior. En nuestros esfuerzos por mejorar la experiencia del usuario, hemos aumentado la complejidad de estas páginas incluyendo carruseles y listados de categorías para ofrecer una selección relevante y visualmente atractiva de las opciones de comida más cercanas.
Nuestro crecimiento en los últimos años puso de manifiesto que el sistema que utilizábamos para servir las páginas de exploración no era escalable, ya que realizaba llamadas repetidas y duplicadas a servicios posteriores. Implementar un sistema más ágil y escalable implicaba crear un nuevo patrón de diseño de canalización para servir el contenido de nuestras páginas de exploración.
Problemas con el servicio de nuestra página de exploración
En DoorDash, nuestra página de exploración ofrece una lista de restaurantes y tiendas recomendados en función del historial de participación y la ubicación del usuario. Mostramos elementos como carruseles, banners y mosaicos de colecciones para que los usuarios se desplacen y exploren las opciones que podrían gustarles.
Utilizamos un microservicio llamado Feed Service para impulsar nuestra página de exploración, que sirve como punto de entrada para las solicitudes durante toda la sesión del consumidor. El Feed Service organiza las respuestas a las solicitudes obteniendo datos de distintos proveedores de contenidos, añadiendo contexto y creando módulos de visualización personalizados como una respuesta de tipo feed antes de devolverlos a los clientes.
Sin embargo, el sistema anterior de Feed Service tenía varias limitaciones, lo que dificultaba la ampliación de la página de exploración con más restaurantes, tiendas y carruseles.
Llamadas ineficaces a otros sistemas
Nuestra página de exploración realizaba una cantidad innecesaria de llamadas a servicios posteriores para obtener la información que necesitaba para mostrar resultados a los usuarios. Para cada carrusel que construíamos, el sistema repetía el mismo flujo de descubrimiento de recuperación, clasificación e hidratación de contenidos, realizando llamadas de contenido duplicadas. A medida que aumentaba el número de carruseles que servíamos, este sistema ineficiente no podía escalar.
Limitaciones de la clasificación entre carruseles
El proceso de clasificación, que determina el orden en que mostramos los restaurantes y tiendas seleccionados en la página de exploración, se realizaba dentro del mismo servicio, llamado Servicio de Búsqueda, que el proceso de recuperación, lo que significaba que la clasificación sólo podía hacerse entre las tiendas o restaurantes recuperados. Dado que distribuimos el flujo de recuperación para cada carrusel, la clasificación sólo podía realizarse dentro del carrusel. Este enfoque nos impedía organizar los carruseles de la forma más óptima para los usuarios y, además, nos impedía mostrar más carruseles cuando no podíamos utilizar la clasificación para seleccionar los más relevantes.
Modularización mínima
Como ya se ha mencionado, cada flujo de descubrimiento puede desglosarse en pasos de recuperación, clasificación e hidratación de contenidos. Pero estos pasos no se extraen o destilan de un servicio existente. Por ejemplo, la funcionalidad de generación de candidatos se implementa por separado en múltiples aplicaciones que tienen funcionalidades muy solapadas. La falta de modularización en este sistema hizo que la sobrecarga de desarrollo continuo fuera proporcional a la complejidad de la lógica existente, ya que cualquier actualización de la generación de candidatos debía duplicarse en todas las instancias.
Modularización con un patrón de diseño de canalización
Convertimos las rutas de servicio existentes en el servicio de alimentación de datos de altamente imperativas a algo declarativo con abstracciones. Estructuramos el sistema en un patrón de diseño de canalización (también conocido como flujo de trabajo) agrupando las funcionalidades comunes en el mismo módulo e incluyendo un operador, como un trabajo o nodo, en el canal. Por ejemplo, abstraemos los conceptos de recuperación de candidatos y obtención de almacenes del Servicio de Búsqueda como una especificación de un operador de generación de candidatos. Del mismo modo, podemos tener más operadores para la clasificación, la hidratación de contenidos y el postprocesamiento. Los operadores individuales tienen un soporte estandarizado a nivel de marco para guardrails, observabilidad y propagación de contexto.
Ejecución de trabajos con un pipeline basado en DAG
Utilizamos un núcleo de ejecución desarrollado por DoorDash llamado Workflow que envía hilos y coroutines basados en dependencias de grafos acíclicos dirigidos (DAG) y ejecuta los trabajos reales. Como se mencionó anteriormente, cada trabajo en la tubería representa un módulo de funcionalidades comunes, que sirve como una abstracción superior, y puede ser:
- Evolucionado mediante una aplicación más compleja.
- Ampliado por otras aplicaciones de exploración que comparten flujos de trabajo similares.
Como se muestra en la Figura 1, el proceso de generación del contenido de una nueva página de exploración puede dividirse en las siguientes tareas:
- Recuperación de candidatos: Obtener fuentes de datos de servicios externos que proporcionan el contenido de la página, como el servicio de búsqueda para las tiendas y el servicio de promoción para los metadatos de los carruseles. En este caso, sólo obtenemos las fuentes de datos una vez para el contenido de toda la página de exploración para evitar la duplicación de llamadas.
- Agrupación de contenidos: Agrupación de contenidos en un conjunto de colecciones que pueden utilizarse posteriormente para la clasificación y la presentación, como la agrupación de tiendas basada en la asociación de carruseles o la lista de tiendas en la página de exploración.
- Clasificación: Clasificar las entidades dentro de cada colección agrupada. Este paso implica resolver el ID de modelo correcto, generar los valores de las características y realizar una llamada al servicio de predicción de aprendizaje automático para calcular las puntuaciones de cada candidato clasificado.
- Decorador de experiencias: Para el conjunto único de tiendas en todas las colecciones, necesitamos hidratarlas desde fuentes de datos externas para obtener más información relacionada con la experiencia del usuario, incluyendo fetch ETA, tarifa de entrega, URL de imágenes y valoraciones de las tiendas que se muestran.
- Procesador de diseño: Este procesador recoge todos los datos que se obtienen y produce marcadores de posición para diferentes estilos de presentación, incluyendo la página de exploración, modelos de datos de formularios para carruseles, listas de tiendas y banners.
- Post-procesador: Clasifica y postprocesa todos los elementos, como carruseles y listas de tiendas, de la página de exploración que se están procesando hasta el momento de forma programática para optimizar la experiencia del usuario.
Separar la clasificación de la recuperación
La transición de la clasificación del Servicio de búsqueda al Servicio de alimentación hace que la función de búsqueda dependa exclusivamente de la recuperación, mientras que la función de alimentación es responsable de la precisión de la personalización. Este cambio significa que ahora podemos realizar clasificaciones personalizadas tanto dentro de los elementos de la colección, como carruseles y listas de tiendas, como entre ellos. Cada usuario verá una página de exploración completamente personalizada con elementos clasificados, junto con elementos individuales que muestran restaurantes y tiendas clasificados.
Tener el módulo de clasificación dentro del servicio de fuentes nos permite implementar funciones más complejas en un servicio independiente que gobierna toda la lógica empresarial relacionada con las recomendaciones y la personalización. Utilizado de esta forma, el módulo de clasificación se convierte en una abstracción ligera que hace que el servicio de alimentación sea más escalable.
Mejorar la observabilidad
Podemos introducir la telemetría del sistema en la parte superior de nuestra canalización, además de los datos de telemetría de consumo existentes de las aplicaciones de usuario final, como se muestra en la Figura 2, a continuación. La telemetría captura automáticamente el contexto y los resultados de los componentes del flujo de trabajo, lo que permite una recopilación estandarizada de detalles de alta fidelidad que nos permiten saber qué ha ocurrido y por qué en el sistema. Los ingenieros y las partes interesadas funcionales podrán acceder a estos datos a través de una interfaz de autoservicio, lo que les permitirá conocer en profundidad la calidad de nuestros algoritmos de personalización.
Resultados
Este proyecto ha sido un éxito en muchos sentidos, ya que crea una arquitectura flexible para que DoorDash pueda ampliarse en los próximos años, abre oportunidades para productos y funciones más personalizados y sienta las bases para nuevas aplicaciones similares a las de descubrimiento.
Reducir los recursos informáticos
Observamos una enorme mejora en las métricas del sistema en todos los servicios posteriores. En particular, observamos:
- Reducción del 35% de la latencia de p95 para el punto final de alimentación de páginas de exploración y reducción del 60% de la CPU del servicio de alimentación.
- Reducción del 80% de las consultas por segundo y del 50% de la CPU del servicio de búsqueda.
- Una reducción global estimada del uso de 4.500 núcleos de CPU.
Desbloquear la clasificación en carrusel cruzado
El nuevo sistema nos ha permitido experimentar con algoritmos que clasifican todos los elementos de la página de exploración, incluidos los carruseles, las listas de tiendas, los mosaicos de colecciones y los banners, para garantizar que:
- Los contenidos más relevantes ocupan los primeros puestos.
- El contenido menos relevante puede recortarse de las listas y otros elementos de visualización, reduciendo el tamaño de la página.
Cimentar otras aplicaciones
Ampliamos el patrón de diseño del flujo de trabajo a otras aplicaciones relacionadas con explore que utilizan una secuencia de operaciones similar, como filtros de búsqueda y cocina, páginas de tiendas de conveniencia y páginas de centros de ofertas. Como cada módulo es una abstracción, cada aplicación puede tener su propia implementación del módulo o compartir la implementación generalizada. Este cambio mejoró nuestra productividad de desarrollo y facilitó mucho el mantenimiento del código.
Conclusión
En resumen, como muchas empresas tecnológicas, DoorDash se enfrenta al reto de ampliar su página de exploración para recomendar los mejores contenidos a los usuarios. Sin embargo, nuestro anterior sistema basado en Feed Service tenía varias limitaciones. Resolvimos nuestros retos de escalado introduciendo un patrón de diseño de canalización que modularizaba cada operador común, lo que se tradujo en una gran mejora de la eficiencia tanto del sistema como del desarrollo.
Aunque el nuevo sistema ha sido un éxito, de ninguna manera será la última iteración de nuestra mejora continua en la optimización de la experiencia de exploración de DoorDash. Habrá más iteraciones en el ajuste fino de cada módulo del sistema para ser más eficiente y flexible, de tal manera que el servicio de alimentación puede llegar a ser más ligero y escalable para el rápido crecimiento de DoorDash en los próximos años.
Los equipos de ingenieros que se enfrentan a problemas de escalado pueden encontrar una solución en el patrón de diseño de canalización. Permite modular los componentes de un flujo de trabajo, creando un sistema más flexible con funciones que pueden utilizarse en múltiples aplicaciones y funciones. También puede dar lugar a un aumento significativo de la eficiencia mediante la eliminación de código y procesos duplicados.
Agradecimientos
Gracias a Jimmy Zhou, Rui Hu, Sonic Wang, Ashwin Kachhara, Xisheng Yao y Eric Gu por su implicación y contribución a este proyecto, y un agradecimiento especial a Yimin Wei por construir el motor de ejecución de Workflow.