Ir al contenido

DoorDash utiliza el aprendizaje automático para determinar en qué invertir mejor su presupuesto publicitario, pero la rápida evolución del mercado y los frecuentes retrasos en la recopilación de datos dificultaban nuestros esfuerzos de optimización. Nuestro nuevo modelo de previsión de atribución nos permite predecir la eficacia de las campañas publicitarias a partir de sus datos iniciales, lo que nos ayuda a tomar antes decisiones empresariales críticas".

Normalmente, tenemos que esperar algún tiempo para medir el rendimiento de los anuncios debido a nuestra metodología de atribución. Esta lentitud de reacción significa que, si un canal publicitario funciona especialmente bien, no podemos trasladar rápidamente nuestros fondos de marketing a ese canal. 

Nuestro nuevo modelo de previsión de atribución predice el volumen de conversión final de un anuncio tras observar únicamente sus datos iniciales. Esto nos permite utilizar datos más recientes, optimizando nuestras conversiones por gasto publicitario al escalar los mejores canales a medida que el rendimiento cambia con el tiempo.

Más allá de la optimización rutinaria, este marco de previsión es especialmente útil durante los experimentos de marketing -donde identificar antes el anuncio ganador acelera el impacto- y podría extenderse a otros problemas de información diferida, como la venta de entradas para conciertos, las reservas hoteleras o la venta de flores navideñas. 

Introducción a la metodología de atribución

Antes de hablar del modelo de previsión, analicemos qué es un sistema de atribución y cómo elegir uno. Un sistema de atribución ayuda a las empresas a medir la eficacia de las campañas de marketing. Tiene dos elementos clave: el método de atribución y la ventana de atribución.

Revisión de los métodos de asignación

El método de asignación determina cómo asignar el crédito a través de los puntos de contacto de marketing cuando un nuevo consumidor realiza su primer pedido, lo que comúnmente se denomina conversión. Los métodos de asignación pueden ser monotáctiles o multitáctiles, dependiendo del número de canales que obtengan crédito por la conversión. Véase la ilustración de la figura 1.

  • El contacto único asigna todo el crédito de conversión a un único canal, asumiendo que el primer o el último punto de contacto impulsa la conversión.
  • Multi-touch tiene en cuenta todos los puntos de contacto a lo largo del camino hacia la conversión. Como ejemplo sencillo, podría distribuir el crédito entre todos los puntos de contacto por igual.
Figura 1: La elección del método de asignación afecta a la cantidad de crédito que recibe cada canal. En este ejemplo, el cliente interactuó con tres puntos de contacto de marketing antes de realizar su primer pedido: un anuncio de televisión de DoorDash, un anuncio en redes sociales y un anuncio de búsqueda de palabras clave de pago.

Compromiso en las ventanas de atribución

Intuitivamente, el impacto del marketing no siempre es inmediato. Después de que el cliente del ejemplo anterior viera el anuncio de DoorDash en televisión, podría haber tardado días o incluso algunas semanas antes de realizar su primer pedido. Por lo tanto, otra cuestión clave en la metodología de atribución es cuántos días mirar hacia atrás al definir los puntos de contacto de marketing. Este período de tiempo es la ventana de atribución.

Una ventana de atribución larga permite a las empresas reconocer más conversiones para un anuncio determinado. La figura 2, a continuación, muestra la diferencia en la curva de costes entre dos ventanas de atribución. Con una ventana de atribución más corta (la atribución de siete días abajo en rojo), los nuevos clientes que se convirtieron después de siete días no se acreditan al anuncio, lo que lleva a una curva de costes que subestima la eficacia del anuncio.

Manténgase informado con las actualizaciones semanales

Suscríbase a nuestro blog de ingeniería para recibir actualizaciones periódicas sobre los proyectos más interesantes en los que trabaja nuestro equipo.

Al mismo tiempo, los patrones de conversión varían según los anuncios. Por ejemplo, algunos anuncios, como los de los canales de búsqueda, conducen a una conversión rápida, mientras que los anuncios de otros canales, como la televisión o la radio, son más lentos. Una ventana de atribución más corta hará que subestimemos más la atribución de los anuncios más lentos, lo que dará lugar a decisiones de marketing subóptimas.

Figura 2: Cada punto representa el gasto en marketing de un día y los nuevos clientes atribuidos. Cuando la ventana de atribución es corta, los datos de conversión atribuidos son incompletos, lo que da lugar a una curva de costes subestimada.

Sin embargo, una ventana de atribución demasiado larga tampoco es deseable porque requiere un tiempo de espera más largo para medir completamente el rendimiento de un anuncio. En un mercado que cambia con rapidez, los tiempos de espera más largos pueden dar lugar a curvas de costes desfasadas. Si pudiéramos incluir de algún modo estos datos que aún no están disponibles, la información adicional mejoraría materialmente nuestras curvas de costes y, a su vez, las decisiones de marketing, como se muestra en la Figura 3:

Figura 3: Con una ventana de atribución larga, los últimos datos de conversión (rojo) no están disponibles hasta muchos días después de la compra de un anuncio. Los últimos datos disponibles (negro) son obsoletos, lo que da lugar a una curva de costes desfasada.

El problema de nuestro antiguo sistema de atribución

Actualmente, DoorDash utiliza un sistema de atribución de último toque de varios días para todos los canales de marketing digital, lo que proporciona un buen equilibrio entre una visión holística de las conversiones para la mayoría de los anuncios y un tiempo de espera razonable para un rendimiento de atribución totalmente actualizado. 

Sin embargo, una ventana de atribución de varios días sigue significando que los anuncios publicados en los últimos días se basan en datos de atribución incompletos, que no pueden informar las decisiones de marketing hasta que la ventana haya transcurrido. Dados los rápidos cambios en el panorama del marketing de entrega de comida, tener que esperar antes de reaccionar a los datos recientes no es lo ideal. Necesitábamos una forma de desbloquear nuestros datos más recientes.

Previsión de los resultados finales

Antes de entrar en detalles, hablemos de la solución ideal. Nuestros canales publicitarios tienen formas y tamaños diferentes. Por ejemplo, publicamos un pequeño número de anuncios de televisión de alto coste y un gran número de anuncios de búsqueda de bajo coste (a veces dirigidos a palabras clave concretas y poco conocidas, como errores ortográficos de "DoorDash"). Lo ideal sería que nuestra solución gestionara tanto anuncios pequeños como grandes.

El enfoque que elegimos fue construir un modelo de previsión que predijera los datos finales de atribución a partir de una cantidad limitada de datos iniciales.

Definición de la precisión de las previsiones

Una forma sencilla de medir el rendimiento de nuestro modelo de previsión es mediante el backtesting. El backtesting consiste en entrenar el modelo con datos antiguos y comprobar si puede predecir datos más recientes.

La principal métrica de rendimiento elegida es el error medio absoluto (MAE), 

donde ci son las conversiones atribuidas al anuncio i y el sombrero ^ distingue una predicción del valor real. Como el MAE simplemente toma el valor absoluto de los errores de predicción, no está sesgado hacia los anuncios más grandes (a diferencia del error cuadrático medio, RMSE) o los más pequeños (a diferencia del error porcentual absoluto medio, MAPE).

Sin embargo, uno de los inconvenientes del MAE es que varía con el volumen de conversiones, lo que dificulta la comparación entre canales u otros segmentos, como el día de la semana. Para facilitar la comparación, normalizamos el MAE por volumen de conversión:

Construcción del modelo de previsión

Queríamos un modelo de previsión que actualizara sus predicciones a medida que recogíamos más datos. Debería ser capaz de predecir el resultado final de la atribución, tanto si hay cuatro días de observaciones como si hay diez. Cuantos más datos iniciales tenga el modelo, más precisa será la predicción.

Evaluamos dos tipos de modelos: 

  • Modelos heurísticos sencillos 
  • Modelos de aprendizaje automático

Modelos heurísticos sencillos

Los modelos más sencillos que hemos considerado suponen que el patrón de conversión de un anuncio será el mismo en el futuro que en las N semanas anteriores. Por ejemplo, supongamos que queremos predecir el número de conversiones atribuidas a un anuncio al final de una ventana de 30 días, c(30 d). La predicción para el día t (día t tras la publicación del anuncio) es

donde c(t) es el número de conversiones atribuidas observadas hasta el momento. Este enfoque aplica directamente un coeficiente histórico para predecir las conversiones finales a partir de la observación actual.

A continuación se presentan algunos de los parámetros o variaciones que exploramos con este modelo heurístico. Seleccionamos el mejor modelo parametrizado mediante backtesting, como se describe en la sección anterior.

  • Número de semanas N utilizado para calcular el índice de conversión histórico. Este parámetro corresponde a la cuestión de cuánto tiempo se mantiene el patrón de conversión: demasiado tiempo ( N mayor) podría ser lento para captar los cambios del mercado, mientras que demasiado corto ( N menor) podría ser ruidoso. Hemos considerado valores de una a doce semanas.
  • Agregación. En relación con el punto anterior, los anuncios pequeños pueden generar muy pocos datos para calcular con seguridad el ratio de conversión histórico. Agregar anuncios similares (por ejemplo, del mismo canal, creatividad o región) al calcular el ratio puede reducir el ruido.
  • Ajustes de estacionalidad. La estacionalidad, especialmente el día de la semana, desempeña un papel importante en la conversión de nuevos clientes. Por ejemplo, es más probable que un consumidor haga su primer pedido un fin de semana por la noche que un martes por la noche. Para tenerlo en cuenta, podríamos calcular un ratio histórico diferente para cada día de la semana.

Modelos de aprendizaje automático

Esta previsión es un problema de regresión típico. Probamos los siguientes métodos de regresión de aprendizaje automático:

Resultados

Como se muestra en la Figura 4, los modelos LightGBM y heurístico simple superan significativamente a los demás modelos.

Figura 4: Cada barra representa la MAE normalizada media de las cinco semanas de backtesting. La barra de error de la parte superior muestra el error estándar.

Sin embargo, ¿cómo se traduciría esta mejora de la precisión en mejores decisiones de marketing? Para comprender mejor el verdadero impacto, volvimos a incluir las predicciones del modelo de previsión en nuestro flujo de trabajo descendente y las utilizamos para dibujar curvas de costes. La figura 5, a continuación, muestra que cuando se incluyen estas predicciones, la curva de costes de un anuncio de ejemplo captura la eficacia del gasto con mayor precisión, lo que a su vez nos ayuda a asignar nuestro presupuesto de marketing de forma más óptima. En este caso, sin las predicciones, subestimaríamos el rendimiento del anuncio y asignaríamos por error parte de su presupuesto a otros canales.

Figura 5: Cuando sólo se utilizan los datos reales históricos (puntos negros) para construir una curva de costes (línea negra), la curva predice mal el rendimiento futuro de los anuncios (triángulos azules). En cambio, cuando se incluyen también los resultados del modelo de previsión (cuadrados rojos), la curva de costes (línea roja) es más precisa, como en la figura 3.

Al igual que en el caso de la precisión de las previsiones, hemos realizado pruebas retrospectivas de la precisión de las curvas de costes utilizando el mismo método y la misma métrica MAE normalizada. Actualmente, las curvas de costes son capaces de alcanzar un MAE normalizado razonable con datos históricos retrasados, como muestra la figura 6. Al introducir las predicciones, los mejores modelos (heurístico simple y LightGBM) reducen aún más el error.

Figura 6: La adición de los datos de atribución previstos más recientes de los dos mejores modelos de previsión (heurístico y LightGBM) mejora significativamente la precisión de la curva de costes.

En conclusión, al aprovechar los datos recientes con mayor rapidez, el modelo de previsión de atribución mejora notablemente nuestra capacidad para trazar curvas de costes y tomar decisiones de marketing. Comprobamos que los modelos heurístico y LightGBM tenían un rendimiento similar, por lo que optamos por producir el modelo heurístico porque es más sencillo e interpretable.

Resumen y próximos pasos

Llegados a este punto, cabe preguntarse por qué el modelo más sencillo es el que mejor funciona. Creemos que hay dos razones:

  • Patrones sólidos existentes: Las conversiones de los consumidores tras un punto de contacto de marketing suelen seguir un patrón determinado. La mayoría de los consumidores van a convertir en los primeros días, y el número se reduce gradualmente. Los factores externos desempeñan un papel relativamente pequeño a la hora de influir en el comportamiento del consumidor cuando el embudo de incorporación es corto. Por lo tanto, una simple heurística capta adecuadamente el flujo de conversión.
  • Cantidad limitada de datos: Normalmente, sólo disponemos de varios días de observaciones en los que basar una predicción. Con esta pequeña cantidad de datos, los modelos ML más complejos no muestran muchas ventajas. 

Cuando los patrones son menos obvios, o los anuncios de interés abarcan una jerarquía de diferentes regiones o países, una simple heurística podría no funcionar tan bien y podría estar justificado un modelo más avanzado.

Una metodología similar podría aplicarse a los sistemas de atribución de otras empresas. En función de los datos disponibles, un modelo sencillo como la heurística simple podría ser un buen punto de partida. Más allá de la atribución de marketing, las aplicaciones también podrían incluir otras situaciones de respuesta retardada. Por ejemplo, predecir el volumen final de entradas de un concierto a partir de los primeros días de venta, o predecir la ocupación final de un hotel en un día determinado a partir de las primeras reservas. Conocer antes el resultado final permite reaccionar con mayor rapidez y tomar mejores decisiones.

Dada la variabilidad en el papel de director técnico de programas (TPM), puede ser difícil saber si una nueva oportunidad hará crecer tu carrera de forma masiva o no cumplirá tus expectativas. Después de años construyendo equipos TPM de clase mundial, anteriormente en Uber y ahora en DoorDash, he desarrollado algunos principios sobre cómo construir y dirigir una organización TPM de alto funcionamiento e impacto. Si está buscando una nueva oportunidad de TPM y se centra en el crecimiento, el alcance y el impacto, debe buscar un equipo que siga estos principios.

La misión que establecí para mi equipo de TPM en DoorDash es impulsar iniciativas de ingeniería complejas y multifuncionales. Sus principales contribuciones incluyen la gestión eficaz del programa, la definición de procesos ligeros y adecuados, y la garantía de que se comprenden y alcanzan las métricas de éxito del programa. Todo esto se lleva a cabo a través de la lente de una sólida perspectiva técnica, que permite al TPM contribuir directamente a la definición y ejecución satisfactorias del programa descubriendo y solucionando lagunas en lugar de simplemente informar de los progresos realizados por los ingenieros que trabajan en el programa. Un TPM sólido permitirá la toma de decisiones estratégicas proactivas y oportunas, una clara alineación entre los equipos y las partes interesadas, la rendición de cuentas y la apropiación, así como la ejecución satisfactoria de los objetivos del programa. 

El equipo de TPM tiene una sólida posición de liderazgo en ingeniería

El TPM debe ser una función centralizada dependiente del jefe de ingeniería. Aunque los TPM representan una pequeña fracción del equipo de ingeniería, contribuyen al éxito de los programas de ingeniería interfuncionales y ejecutan los proyectos más complejos.

Los equipos de TPM tienen un asiento en la mesa de liderazgo, lo que les permite dar su opinión sobre cómo la ingeniería construye para la fiabilidad, la velocidad y la integridad. Su trabajo ayuda al equipo de ingeniería a escalar eficazmente y a adelantarse a los mayores retos técnicos y de crecimiento, así como a llevar a cabo con éxito las mayores iniciativas interfuncionales. También ayudan al equipo de ingeniería a decidir cuándo construir de forma más general, con un objetivo de centralización y reutilización, como plataformas que escalan a través de múltiples casos de uso, o cuándo un caso de uso específico tiene más sentido. 

Los grandes equipos de TPM impulsan la alineación y las mejoras de procesos que empujan a la ingeniería a un nivel operativo superior con el tiempo. Además, dado que los TPM funcionan y se integran en todas las funciones, son un componente fundamental para la cultura, la diversidad y la inclusión, y el desarrollo colaborativo de un equipo de ingeniería en crecimiento.

Por supuesto, hay desventajas en un equipo centralizado de TPM, en particular en la retroalimentación de la superficie de ingeniería y otras partes interesadas multi-funcionales acerca de cómo los TPM individuales se desempeñan en sus funciones, lo que requiere que los gerentes de TPM verifiquen regularmente con estas partes interesadas. Desde mi experiencia, sin embargo, hay una ventaja para el equipo de TPM y el equipo de ingeniería si esas desventajas son gestionadas eficazmente por un fuerte liderazgo de TPM en lugar de distribuir a los TPM para trabajar bajo los líderes de ingeniería. 

Creo que es mejor para el equipo de TPM, los TPM individuales y la organización de ingeniería si el equipo de TPM está centralizado. Las ventajas de un equipo TPM centralizado incluyen:

  • Los equipos centralizados de TPM crean sinergias que conducen a mejores prácticas, aprendizajes compartidos y una forma rápida de encontrar respuestas a preguntas sobre la propiedad del servicio o la experiencia organizativa. Esta red de TPM es especialmente importante en las fases de alto crecimiento de una organización de ingeniería, donde las cosas cambian rápidamente y se documentan a la ligera. Los TPM trabajan más eficazmente en medio de un sistema de apoyo para compartir ideas, aprender lo que ha funcionado bien en otros equipos y combinar sus conocimientos colectivos a través de múltiples equipos de ingeniería y multifuncionales.
  • Una estructura de informes centralizada preserva la imparcialidad del TPM. Aunque su estructura de información está centralizada, los TPM siguen integrados en los equipos de ingeniería con los que trabajan en un programa concreto. Este modelo de integración les permite colaborar estrechamente con los ingenieros y ganarse la confianza de los equipos de proyecto, pero también les proporciona una vía alternativa segura para plantear, intercambiar ideas y resolver cuestiones técnicas, operativas, culturales u organizativas más polémicas.
  • Los equipos centralizados de TPM cuentan con líderes especializados en TPM en funciones directivas que, por lo general, tienen muchos años de experiencia en TPM. Estos líderes entienden la disciplina TPM en profundidad y pueden desarrollar y entrenar a los TPM de una manera óptima que les ayude a crecer y desarrollar sus competencias básicas y el impacto que ofrecen. La mayoría de los líderes de ingeniería no tienen esta experiencia, por lo que una estructura descentralizada de presentación de informes no es óptima para el crecimiento y la eficacia del TPM.
  • Los equipos de TPM siguen siendo esbeltos gracias a una estructura centralizada. Un gestor de TPM con experiencia coloca a los TPM en los equipos donde son necesarios, en lugar de un modelo descentralizado en el que cada equipo de ingeniería tiene su propio TPM. En este último caso, los TPM a menudo terminan haciendo trabajos menos especializados, como la propiedad de sprint/scrum, porque el líder de ingeniería cree erróneamente que es el mejor uso de su tiempo, cuando estas tareas generalmente deben ser manejadas por los gerentes de ingeniería o de producto. Los equipos centralizados de TPM impactantes contribuyen a una cultura de propiedad de la ingeniería. Este modelo también promueve el alcance y las oportunidades de crecimiento para los TPM, lo que facilita la contratación y retención de los mejores talentos.
  • Un equipo centralizado proporciona una visión de toda la organización, integrando a los TPM allí donde pueden apoyar las principales prioridades de la empresa, incluso cuando estas prioridades cambian rápidamente. A veces, esto significa trasladar un TPM de las prioridades de un equipo integrado a otro equipo o programa de mayor impacto o riesgo para la empresa. La flexibilidad de una estructura de informes centralizada fomenta estos debates y cambios de prioridades según convenga para llevar a cabo los proyectos más críticos para el éxito de la empresa y de la ingeniería. 

El equipo de TPM está muy apalancado

Los mejores equipos de TPM son fundamentales para abordar los mayores problemas de la empresa. Para asegurarme de que mis equipos mantienen un alto nivel de apalancamiento, soy especialmente consciente de cómo creamos el equipo en sus primeras etapas, contratando a una docena de miembros fundadores del equipo que sean los mejores talentos en sus dominios, así como expertos en gestión de programas, para que podamos construir en torno a su liderazgo a medida que se integran en cada área de ingeniería de alto nivel. Contar con miembros veteranos del equipo desde el principio lo prepara para un futuro apalancamiento e impacto, y nos permite añadir más TPM junior a su alrededor de forma más eficiente a medida que crece el equipo de ingeniería.  

Aunque los ratios son una medida terrible para una organización de ingeniería pequeña, se convierten en una forma razonable de planificar la contratación y la plantilla a medida que la ingeniería se convierte en una organización más grande. Mi ratio objetivo de ingeniero a TPM a escala es de aproximadamente 50 a 1. También me aseguro de que cada equipo de ingeniería con el que trabajan comprenda el valor que aportan y les dé la oportunidad de formar parte de la ejecución y la planificación estratégicas y tácticas. 

Manténgase informado con las actualizaciones semanales

Suscríbase a nuestro blog de ingeniería para recibir actualizaciones periódicas sobre los proyectos más interesantes en los que trabaja nuestro equipo.

Garantizar que el equipo está altamente apalancado también ayuda a atraer y retener talentos senior de TPM. Este alto apalancamiento garantiza que haya oportunidades de crecimiento para todos y elimina la necesidad de competir entre sí por el alcance y el impacto necesarios para el crecimiento profesional. Los TPM deberían estar constantemente automatizando e inventando, implantando procesos ligeros para el equipo o enseñando a alguien técnicas básicas de gestión de programas, buscando la forma de hacerse innecesarios para poder trabajar en la resolución de los nuevos retos de la organización. A menudo digo a la gente, irónicamente, que si cumplimos nuestros objetivos nos quedaremos sin trabajo. Por supuesto, en una empresa que experimenta un rápido crecimiento, las cuestiones comerciales y de escala implican que siempre hay nuevos problemas que abordar. Pero resolver los problemas de forma sistémica es un principio básico de los equipos de TPM de alto rendimiento.

Estar muy apalancado es también una parte importante de la dinámica interna del equipo de TPM. Un equipo que ve oportunidades de crecimiento apoyará a sus otros miembros, compartirá las mejores prácticas y aprendizajes, entrenará y orientará a los miembros más jóvenes del equipo, y creará sinergia trabajando juntos para resolver problemas más grandes de forma coherente en todas las organizaciones. Se desafiarán mutuamente para mejorar, lo que hace que la ingeniería sea mejor. Esta sinergia es la verdadera superpotencia de un equipo centralizado de TPM y, como líder, la forma más importante de fomentarla es asegurarse de que haya oportunidades para este tipo de polinización cruzada. Si una organización de gestión de programas cree en añadir un director de programa a cada equipo de proyecto de dos pizzas, entonces su liderazgo no sigue este principio de apalancamiento, lo que perjudica el crecimiento y la sinergia creados por un equipo que sí lo hace. 

El equipo de TPM está facultado para priorizar ferozmente el trabajo que dirigen

El trabajo que declinamos es tan importante como las iniciativas que asumimos, y tener principios claros para decidir qué trabajo producirá impacto y se adapta de forma única a nuestros puntos fuertes como TPM es un punto de vista importante que cultivar. Al igual que las decisiones de alcance que tomamos al crear un MVP, el tiempo de ingeniería es valioso y debe sopesarse con el rendimiento que proporciona una función. Del mismo modo, quiero cultivar un equipo que busque la forma más valiosa de emplear su tiempo cada día para ofrecer grandes resultados de ingeniería. Como solía decir uno de mis mentores: "Gasta tus latidos en lo que valoras". Para la mayoría de los TPM lo que valoramos es el impacto que tenemos en el éxito del equipo de ingeniería.

Como responsable de TPM, hago todo lo posible para que cada TPM tenga éxito en el equipo al que se incorpora. Antes de incorporar a alguien a una nueva área, trabajo con la dirección para clasificar todas sus prioridades con el fin de determinar qué debemos apoyar y qué no.  

Además, una vez que un TPM ha estado trabajando con el equipo y los conoce bien a ellos y a su pila tecnológica, debería empezar a cultivar su propio punto de vista. Esto probablemente significará añadir sugerencias sobre lo que debería estar en esta lista clasificada por pila, basándose en las lagunas que observen en aspectos como la forma en que el equipo planifica y entrega el trabajo, apoya a sus equipos asociados, gestiona las solicitudes entrantes, gestiona la deuda técnica y opera de guardia. Identificar las prioridades correctas para mejorar la eficacia de la ingeniería es una gran parte del valor que puede aportar un TPM integrado, con el tiempo, ya que establece la confianza con el liderazgo local de ingeniería y puede informar e influir en la forma en que el equipo asigna su tiempo de ingeniería. 

Ampliar la ingeniería es una de las competencias básicas de una organización TPM sólida. A menudo, las iniciativas que no asumimos son una oportunidad para elevar el nivel de nuestros equipos de ingeniería, orientándoles y desarrollando su capacidad para asumir iniciativas por sí mismos, en lugar de gestionarlo todo por ellos. En lugar de asumir la gestión de proyectos, por ejemplo, podríamos ayudar a los equipos a establecer mejores hitos para reducir el riesgo de un proyecto o sugerir una herramienta que ayude a realizar un seguimiento más eficaz de las dependencias del proyecto. Enseñar técnicas y herramientas de gestión de proyectos a nuestros directores de ingeniería y jefes técnicos que se encargan de la entrega de proyectos más contenidos nos ayuda a escalar y a elevar el nivel de la organización de ingeniería. Cuando un equipo puede gestionar sus propios proyectos contenidos, maximizamos la influencia del equipo de TPM, reservándolo para proyectos más grandes en los que intervienen muchos equipos de ingeniería y que realmente necesitan un experto en gestión de proyectos para tener éxito, en lugar de proyectos más pequeños en los que sólo intervienen uno o dos equipos. Al mismo tiempo, contribuimos a una cultura de ingeniería de alto rendimiento con una fuerte mentalidad de propiedad.

Los TPM deben poder centrarse en hacer la mayor contribución posible con su tiempo limitado. Los TPM no sólo forman parte de un equipo, sino de dos: el equipo de ingeniería con el que trabajan y el equipo centralizado de TPM. Todos los miembros de un equipo deben estar dispuestos a hacer lo que sea necesario para que el equipo avance, incluido el trabajo mundano. Los TPM no están por encima de ciertos trabajos. Incluso como director he pedido personalmente la cena cada noche para un equipo al que había encerrado en una sala de guerra para resolver un enorme problema de escalado del sistema con un plazo límite, porque en ese momento era la forma más importante en la que podía contribuir. La capacidad de centrarse en lo que tendrá mayor repercusión es algo que se gana con lo que se ofrece, el valor que se añade, la colaboración que se ofrece y la confianza que se genera.

El equipo de TPM impulsa sistemáticamente la claridad técnica 

Como TPM, nos integramos en la ingeniería y adquirimos profundos conocimientos de la organización de ingeniería con la que trabajamos. Desarrollamos experiencia en nuestra pila tecnológica, colaborando estrechamente con los jefes de tecnología y los directores de ingeniería para elaborar los entregables a corto plazo y dar forma a la estrategia a largo plazo y la dirección técnica. Los TPM deben ser capaces de conciliar las necesidades empresariales con los retos técnicos para ayudar a los ingenieros a equilibrar el tiempo de comercialización con la fiabilidad y la escalabilidad. Además, los TPM utilizan sus profundos conocimientos técnicos para ayudar a tomar decisiones técnicas, mitigar los riesgos técnicos y proponer alternativas. 

Nuestra asociación con el departamento de ingeniería se basa en nuestra capacidad para añadir valor a las soluciones, no sólo para realizar un seguimiento de los resultados. Los TPM operan de forma transversal, impulsando programas que incluyen muchos equipos y puntos de integración, y esta amplitud y escala suelen aumentar a medida que el TPM gana experiencia y antigüedad. En un equipo de ingeniería fuerte como el de DoorDash, no me preocupa tanto cómo se diseña y construye cada pieza individual, porque confío en que nuestros ingenieros y líderes técnicos se dirijan hacia la solución técnica correcta para su parte del problema. 

Sin embargo, un gran TPM garantiza que esta solución técnica se construya dentro del contexto general, asegurándose de que los equipos comprenden bien las necesidades empresariales, técnicas, de seguridad y de cumplimiento de la solución global. Al tener esta visión de conjunto, los TPM pueden garantizar activamente que todas las piezas del rompecabezas construyan realmente una solución holística una vez que se unen, y que no haya lagunas ni solapamientos entre los equipos y las partes de la solución. Aquí es donde realmente brillan los TPM y donde se ponen a prueba sus habilidades técnicas. Deben buscar estas lagunas o solapamientos y resolverlos activamente en el proyecto, así como alinear los hitos, entregables y dependencias del proyecto para que los problemas se detecten pronto y se resuelvan, reduciendo el riesgo para las fechas de entrega. La profundidad técnica también permite a un TPM hacer las preguntas correctas sobre la escala, el crecimiento y las métricas de éxito de la solución, de modo que invirtamos en una implementación que sirva adecuadamente a nuestros clientes, especialmente en un entorno de hipercrecimiento.

Nuestra perspicacia, tanto técnica como empresarial, nos permite entablar los debates adecuados sobre las ventajas y desventajas y asegurarnos de que se tiene en cuenta el panorama general al tiempo que se resuelve el problema inmediato. La profundidad técnica nos permite impulsar la responsabilidad formando parte del proceso de principio a fin, no limitándonos a seguir su progreso en una hoja de cálculo y a pedir a los ingenieros estimaciones y actualizaciones del proyecto. Es entonces cuando un equipo de TPM realmente brilla, aportando valor a la ingeniería al hacer que los problemas complejos sean consumibles, dando lugar a soluciones sencillas y eficaces.

El equipo de TPM aborda los problemas difíciles en una cultura libre de culpa

Los equipos de TPM caminan por la cuerda floja de la propiedad y la responsabilidad. Como TPM, dirigimos programas que abarcan muchos equipos, y ninguna de las personas implicadas en el trabajo real depende de nosotros. Gran parte de lo que hacemos consiste en dirigir la ejecución de algo transversal y complejo mediante la influencia, la alineación y la rendición de cuentas con respecto al plan del proyecto y los objetivos finales. Liderar con influencia es una competencia básica para un TPM eficaz, y debemos esforzarnos por crear claridad y alineación construyendo una narrativa de por qué el programa es importante y obteniendo el compromiso basado en la fuerza de esa narrativa en relación con otras prioridades empresariales o técnicas.

En DoorDash creemos en una cultura de propiedad y utilizamos un modelo DRI (individuo directamente responsable) para impulsar esa propiedad con claridad. Cada iniciativa importante en DoorDash tiene un DRI, y a veces, pero no siempre, es un TPM. 

El DRI es responsable de garantizar que el programa se complete con éxito. Esto significa asegurarse de que dispone de los recursos necesarios, de que los problemas se resuelven rápidamente, de que los riesgos se gestionan con eficacia y de que las decisiones clave se toman a tiempo, permitiendo que el programa avance. Si el TPM es el DRI del programa, su papel está claro: garantizar que se lleve a cabo con éxito. Pero incluso si otra persona es el DRI, el TPM será un socio clave en el éxito del DRI, y del programa, porque las habilidades clave de organización, definición, alineación, identificación y mitigación de riesgos, desbloqueo y resolución de problemas, y seguimiento de los progresos en relación con los objetivos del proyecto son todas responsabilidades clave del TPM. Un TPM fuerte no va a dejar que un programa fracase por problemas no resueltos, falta de alineación o falta de recursos. Estos son los problemas que luchamos por resolver para que un programa pueda ejecutarse con éxito.

Una de las cosas más importantes que proporciona un equipo de TPM centralizado es la neutralidad. Aunque el TPM esté integrado en un determinado equipo o área de ingeniería, la estructura de informes separada significa que el TPM sigue siendo una parte neutral. Una vez más, se trata de un poder que debe ejercerse con cuidado y ganarse el respeto y la confianza de las partes interesadas, pero quiero que mi equipo se sienta capaz de poner de relieve y comprometerse con el equipo y la dirección para resolver los puntos débiles, ya sean técnicos, organizativos o culturales, que impiden el éxito de un programa. 

Hay que incentivar al equipo para que actúe con franqueza y honestidad y para que cree claridad. Deben trabajar con ingeniería para fijar objetivos e hitos agresivos, sin rellenar, e informar con precisión de la situación del programa con respecto a esos objetivos, sobre todo si hay flujos de trabajo clave que están en amarillo o rojo con respecto a los planes. No podemos arreglar los problemas que no reconocemos, y la neutralidad garantiza que haya cierta cobertura aérea para hacer preguntas difíciles y sacar a la luz los problemas, con el objetivo final de trabajar con los equipos para llegar a una solución y entregar juntos el programa con éxito. A menudo, los problemas salen a la luz en las fronteras entre los equipos de ingeniería, donde un proyecto interfuncional se resiente porque no hay alineación entre los equipos en cuanto a objetivos, responsabilidades o plazos. Los TPM deben colmar estas lagunas entre equipos, y la neutralidad allana el camino para que esto ocurra de forma irreprochable.

El equipo de TPM es reconocido y valorado por la organización por su capacidad para poner orden en el caos

En DoorDash, cada uno de los TPM aumenta el reconocimiento y la reputación del equipo, día a día, con lo que ofrecemos: gestionando los proyectos hasta la línea de meta, asegurándonos de que cada ciclo de planificación es un poco más fluido que el anterior, realizando análisis posteriores eficaces para evitar futuras interrupciones e impulsando revisiones operativas mensuales que refuerzan gradualmente la ejecución de la ingeniería. 

Cuando me uní a DoorDash para dirigir la organización de TPM, supe que habíamos cruzado el punto de inflexión la primera vez que un líder fuera de ingeniería me pidió un TPM para impulsar un objetivo crítico de la empresa. La gente pregunta por nosotros cuando algo es grande, complejo, amplio y difícil. Puede que no siempre diga que sí, adhiriéndome al principio de priorización feroz, pero me alegro cuando alguien ve un problema y piensa en nosotros primero. Es entonces cuando realmente sé que lo hemos conseguido, al menos por hoy. Tenemos que levantarnos y volver a hacerlo mañana, con el siguiente problema difícil. Pero eso es lo divertido de este trabajo, si uno tiene la suerte de poder hacerlo en una empresa de rápido crecimiento, resolviendo problemas interesantes a escala con un grupo de ingenieros realmente increíbles y comprometidos. Me encanta formar ese equipo de TPM y conseguir ese impacto, junto con la ingeniería.

Reflexiones finales

Tu papel en un equipo y en una empresa es contextual, porque actuamos día a día en un entorno específico, no en el vacío, y nos vemos afectados de forma drástica por las características de ese entorno. Las cualidades del trabajo que realizamos y el tiempo que pasamos trabajando dependen en gran medida de la cultura y el liderazgo de la empresa a la que nos incorporamos. Como líderes, por tanto, somos responsables no sólo de los equipos que creamos y desarrollamos, sino también de nuestro impacto reflexivo e intencionado a la hora de proporcionar un entorno óptimo para que ese equipo prospere y haga su mejor trabajo al servicio de la empresa. 

Elegí venir a DoorDash por muchas razones, y una de ellas fue sin duda la cultura que vi aquí y cómo esa cultura fue desarrollada y alimentada por su equipo de liderazgo. No tuve que cambiar nada culturalmente para tener éxito o para desarrollar y construir un equipo de TPM de clase mundial dentro de DoorDash Engineering, y la trayectoria de crecimiento y escala proporcionada por el negocio crea las oportunidades de alcance e impacto que obligan a los grandes TPM. 

En otras palabras, el andamiaje ya estaba preparado para el equipo que yo quería construir y del que quería formar parte. Este equipo es uno en el que los TPM no sólo son valorados por sus habilidades básicas de gestión de programas, sino también por su pensamiento estratégico, contribuciones técnicas, resolución de problemas, impacto cultural positivo y talento operativo. A medida que continúo construyendo nuestro equipo de TPM en DoorDash, me esfuerzo por construir también el mejor entorno y cultura para que ese equipo sea un socio valioso en el éxito de la ingeniería. Soy increíblemente afortunado de tener la oportunidad de construir un equipo TPM de clase mundial en esta increíble compañía.

El equipo de TPM de DoorDash está creciendo. Echa un vistazo a estas oportunidades si estás interesado en unirte a nosotros.

En nuestro anterior artículo de esta serie tratamos la decisión que tomamos en DoorDash de pasar a una arquitectura de microservicios, las tecnologías que elegimos y cómo abordamos la transición. Como ocurre con la mayoría de las decisiones en informática, ésta también vino acompañada de su propio conjunto de retos y deficiencias. Este artículo se centra en los principales puntos conflictivos que encontramos al abandonar el monolito, con especial atención a los siguientes:

  • Cambios en los patrones de fiabilidad 
  • Migración de datos 
  • Visibilidad del flujo de trabajo
  • Capa de acceso a los datos 
  • Comprobabilidad general
  • Etiqueta de despliegue
  • Proliferación de pilas tecnológicas

Esperamos que nuestro resumen de estos retos sea útil para aquellos que están empezando a migrar a una arquitectura de microservicios, ya que ofrecemos una visión general de los problemas más comunes al abandonar el monolito, así como soluciones de alto nivel que hemos implementado o estamos implementando para resolverlos.

Cambios en los patrones de fiabilidad

Cuando se rediseña un sistema de un monolito a una arquitectura orientada a servicios (SOA), es habitual encontrarse con problemas de fiabilidad que pueden descontrolarse si no se abordan en una fase temprana del proceso. Estos retos se deben normalmente a la naturaleza intrínseca de trabajar con una SOA en lugar de con un monolito: cuando se trata del monolito, trabajar con lógica de negocio entre dominios es tan fácil como hacer una simple llamada a una función. Cuando se pasa a los microservicios, este tipo de interacción entre dominios se complica debido a que la comunicación de procesos internos se sustituye por RPC, lo que puede afectar a la latencia y la fiabilidad a menos que se establezcan las barandillas adecuadas.

El monolito de DoorDash era un único gran servicio respaldado por un único clúster Postgres como dependencia externa de acceso más frecuente. Mientras ambos componentes estén sanos, el sistema puede considerarse funcional en su mayor parte. Sin embargo, una vez que los microservicios entran en escena, comienzan a aparecer antipatrones como los siguientes: 

  • Dependencias fuertes, en las que los servicios se vuelven incapaces de satisfacer una petición del usuario, aunque sea parcialmente, a menos que uno o más servicios dependientes también funcionen.
  • Servicios T0 (Golden Workflows), una nueva clase de servicios críticos para la lógica empresarial, hasta el punto de que cualquier interrupción en cualquiera de ellos provoca que la aplicación deje de estar disponible.

En los dos casos anteriores, el efecto neto es que los SLOde la plataforma, ya sea a nivel de servicio o de todo el sistema, pasan a estar sujetos a la probabilidad compuesta de tiempo de actividad: si dos servicios forman parte de los flujos de trabajo dorados (como el cumplimiento de pedidos y el servicio logístico de envío en la plataforma de DoorDash) y ambos tienen un tiempo de actividad del 99,9%, el tiempo de actividad del flujo de trabajo en sí pasa a ser del 99,8%:

Fórmula 1: La probabilidad de que ocurran dos sucesos estocásticamente independientes, como la disponibilidad (A) de un servicio (S) y de su dependencia (D), viene dada por el producto de la probabilidad de que cada uno de ellos esté disponible.

Cuando demasiados servicios se enredan en una malla de fuertes dependencias, la plataforma se convierte en un monolito distribuido. Un monolito distribuido es estrictamente peor que un monolito normal en términos de fiabilidad, porque los fallos, incluso en flujos aparentemente sin importancia, pueden hacer caer todo el sistema.

Para evitar la proliferación de casos como el descrito y mitigar el efecto negativo de las dependencias entre servicios, introdujimos nuevos patrones de acceso para la comunicación entre servicios, como:

  • Solicitud de reserva, en la que nos aseguramos de que existe una fuente alternativa de datos críticos en caso de que la fuente principal no esté disponible. De este modo, se aumenta la probabilidad de tiempo de inactividad en lugar del tiempo de actividad, lo que se traduce en una mayor disponibilidad. Por ejemplo, si la fuente principal de unos datos concretos tiene una disponibilidad del 99,9% y la fuente alternativa también tiene una disponibilidad del 99,9%, el tiempo de actividad de un servicio no se verá tan afectado, como se muestra en la Fórmula 2, a continuación.
  • Fail open, si podemos servir parcialmente una petición cuando una dependencia no está disponible simplemente ignorando el fallo y aplicando algún comportamiento por defecto, habremos eliminado una dependencia fuerte de nuestro microservicio. Por ejemplo, si no podemos acortar una URL que deseamos enviar por SMS a un consumidor, utilizamos la URL completa en lugar de fallar la solicitud.
Fórmula 2: La probabilidad de que se produzcan dos eventos no excluyentes entre sí, como que la dependencia D del servicio S o su reserva DF estén disponibles en un momento dado, viene dada por la probabilidad de que D y DF no estén caídos al mismo tiempo. Esto, sumado a la supuesta disponibilidad del servicio S (99,9%), da como resultado un tiempo de actividad apenas afectado.

Con el ánimo de generalizar el uso de estos patrones, hemos introducido bibliotecas que se ocupan de ellos automáticamente, mediante recomendaciones del propietario del servicio. También hemos creado un campamento de fiabilidad en el que ayudamos a los nuevos desarrolladores a conocer los problemas más comunes de la creación de microservicios.

Migración de datos desde el almacenamiento monolítico

Al migrar a una arquitectura de microservicios, existe la expectativa de que cada servicio individual sea el propietario y la única fuente de verdad de sus datos. En el caso de DoorDash, el monolito no se limitaba simplemente al código de la aplicación, sino que nuestro almacenamiento de datos también era monolítico. En su mayor parte, nuestros datos existían en un único clúster Postgres que estaba alcanzando rápidamente sus límites en términos de escalabilidad vertical del nodo primario. Al descomponer la aplicación monolítica en microservicios, también tuvimos que realizar la extracción de datos del almacenamiento monolítico en varios clústeres.

La extracción de datos de una base de datos (BD) es un problema bien conocido en el sector y que no tiene fácil solución, sobre todo cuando la expectativa es no tener tiempo de inactividad durante la migración. Uno de los enfoques más comunes para las extracciones de datos es el uso de dobles escrituras y dobles lecturas, donde los servicios cambian gradualmente su tráfico de la instancia de base de datos monolítica a una nueva, tabla por tabla, como se muestra en la Figura 1, a continuación:

Figura 1: Las lecturas y escrituras dobles permiten pasar gradualmente de una base de datos a otra escribiendo todos los datos en ambas bases y ralentizando el desplazamiento de la original.

Para dar un resumen de alto nivel de cómo funciona, el enfoque consiste en escribir todas las filas nuevas de un conjunto específico de tablas tanto en la BD nueva como en la antigua, mientras que un trabajo de backfiller copia las filas antiguas. Una vez que hay suficiente confianza en la nueva BD, el tráfico de lectura se desplaza gradualmente a la nueva BD hasta que la antigua es finalmente desmantelada.

Aunque este enfoque funciona en la mayoría de los casos, en DoorDash encontramos algunas complejidades ocultas en forma de condiciones de carrera que pueden producirse en función de la especificidad de los patrones de acceso y los esquemas que se están migrando. De hecho, ninguna configuración puede propagarse instantáneamente a todos los componentes de un sistema distribuido, por lo que durante breves periodos de tiempo no había una única fuente de verdad para una fila o partición de filas determinada. En las tablas que exponen múltiples restricciones de unicidad, esto puede dar lugar a incoherencias o conflictos de datos que a menudo deben resolverse manualmente. 

Además, este enfoque requeriría o bien una capa común de acceso a los datos para gestionar la doble lectura/escritura, o bien que todos los propietarios de los servicios realizaran algún trabajo servicio por servicio, lo que resultaría costoso. Una capa de acceso a datos común suele estar presente en un monolito, pero dependiendo del orden en que se extraigan los datos, respectivo a la extracción de la lógica de la aplicación, esto podría no ser cierto. En un momento en el que la proliferación de pilas era un problema, ya que los nuevos microservicios nacían más rápido de lo que se creaban los estándares de la empresa, optamos por un enfoque diferente pero exitoso: el intercambio atómico de una única fuente de verdad.

Este tema merece por sí solo un artículo, que publicaremos en el futuro. Y va a tocar montones de aspectos técnicos interesantes de los sistemas de gestión de bases de datos en general y de Postgres en particular.

Garantizar la visibilidad y observabilidad del flujo de trabajo

Una de las ventajas de ejecutar una aplicación monolítica es que, en la mayoría de los casos, existe una capa de middleware común que intercepta todas las llamadas y puede imponer todo tipo de funcionalidades comunes. Esto es muy útil porque puede controlar todo, desde la autenticación hasta la autorización, así como las métricas y el registro.

Cuando se ejecuta un monolito, las métricas son predecibles, los cuadros de mando pueden reproducirse fácilmente y se necesita un conocimiento mínimo del dominio para obtener un conjunto común de indicadores relevantes para los flujos de trabajo que sean útiles para crear nuevas mediciones entre dominios. Por ejemplo, los indicadores de nivel de servicio (SLI) pueden identificarse para todos los flujos de trabajo, ya que todos exponen las mismas métricas, con la misma denominación y etiquetas, lo que permite una definición más coherente de los SLO por flujo de trabajo.

En el frenesí de la extracción de microservicios, es fácil acabar en una situación en la que cada equipo adopta una pila tecnológica y versiones de bibliotecas diferentes, creando sus propias convenciones en torno a las métricas. Esta situación da lugar a que los equipos desarrollen su propio conocimiento tribal separado.

Cuando esto sucede, no sólo se hace difícil para los no propietarios de dominios entender las métricas de otros dominios, sino que a menudo surgen situaciones en las que es realmente difícil saber qué servicio está involucrado en un flujo de trabajo determinado. Esta ambigüedad hace que sea muy difícil identificar dependencias fuertes superfluas (como las definidas anteriormente) hasta que se produce una interrupción total.

Para resolver este problema de tribalismo de dominios, es importante hacer el esfuerzo inicial de especificar un estándar de observabilidad, un conjunto de recomendaciones para toda la empresa que definan lo que debe medirse en cada servicio, así como la denominación y el etiquetado. Además de esa norma, adoptar soluciones transparentes para la trazabilidad distribuida (a la OTEL) más pronto que tarde ahorra muchos quebraderos de cabeza a la hora de responder a preguntas como: "¿Por qué el aumento del tiempo de respuesta p99 de un determinado servicio provocó una enorme caída del tráfico de un servicio aparentemente no relacionado?".

A medida que el esfuerzo de estandarización se hace más sustancial, y nacen nuevos marcos internos, también es esencial incluir todo el conocimiento anterior en estos marcos para que las futuras generaciones de arquitectura puedan beneficiarse de ellos y obtener una vez más esa capa centralizada de control para la observabilidad de los extremos.

Construir una capa de acceso a los datos

Una vez más en este artículo, parece que vamos a alabar al monolito por todos sus componentes centralizados que pueden ser retocados por unos pocos ingenieros expertos de manera que beneficien a todos en la empresa. En este caso, nos referimos a la capa de acceso a datos del monolito, en la que incluso el cambio más pequeño puede resultar en enormes beneficios para todo el equipo. La capa de acceso a datos es un componente que suele encontrarse en las aplicaciones monolíticas y que intercepta todas las consultas a almacenes de datos externos, pudiendo ejecutar cualquier código personalizado para observar, optimizar y redirigir dichas consultas.

Aunque es arriesgado tener un único clúster de bases de datos que contenga todos los datos de una empresa, en realidad es bueno tener una única base de código que gestione todo el acceso al almacenamiento. Esta capa de acceso centralizada puede utilizarse y ajustarse para obtener cosas como:

  • Observabilidad predecible de la consulta (descrita en la sección anterior)
  • Almacenamiento automático
  • Multiarrendamiento
  • Enrutamiento automático primario/de réplica con funciones de lectura y escritura propias
  • Optimización de consultas
  • Agrupación de conexiones
  • Control de los patrones de acceso subóptimos( ¿alguien quiereN+1?) 
  • Control de las migraciones de esquemas para tablas en línea (una simple palabra clave CONCURRENTLY puede marcar la diferencia entre una interrupción y una creación de índices sin problemas).

Para ser completamente justos, una de las ventajas de pasar a una arquitectura de microservicios es la posibilidad de experimentar con nuevas tecnologías de bases de datos que podrían encajar mejor que otras en un caso de uso específico. Pero, al fin y al cabo, existe la posibilidad de que la mayoría de los servicios de una organización de ingeniería utilicen tipos de bases de datos homogéneos. Y les vendría muy bien todo lo mencionado anteriormente.

Pasar de una capa de acceso a datos centralizada a un sistema distribuido es un problema que todavía está muy abierto en DoorDash, y también ampliamente debatido. Las posibles soluciones incluyen cosas como la creación de una herramienta de migración de esquemas centralizada, gestionada por el equipo de almacenamiento, que proporcione revisiones de código y pelusas que garanticen que las migraciones son seguras antes de que se ejecuten en producción. Además, los equipos de plataforma central y almacenamiento de DoorDash han invertido recientemente en una capa de acceso a datos centralizada en forma de pasarela de base de datos, que se despliega de forma aislada para cada clúster de base de datos y sustituye la interfaz SQL para microservicios por una API abstracta servida por una pasarela gRPC. Una pasarela de este tipo requiere muchas precauciones, como despliegues aislados, control de versiones y configuración, para garantizar que no se convierta en un punto único de fallo. La figura 4 muestra a grandes rasgos el aspecto de esta pasarela de datos.

Figura 2: La capa de acceso a los datos se convierte en el punto de contacto entre los servicios y su almacenamiento, ocultando complejidades como el almacenamiento en caché o el enrutamiento.

Garantizar la comprobabilidad

Los ingenieros experimentados sentirán esa sensación de déjà vu cada vez que vean un entorno de staging caer en el olvido a una velocidad que es directamente proporcional al número de servicios heterogéneos que lo pueblan. La degradación del entorno de staging es un proceso que corre el riesgo de acelerarse durante el frenesí de la migración de microservicios: es un momento en el que los nuevos servicios son extremadamente fluidos y cambian continuamente, especialmente en staging, que a menudo no tiene los mismos SLO que se esperan de un entorno de producción, lo que acaba por dejarlo casi inutilizable. Los servicios con muchas dependencias agravan esta degradación.

Para superar este problema, las pruebas deben evolucionar junto con la arquitectura. Junto con la introducción de nuevos marcos de pruebas para las pruebas de integración, la supervisión sintética y las pruebas de carga, DoorDash ha iniciado recientemente el proceso de implantación de las barreras necesarias en su entorno de producción para permitir la realización de pruebas seguras en producción. Estas barandillas se basan en el principio de permitir a nuestros desarrolladores experimentar con nuevas funciones y correcciones de errores en producción sin riesgo de contaminar el tráfico real o, peor aún, los datos.

Este tema está ampliamente cubierto en la industria, y entrar en los detalles de lo que DoorDash está construyendo para que esto suceda probablemente merece su propio artículo. Por ahora, aquí está una visión general de alto nivel de los principales componentes y barandillas que conforman nuestro entorno de pruebas de producción:

  • Proxies que redirigen el tráfico de prueba desde la periferia a entornos de desarrollo locales.
  • Definición y propagación estandarizada del tráfico de prueba en cada salto de una solicitud a través del equipaje OpenTelemetry (OTEL)
  • Aislamiento de datos en reposo mediante enrutamiento y filtrado de consultas en función de la tenencia del tráfico (aplicado por la capa de acceso a datos antes mencionada).
  • Aislamiento de la configuración mediante el espaciado de nombres de nuestros experimentos, la configuración en tiempo de ejecución y los secretos, en función de la tenencia de tráfico (aplicada mediante bibliotecas comunes).
  • Servicio de Actores de Pruebas que proporciona usuarios de pruebas a los desarrolladores
  • Consola de desarrollo para gestionar los entornos de prueba y crear nuevos escenarios

Un objetivo importante del proyecto de pruebas en producción de DoorDash es que, una vez generado el tráfico de prueba, todas las barreras en torno a los datos de prueba se apliquen a nivel de plataforma/infraestructura sin necesidad de que los microservicios tengan conocimiento alguno. Al asegurarnos de que nuestros servicios son agnósticos para el arrendatario, evitamos la proliferación de ramificaciones específicas del arrendatario en la base de código de nuestros servicios que inevitablemente tendríamos de otro modo.

Hacer frente a la proliferación de pilas tecnológicas

Al recordar los retos a los que se enfrentó DoorDash en la construcción de la arquitectura existente, a menudo nos venía a la mente una respuesta sencilla: basta con construir una biblioteca común.

En el mundo monolítico, en el que todo se ejecutaba en una única base de código basada en el framework Django, era realmente fácil crear nuevas bibliotecas para uso común, así como actualizar las existentes cada vez que se publicaba una nueva función o una mejora de la seguridad. La idea de tener un único archivo de requisitos que actualizar para que todos los equipos se beneficiaran de él era reconfortante.

A medida que avanzábamos hacia los microservicios, los desarrolladores empezaron a experimentar con los lenguajes y soluciones que consideraban más adecuados para el problema en cuestión. Al principio, los servicios nacían utilizando diversos lenguajes, concretamente Python3, Kotlin, Java y Go. Por un lado, este fue un momento realmente bueno para la empresa: al adquirir experiencia práctica con múltiples lenguajes pudimos estandarizarnos finalmente en unas pocas tecnologías, y comenzamos nuestro esfuerzo de estandarización interna basado en Kotlin. Por otro lado, sin embargo, se hizo realmente difícil compartir código y añadir nuevas funcionalidades a nivel de servicio. Acomodar todas nuestras pilas diferentes ahora requería escribir bibliotecas para múltiples lenguajes, así como depender de la cadencia de despliegue de cada servicio para que los servicios pudieran recoger cualquier nueva versión de biblioteca que fuera necesaria. 

Después de ese período inicial de experimentación, empezamos a construir frameworks y librerías soportados internamente para servicios greenfield, añadimos linting a todos nuestros repositorios para detectar dependencias que necesitan ser actualizadas, y comenzamos el esfuerzo de reducir el número de repositorios, manteniendo aproximadamente el mismo número de microservicios (algunas organizaciones están probando actualmente un monorepo por org). En su mayor parte, el equipo de la Plataforma Kotlin en DoorDash es responsable de liderar estos esfuerzos de estandarización, proporcionando a los desarrolladores las plantillas y marcos básicos que resuelven algunos de los problemas discutidos en las secciones anteriores de este artículo.

Definir el protocolo de despliegue

Hasta ahora nos hemos centrado en una serie de retos relacionados con la construcción de una arquitectura de microservicios que, en su mayoría, se basaban en el mismo principio: la base de código compartida de un monolito tiene algunas ventajas que los microservicios corren el riesgo de perder. Otro aspecto a considerar, sin embargo, es cómo las cosas que normalmente se perciben como una ventaja de alejarse de una arquitectura monolítica podrían en realidad ocultar algunos desafíos. Por ejemplo, la capacidad de desplegar libremente servicios independientes entre sí.

Con el monolito, los despliegues eran más predecibles: un único equipo de lanzamiento se encargaba de todos los despliegues, y una nueva versión de toda la aplicación se lanzaba al público con una cadencia regular. Sin embargo, la libertad de despliegue que ofrecen los microservicios puede dar lugar a la proliferación tanto de buenas como de malas prácticas, y estas últimas pueden causar distintos tipos de interrupciones de vez en cuando. Además, los tiempos de despliegue impredecibles pueden provocar retrasos en la respuesta de las llamadas a servicios relacionados, como las dependencias ascendentes.

Para mitigar estos problemas y establecer un protocolo de despliegue adecuado, el equipo de lanzamiento de DoorDash tuvo que dejar de ser el guardián del despliegue para crear herramientas de despliegue destinadas a hacer cumplir estas prácticas recomendadas, como avisar cada vez que se intenta realizar un despliegue en hora punta o proporcionar formas sencillas de deshacer el código con sólo pulsar un botón. Además, se han establecido interruptores de bloqueo globales para congelar todos los despliegues no aprobados en determinadas situaciones críticas, con el fin de evitar que desarrolladores inconscientes desplieguen código nuevo durante, por ejemplo, una interrupción del servicio. Por último, se han creado canalizaciones para implantar un registro de cambios global, que da visibilidad a todos y cada uno de los cambios que se producen en nuestro entorno de producción, desde el despliegue hasta los cambios de configuración en tiempo de ejecución. El registro global de cambios es un poderoso recurso en caso de interrupción, ya que permite a los ingenieros identificar la causa del problema y revertirlo rápidamente.

Lecciones aprendidas de la migración fuera del monolito

Después de discutir todos los puntos de dolor de dejar el monolito casi hace que uno se pregunte por qué lo hicimos en primer lugar. A pesar de todas las ventajas que un pequeño equipo de ingeniería podría obtener de trabajar en un monolito, las ventajas que vienen de pasar a microservicios son enormes y vale la pena resolver los puntos de dolor mencionados anteriormente. Obtenemos la capacidad de escalar y desplegar componentes individuales de forma independiente, reducir el radio de explosión de los despliegues erróneos, escalar como organización y experimentar con mayor rapidez.

Para beneficiarse de una arquitectura de microservicios, una organización debe abordar la extracción con cuidado. Después de todo, la pérdida de código comúnmente compartido puede dar lugar a comportamientos incoherentes entre los servicios para cosas como la visibilidad y el acceso a los datos, la libertad de despliegue puede ser perjudicial sin las barandillas adecuadas en su lugar, la proliferación de la pila tecnológica puede crecer fuera de control, la comprobabilidad puede ser más difícil, y los malos patrones en el establecimiento de dependencias de servicio pueden conducir a monolitos distribuidos.

En DoorDash nos enfrentamos a todos estos retos de una forma u otra, y aprendimos que invertir en estandarización, buenas prácticas, comprobabilidad y capas comunes de acceso/observabilidad de datos dará como resultado un ecosistema de microservicios más fiable y mantenible a largo plazo.

Me complace anunciar la oficina de ingeniería más reciente de DoorDash, ubicada en Los Ángeles, lo que demuestra nuestro compromiso con la segunda ciudad más grande de los EE. UU., ¡y uno de los lugares más diversos del mundo! Creemos que los aproximadamente 25.000 restaurantes de Los Ángeles representan oportunidades ilimitadas para DoorDash. El potencial de crecimiento en LA será igualado por el tamaño de la fuerza de trabajo de ingeniería de esta nueva oficina, que apoyará los aspectos críticos de la misión de nuestro negocio. Buscamos ingenieros cualificados de todos los niveles para unirse a nuestro equipo en Los Ángeles. 

Nuestro enfoque de misión crítica: DoorDash'Äôs infraestructura financiera 

Una parte importante de nuestra plataforma consiste en garantizar pagos fiables y seguros. Los clientes deben poder pagar sus entregas de la forma que prefieran, mientras que los comerciantes y los Dashers (nuestro término para los repartidores) reciben sus ganancias lo antes posible. Garantizar transacciones financieras eficientes, escalables, precisas y fiables es una parte fundamental de la plataforma DoorDash. La oficina de ingeniería de Los Ángeles estará en el centro de ese universo para el negocio mundial de DoorDash. Además, la oficina de Los Ángeles será responsable de la expansión del negocio de DoorDash a nuevos verticales y productos. Vamos a profundizar un poco más en los equipos que conformarán la oficina de LA DoorDash. 

Equipo de productos monetarios 

El equipo de Productos Monetarios tiene uno de los mayores impactos en nuestros clientes, ya que se centra en hacer que los pagos a través de la plataforma DoorDash sean lo más fácil posible. El trabajo de este equipo afecta prácticamente a todos los usuarios de DoorDash y ayuda a deleitar y retener a los clientes a través de una mayor comodidad. El equipo de Productos Monetarios también hace que sea más fácil para los Dashers y comerciantes asegurar los ingresos que reciben al utilizar la plataforma. El equipo de Productos Monetarios diseña e implementa programas nuevos e innovadores que tendrán un impacto inmediato y real en los usuarios de DoorDash al facilitar el acceso y el pago de los servicios. Por ejemplo, con nuestra tarjeta de débito prepago DasherDirect, los Dashers cobran a diario, obtienen un 2% de devolución en efectivo en todas las compras de gasolina y pueden gestionar fácilmente su dinero y sus recompensas a través de una aplicación de banca móvil. 

Equipo de la Plataforma Financiera 

Mientras que el equipo de Money Products se centra en los métodos de pago y la experiencia del usuario, el equipo de Money Platform construye y opera una plataforma de software para gestionar las transacciones diarias realizadas a través de DoorDash. Cada mes, millones de personas piden comida a DoorDash utilizando diferentes tarjetas de crédito, lo que se traduce en miles de millones de dólares en volumen bruto de pedidos. El equipo de la plataforma monetaria garantiza que cada transacción se ejecute de forma segura, eficiente y precisa, y que el dinero se transfiera a las cuentas correctas sin problemas. Este equipo central respalda el modelo de negocio de DoorDash en todo el mundo.  

Regalos 

Un aspecto emocionante de trabajar para una empresa de rápido crecimiento como DoorDash es la posibilidad de trabajar en proyectos nuevos e innovadores. Estamos muy contentos de estar construyendo este producto totalmente nuevo que reinventará el envío de regalos, haciendo el proceso más rápido, más barato y más conveniente. Una de las oportunidades de unirse a la oficina de Los Ángeles es poder crear algo totalmente nuevo que hará las delicias de nuestros clientes. 

Al unirse a la oficina de LA, no sólo se unirá a una empresa de hiper-crecimiento, sino también ayudar a establecer DoorDash'Äôs LA cultura de la oficina. Nuestro equipo está creciendo rápidamente y estamos buscando ingenieros con talento de diversos orígenes que tienen una pasión por el impacto.

Si te interesa la oportunidad de crear productos financieros y nuevas funciones para DoorDash, ¡únete a nosotros hoy mismo!

Consulte nuestros puestos vacantes:

Ingeniero de Software, Producto (todos los niveles)

Director de Ingeniería, Regalos 

Fotografía del encabezado por Cedric Letsch en Unsplash.

Mantener una experiencia de pedido en línea agradable implica garantizar que los grandes índices de búsqueda sigan siendo eficaces a escala. Para DoorDash, esto suponía un reto especial, ya que el número de tiendas, artículos y otros datos aumentaba cada día. Con esta carga, podía llevar hasta una semana reindexar todos los cambios y actualizar nuestra base de datos de búsqueda. 

Necesitábamos una forma rápida de indexar todos los datos de búsqueda de nuestra plataforma para mejorar el descubrimiento de productos y garantizar que ofrecíamos a los consumidores todas las opciones de pedido disponibles. Además, este proyecto también aumentaría la velocidad de experimentación en nuestra plataforma para que pudiéramos mejorar el rendimiento de nuestras búsquedas con mayor rapidez. 

Nuestra solución consistió en crear una nueva plataforma de indexación de búsquedas que utiliza la indexación incremental en nuestras fuentes de datos. Basamos esta plataforma en tres proyectos de código abierto: Apache Kafka, Apache Flink y Elasticsearch.

El problema de DoorDash con la indexación de búsquedas 

Nuestro sistema de indexación heredado no era fiable ni ampliable, y además era lento. Un sistema de indexación fiable garantizaría que los cambios en los almacenes y los artículos se reflejaran en el índice de búsqueda en tiempo real. La indexación incremental ayuda a actualizar los datos más rápidamente, construyendo índices frescos para introducir nuevos analizadores y campos adicionales en menos tiempo, lo que en última instancia ayuda a mejorar la recuperación.

Los equipos de las nuevas verticales de negocio dentro de DoorDash querían crear su propia experiencia de búsqueda, pero no querían reinventar la rueda a la hora de indexar los datos de búsqueda. Por lo tanto, necesitábamos una solución plug-and-play para mejorar las nuevas experiencias de búsqueda sin ralentizar el desarrollo de estos equipos verticales de negocio. 

Creación de un proceso de indexación de documentos basado en eventos 

Hemos resuelto estos problemas creando una nueva plataforma de indexación de búsquedas que proporciona una indexación rápida y fiable para diferentes sectores verticales, al tiempo que mejora el rendimiento de las búsquedas y la productividad de los equipos de búsqueda. Utiliza Kafka como cola de mensajes y para el almacenamiento de datos, y Flink para la transformación de datos y su envío a Elasticsearch.

Arquitectura de alto nivel

Diagrama del proceso de indexación de datos
Figura 1: El canal de datos de nuestro nuevo sistema de índices de búsqueda utiliza Kafka para la cola de mensajes y el almacenamiento de datos, y Flink para ETL y la sincronización con Elasticsearch.

La figura 1 muestra varios componentes de nuestra cadena de índices de búsqueda. Los componentes se agrupan en cuatro categorías:

  • Fuentes de datos: Son los sistemas que poseen operaciones CRUD sobre los datos. Los llamamos la fuente de verdad de los datos. En nuestra pila utilizamos Postgres como base de datos y Snowflake como almacén de datos. 
  • Destino de los datos: Es el almacén de datos que se ha optimizado para la búsqueda. En nuestro caso elegimos Elasticsearch.
  • Aplicación Flink: Añadimos dos aplicaciones Flink personalizadas en nuestra canalización de indexación, Assemblers para transformar los datos y Sinks para enviar los datos al almacenamiento de destino. Los ensambladores se encargan de ensamblar todos los datos necesarios en un documento de Elasticsearch. Los sumideros son responsables de dar forma a los documentos según el esquema y escribir los datos en el clúster Elasticsearch de destino.
  • Cola de mensajes: Utilizamos Kafka como tecnología de cola de mensajes. El componente Kafka 2, de la Figura 1, más arriba, utiliza los temas de registro compactados y conservados indefinidamente.

Juntos, estos componentes conforman un canal de datos de extremo a extremo. Los cambios en las fuentes de datos se propagan a las aplicaciones de Flink mediante Kafka. Las aplicaciones Flink implementan la lógica de negocio para curar los documentos de búsqueda y escribirlos en el destino. Ahora que entendemos los componentes de alto nivel, vamos a repasar los diferentes casos de uso de la indexación.

Manténgase informado con las actualizaciones semanales

Suscríbase a nuestro blog de ingeniería para recibir actualizaciones periódicas sobre los proyectos más interesantes en los que trabaja nuestro equipo.

Indexación incremental

El proceso de indexación procesa los cambios incrementales de datos procedentes de dos fuentes distintas. La primera captura los cambios de datos a medida que se producen en tiempo real. Normalmente, estos eventos se generan cuando operadores humanos realizan cambios ad hoc en almacenes o artículos. La segunda son los cambios de datos ETL. Nuestros modelos de aprendizaje automático generan datos ETL en un almacén de datos. El proceso de indexación gestiona los eventos de estas dos fuentes de datos de forma diferente.

Indexación de eventos de captura de datos de cambios (CDC) 

Los datos de DoorDash sobre los comerciantes se crean y actualizan continuamente, y deben ser tratados por nuestra solución de canalización de índices. Por ejemplo, estas actualizaciones pueden ser cualquier cosa, desde operadores comerciales que añaden etiquetas a una tienda hasta la actualización de menús. Tenemos que reflejar estos cambios en la experiencia del consumidor lo más rápidamente posible o los consumidores verán datos obsoletos en la aplicación. Estas actualizaciones de la plataforma se guardan en almacenes de datos como Postgres y Apache Cassandra. Los flujos de trabajo iterativos también crujen los datos en el almacén de datos con cadencia diaria, impulsando cosas como las aplicaciones de inteligencia empresarial.

Para capturar de forma fiable estos eventos de actualización de la base de datos de un servicio, exploramos la posibilidad de habilitar la captura de datos de cambio (CDC) para Aurora/Postgres utilizando el conector Debezium, un proyecto de código abierto desarrollado por Red Hat para capturar cambios a nivel de fila. Las pruebas de rendimiento iniciales llevadas a cabo por el equipo de almacenamiento sugirieron que esta estrategia tenía demasiada sobrecarga y no era eficaz, especialmente cuando el servicio utiliza la misma base de datos para servir el tráfico en línea. Por lo tanto, implementamos save hooks en la aplicación, que se encargan de gestionar las solicitudes de actualización de datos, para propagar los eventos de cambio a través de Kafka cada vez que se produce un cambio en el almacén de datos subyacente. Llamamos a este enfoque CDC a nivel de aplicación.

Con el CDC a nivel de aplicación, podríamos encontrarnos con problemas de coherencia. Una aplicación distribuida tiene múltiples instancias. Dos llamadas de actualización separadas pueden ser servidas a través de dos instancias diferentes. Si incluimos valores actualizados en los mensajes de Kafka, no se garantizaría la coherencia ni se resolvería el problema, ya que en algunos casos varias instancias de la aplicación enviarán eventos que actualicen el mismo valor. 

Por ejemplo, si la instancia de aplicación nº 1 envía un evento, {store_id: 10, is_active=true}y la instancia de aplicación nº 2 envía un evento, {store_id: 10, is_active=false}...habría conflictos en el lado del consumidor.

Para garantizar la coherencia, solo enviamos los ID de entidad modificados en los eventos de Kafka. Al recibir los eventos de Kafka, nuestra aplicación Assembler llama a las API de REST de la aplicación para recopilar otra información sobre las entidades que están presentes en los eventos de Kafka. Las llamadas a las API REST garantizan la coherencia de los datos sobre la entidad. Assembler combina la información para crear un evento que envía a Kafka para que la aplicación Sink lo consuma. El ensamblador implementa una deduplicación por ventanas, que impide llamar a las API REST para la misma entidad varias veces en un periodo de tiempo determinado. El ensamblador también agrega eventos para llamar a puntos finales REST de forma masiva. Por ejemplo, durante un periodo de 10 segundos, agrega actualizaciones de artículos para una tienda. Llama a las API de REST para ese almacén incluyendo todos los artículos deducidos y agregados.

En resumen, utilizamos el CDC a nivel de aplicación para capturar los eventos de cambio de datos. Resolvemos los problemas de coherencia con eventos simplificados y API REST. Utilizamos funciones de deduplicación y ventana para optimizar el procesamiento de eventos. 

Indexación de datos ETL

Muchas propiedades de los documentos de la tienda y de los artículos que son fundamentales para nuestro proceso de recuperación, como las puntuaciones y las etiquetas generadas por los modelos ML, se actualizan en bloque una vez al día. Estos datos son generados por modelos, como cuando un modelo ML ejecuta los datos más recientes, o manualmente, como cuando nuestros operadores humanos etiquetan manualmente los artículos con "pollo" para una tienda en particular. Estos datos se introducen en tablas de nuestro almacén de datos tras una ejecución nocturna de los respectivos trabajos ETL. 

Antes de nuestra nueva plataforma de índice de búsqueda, no disponíamos de una forma fiable de cargar datos en nuestro índice, sino que utilizábamos soluciones lentas e imprecisas. Queríamos mejorar nuestro proceso actual dotando a nuestra nueva plataforma de índices de búsqueda del mecanismo necesario para introducir datos ETL en nuestro índice de forma fiable en un plazo de 24 horas. 

Los patrones de CDC para el caso de uso de ETL son muy diferentes del caso de actualización incremental descrito en la sección anterior. En el caso de la actualización incremental, los almacenes de datos comerciales se actualizan constantemente, lo que da lugar a un flujo continuo de actualizaciones a lo largo del día. En cambio, en el caso de uso de ETL, las actualizaciones se producen de una sola vez cuando se ejecuta la ETL, sin que se produzcan otras actualizaciones hasta la siguiente ejecución.

Decidimos no utilizar una variante del CDC a nivel de aplicación para las fuentes ETL porque veríamos grandes picos de actualizaciones cada vez que se ejecutara el ETL, y este pico podría sobrecargar nuestros sistemas y degradar el rendimiento. En su lugar, queríamos un mecanismo que repartiera la ingesta de ETL a lo largo de un intervalo para no saturar los sistemas.

Como solución, desarrollamos una función de origen de Flink personalizada que transmite periódicamente todas las filas de una tabla ETL a Kafka en lotes, cuyo tamaño se elige para garantizar que los sistemas posteriores no se vean desbordados. 

Envío de documentos a Elasticsearch

Una vez que las aplicaciones de ensamblaje publican los datos en los temas de destino, tenemos un consumidor que lee los mensajes hidratados, los transforma de acuerdo con el esquema de índice específico y los envía a su índice correspondiente. Este proceso requiere la gestión del esquema, el índice y el clúster. Mantenemos un único grupo de consumidores Kafka por índice ElasticSearch para que los consumidores puedan mantener desplazamientos para cada índice. Para transformar los mensajes, utilizamos un DocumentProcessor(s), que recibe un evento hidratado del tema de destino y emite documentos formateados que están listos para ser indexados. 

El proceso Sink utiliza Flink Elasticsearch Connector para escribir documentos JSON en Elasticsearch. Tiene capacidades de limitación de velocidad y estrangulamiento, esenciales para proteger los clústeres de Elasticsearch cuando el sistema está sometido a una gran carga de escritura. El proceso también admite la indexación masiva, en la que reunimos todos los documentos y las operaciones relevantes durante una ventana de tiempo y realizamos solicitudes masivas. Cualquier fallo a la hora de indexar un documento hace que éste se registre y se almacene en una cola de letra muerta que puede procesarse más tarde.

Rellenar rápidamente un nuevo índice

A menudo, es posible que queramos añadir una nueva propiedad a nuestro índice, como añadir el ID de mercado asociado a una tienda o artículo al documento porque nos ayuda en la fragmentación. Del mismo modo, puede que necesitemos recrear rápidamente un nuevo índice, como cuando queremos probar diferentes estructuras de índice para ejecutar benchmarks de eficiencia. 

En el sistema heredado dependíamos de un trabajo lento y poco fiable que normalmente tardaba un mes en reindexar todos los documentos de la tienda y los artículos. Dada la larga duración de la indexación, era difícil calcular correctamente la tasa de error asociada al proceso de reindexación. Por tanto, nunca estábamos seguros de la calidad de la indexación. A menudo recibíamos quejas sobre desajustes en los detalles de la tienda entre el índice y la fuente de verdad, que había que corregir manualmente.

Con nuestra nueva plataforma de índice de búsqueda, queríamos un proceso para recrear rápidamente un nuevo índice o rellenar una propiedad en un índice existente en 24 horas. Para el proceso de bootstrapping, necesitábamos un mecanismo para recrear rápidamente todos los documentos que debían indexarse en Elasticsearch. Este proceso implica dos pasos: 

  1. Transmisión de todos los ID de entidad correspondientes a los documentos que deben indexarse en ElasticSearch 
  2. Asignación de los ID de entidad a su forma final mediante llamadas externas antes de que se envíen para su indexación. 

El proceso para asignar el ID de entidad a la forma final de la entidad ya se había establecido como parte de nuestro trabajo en el ensamblador en línea, mencionado anteriormente. Por lo tanto, todo lo que se necesitaba era transmitir todos los ID de documentos que necesitaban ser indexados en Elasticsearch. En consecuencia, mantenemos una copia actualizada de todos los ID de entidad que necesitan ser indexados en tablas bootstrap en nuestro almacén de datos. Cuando necesitamos realizar un bootstrap, utilizamos la función de origen descrita en la sección ETL para transmitir todas las filas de estas tablas bootstrap a Kafka. Encapsulamos la lógica para realizar los dos pasos anteriores en un único trabajo.

Si ejecutamos nuestro proceso de indexación incremental al mismo tiempo que nuestro proceso de arranque, corremos el riesgo de obtener datos obsoletos en Elasticsearch. Para evitar estos problemas, reducimos nuestro indexador incremental cada vez que se ejecuta el bootstrap, y lo volvemos a aumentar una vez que el bootstrap se ha completado.

Si lo ponemos todo junto, los pasos que damos para rellenar y recrear el índice son los siguientes:

  • Cree el índice y actualice sus propiedades según sea necesario, y actualice la lógica empresarial y las configuraciones en el ensamblador y el sumidero para rellenar la nueva propiedad.
  • Reducir el ensamblador en línea. 
  • Amplía el trabajo de arranque.
  • Una vez completado el bootstrap, reduzca el trabajo de bootstrap y aumente el ensamblador en línea. Cuando el desplazamiento sea reciente, el proceso de arranque habrá finalizado.

Activación de una función de reindexación forzada 

De vez en cuando, algunos de nuestros documentos en Elasticsearch pueden tener datos obsoletos, posiblemente porque algunos eventos de aguas arriba no se entregaron, o uno de nuestros servicios de aguas abajo tardó demasiado en responder. En estos casos, podemos forzar una reindexación de los documentos en cuestión. 

Para realizar esta tarea, enviamos un mensaje con el ID de la entidad a indexar al tema del que consume datos el ensamblador en línea. Una vez consumido el mensaje, se pone en marcha nuestro canal de indexación descrito anteriormente y cada documento se vuelve a indexar en Elasticsearch.

Anotamos los mensajes que se envían en nuestras tareas puntuales de indexación con etiquetas únicas que nos proporcionan una traza detallada del documento a medida que pasa por las distintas etapas del flujo de indexación. Además de garantizarnos que el documento ha sido indexado, nos proporciona una gran cantidad de información de depuración que nos ayuda a validar y descubrir cualquier error que pudiera haber impedido su indexación en primer lugar.

Resultados

Nuestra nueva plataforma de indexación de búsquedas es más fiable. La velocidad de indexación incremental ayuda a que los datos se actualicen más rápido y aparezcan con mayor prontitud en nuestras aplicaciones de consumo. Una reindexación más rápida permite crear nuevos índices en poco tiempo para mejorar la recuperación: 

  • Reducción del tiempo de relleno de todo nuestro catálogo de tiendas de una semana a 6,5 horas.
  • Reducción del tiempo de relleno de todo nuestro catálogo de artículos de dos semanas a 6,5 horas.
  • Reducción del tiempo de reindexación de las tiendas y artículos existentes en la plataforma de una semana a 2 horas.

Conclusión

Los datos viven en el corazón de cualquier organización. Mover los datos sin problemas y remodelarlos para diferentes casos de uso es una operación esencial en nuestra arquitectura de microservicios. Esta nueva plataforma de índice de búsqueda permite a otros equipos de DoorDash diseñar experiencias de búsqueda para líneas de negocio específicas sin tener que construir una arquitectura de índice de búsqueda completamente nueva. Nuestra confianza en las herramientas de código abierto para este índice de búsqueda significa una gran cantidad de documentación accesible en línea e ingenieros con esta experiencia que podrían unirse a nuestro equipo. 

En general, este tipo de solución se aplica a cualquier empresa con un catálogo en línea grande y en crecimiento que se centre en realizar cambios en su experiencia de búsqueda. Al adoptar un enfoque similar al descrito anteriormente, los equipos pueden reducir el tiempo de reindexación y permitir iteraciones más rápidas y menos intervenciones manuales, al tiempo que mejoran la precisión de su índice. Nuestro enfoque es especialmente beneficioso para las empresas que tienen un catálogo en rápido crecimiento y múltiples operadores manuales que realizan cambios que deben reflejarse en el índice.  

Fotografía del encabezado de Jan Antonin Kolar en Unsplash.

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.
Diagrama de los procesos modulares
Figura 1: En nuestro nuevo canal, hemos modularizado los procesos para aumentar su escalabilidad. La recuperación de candidatos recopila tiendas y restaurantes de los proveedores, y luego los pasa a otros módulos, como Ranking y el procesador Layout, para prepararlos para su visualización en la página de exploración.

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.

Diagrama del sistema, observabilidad
Figura 2: La capacidad de observación incorporada a nuestro sistema no sólo nos ayuda a comprender el comportamiento de los consumidores, sino que también logra la supervisión tradicional del sistema para evitar cortes.

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. 

Imagen de cabecera por Peter H de Pixabay.

En DoorDash, queremos que nuestro servicio sea una comodidad diaria que ofrezca entregas puntuales y precios coherentes. Lograr estos objetivos requiere un buen equilibrio entre la oferta de Dashers (nuestro término para los conductores de reparto) y la demanda de pedidos. 

En periodos de gran demanda, solemos aumentar la remuneración, lo que supone un incentivo para garantizar que haya suficientes Dashers disponibles para que los consumidores reciban sus pedidos lo antes posible. No repercutimos este aumento a los consumidores, que pagarán las mismas tarifas sea cual sea la hora del día. 

Dada la complejidad de ofrecer a Dashers una retribución por picos de demanda, creamos un nuevo sistema de movilización que asigna los incentivos antes de que se produzca cualquier desequilibrio previsto entre la oferta y la demanda. Al construir este sistema, nos centramos en lo siguiente:

  • Definir claramente nuestros parámetros de medición de la oferta y la demanda y los objetivos del proyecto
  • Previsiones fiables de la oferta y la demanda
  • Establecimiento de un nuevo proceso de optimización para la asignación de incentivos con restricciones
  • Gestión de la incertidumbre 
  • Mejora de la fiabilidad y el mantenimiento del sistema

¿Cómo cuantificar el desequilibrio entre la oferta y la demanda?

A la hora de esbozar el problema del desequilibrio entre la oferta y la demanda, conviene adoptar el contexto de todas las partes afectadas:

  • Para los consumidores, es más probable que la falta de disponibilidad de Dasher durante los picos de demanda provoque retrasos en los pedidos, plazos de entrega más largos o la imposibilidad de solicitar una entrega y tener que optar por la recogida. 
  • Para los Dashers, la falta de pedidos conlleva menores ingresos y turnos más largos y frecuentes para alcanzar sus objetivos personales.
  • Para los comerciantes, un suministro insuficiente de Dashers provoca retrasos en las entregas, lo que suele traducirse en alimentos fríos y un menor índice de repetición de pedidos.

En este contexto, queda claro que lo ideal sería contar con un sistema que equilibrara la oferta y la demanda a nivel de entrega en lugar de a nivel de mercado, pero esto no es realista a la hora de elegir métricas de medición del mercado. Equilibrar a nivel de entrega significa que cada pedido tiene un Dasher disponible en el momento más óptimo y que cada Dasher alcanza su objetivo de pago por hora.

Manténgase informado con las actualizaciones semanales

Suscríbase a nuestro blog de ingeniería para recibir actualizaciones periódicas sobre los proyectos más interesantes en los que trabaja nuestro equipo.

Por el contrario, el equilibrio a nivel de mercado significa que hay un número relativamente igual de Dasher y de pedidos en un mercado, pero que no existen necesariamente condiciones óptimas para cada uno de estos grupos a nivel de entrega. En la práctica, el nivel de variación de la oferta y la demanda impulsado por las preferencias de Dasher y consumidores y otras condiciones cambiantes del entorno, como el tráfico y el clima, dificultan el equilibrio de la oferta y la demanda a nivel de entrega. De ahí que nos hayamos centrado en las métricas a nivel de mercado para definir el estado de cada mercado, aunque una métrica a nivel de entrega habría proporcionado un resultado más ideal. 

Para nuestra principal medida de la oferta y la demanda, nos fijamos en el número de horas necesarias para realizar las entregas manteniendo una duración de las mismas baja y una alta ocupación del Dasher. Al centrarnos en las horas, podemos tener en cuenta las variaciones regionales debidas a las condiciones del tráfico, los índices de dosificación y los tiempos de preparación de los alimentos. 

Para entender cómo funcionaría esta métrica en la práctica, veamos un ejemplo. Imaginemos que es domingo a la hora de cenar en Nueva York y estimamos que se necesitan 1.000 horas Dasher para satisfacer la demanda prevista. También podríamos estimar que, a menos que ofrezcamos incentivos adicionales, es probable que los Dasher sólo aporten 800 horas de forma orgánica. Sin acciones de movilización estaríamos desabastecidos en unas 200 horas.

Generalmente calculamos esta métrica cuando los Dashers se registran en Dash y en unidades de tiempo que pueden abarcar desde duraciones de una hora hasta unidades de un día como el almuerzo y la cena. Es muy importante no seleccionar un nivel de agregación que pueda conducir a un alisamiento artificial de la oferta y la demanda. Por ejemplo, en un mismo día puede haber un exceso de oferta en el desayuno y un déficit en la cena. Optimizar para un día completo llevaría a suavizar cualquier desequilibrio y generaría acciones de movilización incorrectas.

Una vez que decidimos la métrica sanitaria y la unidad en la que actuamos, procedemos a equilibrar la oferta y la demanda mediante ajustes de la oferta. Por lo general, nuestro equipo ajusta la oferta ofreciendo incentivos para aumentar la movilización de Dasher cuando hay más demanda. Mediante incentivos, ofrecemos a los Dasher la garantía de que ganarán una cantidad fija de dinero por cualquier entrega que acepten en una unidad de tiempo-región específica. En la sección siguiente describiremos el papel que desempeñan en ello la previsión y la optimización.

¿Cómo prever la oferta y la demanda a escala local?

Ahora que tenemos una métrica para medir los niveles de oferta y demanda, una unidad de región/tiempo para tomar medidas y las medidas que tomamos para gestionar la oferta, podemos determinar los detalles de nuestros requisitos de previsión y cómo pronosticamos las condiciones de oferta y demanda de cada mercado.

Definir las necesidades de previsión

Dado que las previsiones que generamos están destinadas a ser utilizadas en un sistema automatizado, tanto el algoritmo que utilizamos para la previsión como el ecosistema de bibliotecas en el que nos basaríamos posteriormente pueden tener un gran impacto en el mantenimiento de la automatización a largo plazo. En primer lugar, reformulamos el problema de previsión para convertirlo en un problema de regresión y utilizamos el gradient boosting a través del marco de código abierto LightGBM desarrollado por Microsoft. Esta elección obedece a un par de razones.

Previsiones multivariantes

Muchos enfoques de previsión univariante no se adaptan bien cuando se trata de generar miles de previsiones regionales con una granularidad de bajo nivel. Nuestra experiencia apoya firmemente la tesis de que algunos de los mejores modelos se crean a través de un proceso de creación rápida de prototipos, por lo que buscamos enfoques en los que pasar de plantear una hipótesis de mejora del modelo a tener el resultado final pueda hacerse rápidamente. LightGBM puede utilizarse para entrenar y generar miles de previsiones regionales en una sola ejecución de entrenamiento, lo que nos permite iterar muy rápidamente en el desarrollo del modelo. 

Apoyo a la extrapolación

A medida que DoorDash se expande tanto a nivel nacional como internacional, necesitamos que nuestro sistema de previsión sea capaz de generar algunas expectativas sobre cómo sería el crecimiento de nuestra oferta y demanda en lugares donde actualmente no ofrecemos nuestros servicios. Por ejemplo, si nos lanzamos en una ciudad nueva, podemos hacer proyecciones razonables sobre la trayectoria de la oferta y la demanda incluso sin datos históricos. El aprendizaje profundo y los enfoques tradicionales basados en el aprendizaje automático (ML) funcionan especialmente bien en este caso, ya que la información latente que ayuda a la extrapolación puede aprenderse a través de vectores incrustados o mediante una buena ingeniería de características. La información sobre el tamaño de la población, las condiciones generales del tráfico, el número de comerciantes disponibles, el clima y la geografía puede utilizarse para realizar extrapolaciones.

Apoyo a los contrafácticos

Las previsiones sirven para establecer una expectativa de lo que ocurrirá, pero también se utilizan inevitablemente para orientar el proceso de toma de decisiones. Por ejemplo, nuestros interlocutores nos preguntarían cómo cambiarían las condiciones si modificáramos los niveles de incentivos en nuestro modelo de previsión de la oferta, para que podamos entender cómo hacer concesiones entre oferta y costes. Este tipo de contrafactuales son muy útiles no sólo para prever lo que creemos que ocurrirá, sino también para estimar el impacto de las medidas que vamos a tomar. En LightGBM, los contrafactuales aproximados pueden generarse cambiando las entradas que entran en el modelo en el momento de la inferencia. 

Pequeña huella de dependencia

Queríamos que el sistema de previsión tuviera una huella de dependencia mínima, es decir, que no dependiéramos excesivamente de una gran cantidad de bibliotecas de terceros. Este requisito eliminó de inmediato muchos de los enfoques de previsión automática, en los que la instalación de una biblioteca a menudo implicaba la instalación de más de 100 bibliotecas adicionales, o enfoques que proporcionaban conjuntos de herramientas unificados y tenían un gran número de dependencias transitivas. Una huella hinchada crea problemas de compatibilidad, desafíos de actualización y una gran área de exposición a vulnerabilidades de seguridad. LightGBM tiene una huella de dependencia muy pequeña, y es relativamente fácil realizar actualizaciones. 

Una comunidad próspera

Por último, queríamos confiar en un ecosistema con una comunidad próspera y un grupo de mantenedores sólido. Mantener una biblioteca de código abierto es todo un reto. Una biblioteca puede ser creada por un estudiante de posgrado o por uno o tres desarrolladores que trabajen en una empresa. Sin embargo, la gente encuentra nuevos intereses, nuevos trabajos, cambia de trabajo, encuentra nuevas carreras o las abandona. El seguimiento de los problemas y errores relacionados con una biblioteca no suele ser una prioridad al cabo de unos años o meses. Esta eventual falta de soporte obliga entonces a los usuarios a crear bifurcaciones internas para adoptar la previsión de herramientas para sus casos de uso o a emprender un ejercicio de remodelación completa. Por estas razones, a la hora de seleccionar una herramienta, nos fijamos en parámetros como los ciclos de publicación, el número de estrellas y la implicación de la comunidad para asegurarnos de que la comunidad mantendría un buen nivel de mantenimiento en el futuro. 

Previsiones con ML

La previsión en el contexto de un problema de regresión pura puede plantear dificultades, una de las cuales tiene que ver con la comprensión del proceso de generación de datos y la causalidad entre las entradas y las salidas. Por ejemplo, la figura 1, a continuación, muestra cómo se relacionan nuestros incentivos con el crecimiento del número de horas Dasher.

Figura 1: Estos datos sugieren que existe una relación no lineal entre los incentivos y la movilización, ya que los incentivos más elevados conllevan un aumento de las horas Dasher y los incentivos muy elevados están relacionados con un descenso drástico de las horas. Esta relación contraintuitiva puede ser a menudo señal de un sesgo de variable omitida o de causalidad simultánea.

Si confiamos ciegamente en el modelo para aprender la causalidad a través de las correlaciones encontradas en los datos, habríamos creado un sistema que asumiría erróneamente que proporcionar niveles de incentivos muy altos llevaría a menos Dashers en la carretera. Una interpretación causal, en la que incentivos de alto crecimiento llevarían a una disminución de la movilización, no tendría sentido. 

Es más probable que al modelo le falte una variable de confusión. Por ejemplo, en periodos de mal tiempo o vacaciones, los dashers prefieren pasar tiempo en casa o con la familia. Es más probable que en esas épocas disminuya la disponibilidad, lo que provocaría que nuestros sistemas de oferta y demanda ofrecieran mayores incentivos para mantener el equilibrio del mercado. 

Un modelo que carezca de conocimientos sobre el tiempo o las vacaciones podría aprender que los incentivos elevados conducen a menos horas Dasher, cuando la relación causal simplemente carece de un vínculo covariable. Este ejemplo ilustra por qué es importante encontrar una manera de restringir a veces las relaciones encontradas en los datos a través del conocimiento del dominio, o confiar en los resultados experimentales para regularizar algunas relaciones correlacionales identificadas por el modelo y no aplicar ciegamente el algoritmo a los datos disponibles.  

Un segundo reto tiene que ver con un tópico común en previsión: la unidad de previsión tiene que coincidir con el contexto en el que se toman las decisiones. Puede resultar tentador hacer previsiones aún más detalladas, pero en general es una mala idea. Esto puede demostrarse fácilmente mediante una simulación.

Consideremos las tres subregiones siguientes, que describen la demanda diaria extrayendo muestras, como se muestra en la figura 2, de una distribución normal con una media de 100 y una desviación típica de 25, lo que nos da un coeficiente de variación del 25%. Cuando agregamos estas regiones, simplemente sumamos la media esperada para obtener una demanda agregada esperada de 300. No obstante, la desviación típica combinada no es igual a la suma de las desviaciones típicas, sino a la suma de las varianzas lo que nos da un coeficiente de variación de la previsión combinada del 14,4%. Simplemente agregando variables aleatorias, conseguimos reducir la varianza con respecto a la media en más de un 40%. 

Figura 2: El panel superior muestra la demanda de tres subregiones. El panel inferior representa simplemente la demanda agregada. La varianza relativa del panel inferior se reduce en un 40% mediante la agregación de variables aleatorias.

Aunque la agregación de datos puede ayudar a obtener previsiones globales más precisas, las acciones realizadas sobre datos agregados pueden conducir a una movilización ineficaz. Lo mejor es optar por una solución en la que la unidad de previsión coincida con la unidad de decisión.  

Elegir un optimizador

Una de las ventajas de utilizar algoritmos de ML es que proporcionan expectativas más precisas de lo que ocurrirá dados los datos de entrada. No obstante, los algoritmos de ML suelen ser simplemente un componente básico de un sistema más amplio que consume predicciones e intenta generar un conjunto de acciones óptimas. Las soluciones basadas en programación mixta entera (MIP) o en aprendizaje por refuerzo (RL) son excelentes para construir sistemas centrados en la maximización de la recompensa bajo restricciones empresariales específicas. 

Decidimos adoptar un planteamiento PIM porque era fácil de formalizar, aplicar y explicar a las partes interesadas, y porque tenemos mucha experiencia en este campo. El optimizador tiene una función objetivo personalizada que consiste en minimizar el desabastecimiento con varias restricciones. El objetivo en sí es muy flexible y puede especificarse para favorecer la rentabilidad o el crecimiento, en función de las necesidades de la empresa. En el optimizador, generalmente codificamos unas pocas restricciones globales:

  • Nunca asigne más de un incentivo en una unidad de tiempo-región concreta.
  • No superar nunca el presupuesto máximo permitido fijado por nuestros socios financieros y operativos.

En función de las necesidades, también podríamos tener distintas limitaciones regionales o nacionales, como disponer de presupuestos diferentes, penalizaciones personalizadas, criterios de exclusión para las unidades que no deben incluirse en la optimización o limitaciones de incentivos que se guíen por la variabilidad de los insumos. 

Afrontar la incertidumbre

La incertidumbre en los insumos desempeña un papel importante en la forma en que el optimizador asigna los incentivos cuando los recursos son limitados. Para demostrarlo, la Figura 3 muestra la distribución del desequilibrio hipotético entre la oferta y la demanda en dos ciudades.

Figura 3: En la ciudad A, creemos que habrá un exceso de oferta y, en general, confiamos en nuestra predicción. En la ciudad B, nuestra predicción media es que habrá un ligero exceso de oferta, pero estamos menos seguros de nuestra estimación y existe cierta probabilidad de que se produzca una gran escasez de oferta. 

Si el optimizador no sabe nada sobre la incertidumbre, se encontrará con dos problemas. En primer lugar, no comprenderá que el equilibrio entre la oferta y la demanda puede ir desde un espectro de extrema suboferta a un extremo exceso de oferta. Si el optimizador está configurado para optimizar bajo unos umbrales preestablecidos, perdería la oportunidad de optimizar para la ciudad B porque la media de la distribución está por encima de cero. 

El segundo problema es que acabaría sobreasignando recursos en lugares donde nuestras previsiones son inciertas. Nuestras previsiones suelen tener más ruido en la cola larga de las regiones pequeñas que tienen pocos Dashers y pocos pedidos. Dado que el recuento de estas regiones es elevado y presentan una gran varianza, si no tenemos en cuenta explícitamente esta incertidumbre es más probable que generemos estimaciones que, por casualidad, presenten una gran escasez de oferta y, por tanto, sobreasignemos incentivos a los lugares que presentan una gran varianza en relación con los lugares que tienen una baja varianza. 

Para abordar la cuestión de la varianza, generamos estimaciones esperadas del déficit de horas a partir de las previsiones utilizando un proceso de remuestreo. Al realizar el remuestreo, medimos esencialmente el impacto de la falta de oferta en el contexto de la probabilidad de que ocurra. Por ejemplo, en el gráfico 3, la ciudad B sólo tiene un 34% de probabilidades de sufrir un desabastecimiento. Sin embargo, si eso ocurre, podemos estimar con mayor precisión el impacto de cambios considerables en la escasez de oferta. Cualquiera de estos enfoques conduce a una decisión más óptima en la asignación de incentivos en lugar de limitarse a utilizar estimaciones medias a partir de datos de previsión previos.  

Mejoras de fiabilidad y mantenimiento

DoorDash ha crecido enormemente en el último año. Más del 70 % de las personas de DoorDash se incorporaron en el periodo 2020-21. Esto generalmente trajo una ola de nuevos proyectos relacionados con la ingeniería, el producto, la plataforma y la infraestructura para ayudar con el crecimiento continuo, la expansión y la escalabilidad. Por ejemplo, tuvimos docenas de proyectos internos relacionados con la descomposición de nuestro monolito y la adopción de una arquitectura más orientada a microservicios. Tuvimos cientos de pequeños y grandes proyectos relacionados con mejoras de productos o nuevos lanzamientos verticales. Muchos de estos proyectos venían acompañados de cambios en nuestros modelos de datos y en nuestros procesos de generación y recopilación de datos. Por desgracia, los modelos de ML pueden ser terriblemente poco fiables cuando el ecosistema de cómo se producen y exponen los datos cambia constantemente, por lo que necesitábamos hacer algunos cambios para mejorar la fiabilidad de nuestro sistema.

Desacoplamiento de las cadenas de dependencia de datos

Podríamos utilizar muchas fuentes de datos diferentes y codificar cientos de características para construir un modelo de alto rendimiento. Aunque esa opción es muy atractiva y ayuda a crear un modelo con mejores resultados que otro que tenga una canalización de datos sencilla, en la práctica crea un sistema que carece de fiabilidad y genera una gran superficie para la deriva de características, en la que la distribución de las entradas cambia con el tiempo. Por eso, a la hora de construir nuestros modelos de previsión, optamos primero por la sencillez y la fiabilidad. Esta estrategia también nos ayudó a avanzar más rápidamente hacia la creación de un sistema de movilización de extremo a extremo.

Canalización complejaCanalización simple
Rendimiento en relación con el ingenuo (en 1 mes)GenialMedia
Rendimiento relativo al ingenuo (en 2 meses)MediaMedia
Rendimiento en relación con el ingenuo (en >3 meses)TerribleMedia
Remodelar la cargaAltaBajo
Carga de guardiaAltaBajo
Figura 4: Los modelos que tienen canalizaciones de datos complejas suelen mostrar su solidez en las primeras semanas de implantación, pero se deterioran rápidamente con el tiempo. Las canalizaciones de datos sencillas suelen ser más robustas y reducen la carga de mantenimiento y remodelación.

Para gestionar la complejidad y los cambios en la forma de exponer los datos, hicimos dos cosas. En primer lugar, desacoplamos las canalizaciones de datos entre sí en dominios empresariales separados. Ahora, cuando alguien trabaja en la mejora de la señal de las entradas utilizadas en la previsión, no necesita verse abrumado por consultas monolíticas y canalizaciones de datos. En su lugar, un desarrollador trabaja en una canalización aislada y sólo necesita asegurarse de que las agregaciones se realizan en una entidad adecuada. 

Lo segundo que hicimos fue eliminar las dependencias de datos intermedias. Reconocimos que si nuestras señales de entrada procedían de trabajos ETL con cadenas de dependencia muy largas en sus grafos acíclicos dirigidos (DAG), aumentaríamos nuestra superficie de fallos. Por lo tanto, aspiraríamos a basarnos en tablas primarias minuciosamente examinadas por nuestro equipo de Business Intelligence o en una copia de fuentes de datos de producción cuya generación de datos no se oculte tras complejos ETL. 

Figura 5: En este ejemplo, la tarea E es la tarea ML con transformaciones de datos. Si los datos de origen de los modelos ML proceden de una larga cadena de dependencias en los sistemas ETL (de A a D), existe un alto riesgo de experimentar desviaciones de características y fallos en los datos si falla alguna de las dependencias ascendentes. 

Centrarse en la experimentación

Una de las mejores formas de comprobar si nuestro sistema es mantenible es simplemente comprobar la velocidad de iteración con la que podemos introducir nuevos cambios y lanzar experimentos sin crear errores ni introducir regresiones. En DoorDash, llevamos a cabo muchos experimentos para determinar si una característica funciona según lo previsto. Por lo general, esto significa que ponemos mucho más énfasis en medir la calidad del software en función de la rapidez con la que podemos ampliar y ofrecer nuevas funcionalidades. Como era de esperar, si los experimentos son difíciles de poner en marcha y las nuevas características son difíciles de probar, hemos fracasado en nuestro objetivo. 

Del mismo modo, si un sistema se mantiene sin cambios durante un largo periodo de tiempo (por ejemplo, seis meses o más), también sería un indicio de que algo va mal, porque significa que hemos creado un sistema que es rígido y no invita a las mejoras, la ideación y la experimentación. La experimentación continua es una función de forzamiento hacia la adopción de mejores prácticas, como confiar en un sistema CI/CD para la automatización, establecer una gestión de dependencias de bibliotecas, desacoplar el código de los dominios de negocio y añadir pruebas, alertas y documentación. 

Resultados

La implantación de este sistema de movilización nos permitió asignar con mayor precisión los incentivos a las unidades regionales que más se beneficiaban de la mejora del equilibrio entre la oferta y la demanda. El sistema se tradujo en una mejor experiencia para nuestros clientes, ya que nuestras métricas mostraron reducciones en los plazos de entrega, cancelaciones y retrasos extremos. Para los Dashers, pudimos garantizar que los incentivos se asignaran en los lugares donde más se necesitaban. En el caso de los comerciantes, redujimos el número total de cancelaciones de pedidos. Además, gracias a las mejoras en la precisión de las previsiones, también pudimos cumplir con mayor fiabilidad las expectativas presupuestarias y observamos una menor variabilidad en las decisiones de gasto. Gracias a las mejoras en la fiabilidad, pudimos llegar a un punto en el que nuestra velocidad de experimentación en el sistema de movilización de incentivos aumentó rápidamente.

Conclusión

Los problemas que tienen que ver con la optimización de un sistema bajo restricciones se benefician enormemente de la alineación con métricas que estimen el impacto de las acciones a un nivel apropiado de agregación. Si las previsiones se hacen a diario, pero las acciones de optimización se toman subdiariamente, es probable que la toma de decisiones no sea óptima. 

En segundo lugar, en general recomendamos desacoplar los componentes de previsión de los de toma de decisiones. La mayoría de los sistemas de optimización funcionan mejor si las entradas tienen propiedades estadísticas estables y las predicciones son estimaciones insesgadas. Por ejemplo, puede ser tentador empezar a utilizar una función de pérdida asimétrica en la previsión para alinearnos con si nos preocupa más predecir mal o demasiado bien el resultado. Aunque este enfoque es perfecto para una serie de problemas en los que la salida de un modelo ML se utiliza inmediatamente para tomar una decisión, para los problemas en los que las predicciones ML son simplemente otra entrada en un motor de optimización más amplio, es mejor generar predicciones insesgadas. En su lugar, es mejor dejar que un motor de optimización se encargue de las compensaciones. 

Si te apasiona crear aplicaciones ML que tengan un impacto positivo en la vida de millones de comerciantes, Dashers y clientes, considera unirte a nuestro equipo.

Agradecimientos

Muchas gracias a Jared Bauman y Dan Madwed por ayudarnos a idear la arquitectura del nuevo sistema, a Gary Ren por guiarnos a través de la complejidad de los componentes de ingeniería de la oferta y la demanda, a Henry Liao por agilizar la experimentación, y a Matthew Ferro y Eugene Braude por presionar para aumentar la automatización.

Un marco de optimización adecuado para la infraestructura de datos agiliza los esfuerzos de ingeniería y permite escalar las plataformas. Aunque una organización de ingeniería puede planificar el crecimiento y la extensibilidad, el rápido crecimiento de los conjuntos de datos es un problema con el que casi todos los equipos de datos se encontrarán en algún momento. 

En 2020, DoorDash experimentó un crecimiento acelerado en cada sección de nuestro mercado tripartito: más clientes haciendo pedidos en nuestra aplicación, más comerciantes en la plataforma y más Dashers (nuestro término para los conductores de reparto) dispuestos a hacer entregas. Este crecimiento dio lugar a un mayor procesamiento de datos, pero seguíamos teniendo que cumplir los mismos acuerdos de nivel de servicio con nuestros socios comerciales, que incluyen consumidores de cuadros de mando, escritores SQL avanzados y científicos de datos. Para seguir cumpliendo nuestros acuerdos de nivel de servicio era necesario optimizar nuestros trabajos ETL para gestionar mejor el aumento de la carga y el uso de los datos. 

En DoorDash nuestro almacén de datos está en Snowflake. Aunque podríamos haber aumentado nuestros recursos informáticos para gestionar la carga adicional, decidimos implementar un marco de optimización para nuestros trabajos ETL con el fin de cumplir nuestros acuerdos de nivel de servicio sin aumentar los costes. 

Orden de optimización

Hemos elaborado esta lista de optimizaciones para priorizar nuestros esfuerzos centrándonos en los elementos que requieren menos esfuerzo y ahorran más costes:

  1. ¿Se puede dar de baja esta ETL?
  2. ¿Podemos romper las dependencias en el grafo acíclico dirigido (DAG)? 
  3. ¿Puede hacerse este ETL de forma incremental? 
  4. ¿Podemos reducir el número de columnas? 
  5. ¿Podemos detener el desbordamiento de datos dividiendo las consultas en un conjunto de datos más pequeño?
  6. ¿Podemos aplicar la agrupación?
  7. ¿Podemos utilizar las funciones Snowflake?

El desmantelamiento de un trabajo ETL ahorra todo el uso computacional de ese trabajo, mientras que añadir clustering a una columna puede que sólo acelere las funciones SQL GROUP BY/ORDERBY, lo cual es menos efectivo. Muchas de las optimizaciones pueden utilizarse conjuntamente, como el uso de una función Snowflake y la reducción del número de columnas. Para cada optimización, discutiremos los pasos de implementación y las ventajas y desventajas asociadas. 

¿Se puede dar de baja esta ETL?

Con la aparición de nuevas fuentes de datos y procesos, es importante eliminar cualquier trabajo ETL heredado que ya no se utilice. Rastreando el DAG hacia abajo podemos hacer un análisis de impacto para ver si se puede eliminar un trabajo. Un análisis de impacto consiste en

  • Encontrar todas las tablas, informes y trabajos ETL que dependen del trabajo ETL que queremos eliminar
  • Observar cómo se utiliza el trabajo. Por ejemplo, si la tabla se utiliza en un LEFT JOIN, puede ser candidata a ser eliminada, ya que los LEFT JOINs pueden tener campos que pueden eliminarse sin afectar a otros campos en algo como un INNER JOIN
  • Mirar hacia arriba para comprobar si las tablas de origen siguen proporcionando datos precisos. La mayoría de las veces es mejor no proporcionar datos que proporcionar datos inexactos.

(Para una explicación más detallada de los DAG en términos de ETL, consulte esta página de documentación de Apache Airflow).

Al desmantelar los trabajos ETL, los equipos pueden liberar recursos informáticos para que se ejecuten otros trabajos. Esto reduce el coste informático total y ayuda a cumplir los acuerdos de nivel de servicio al mismo tiempo, ya que se procesan menos trabajos.

¿Podemos romper las dependencias en el DAG? 

Si el ETL no puede desmantelarse, el siguiente paso es intentar romper sus dependencias. Por ejemplo, supongamos que tenemos dos trabajos que alimentan un trabajo aguas abajo. Ambas tablas de origen son necesarias en la tabla de destino y no podemos reescribir la consulta SQL. Un trabajo, dataset_a, se ejecuta a las 3 de la mañana y el otro trabajo, dataset_b, se ejecuta cada hora. Nuestro trabajo posterior, daily_aggregate, tiene un SLA a las 8 de la mañana. En la Figura 1 podemos ver el aspecto de las dependencias:

Figura 1: En un diagrama del flujo de trabajo ETL descrito anteriormente, la línea de puntos que va del conjunto de datos_b a daily_aggregate representa la unión izquierda. Podemos eliminar potencialmente la dependencia de dataset_b para ayudar a que el trabajo daily_aggregate se complete antes de las 8 de la mañana.

Supongamos que daily_aggregate procede de dataset_a con un LEFT JOIN a dataset_b. Los datos del conjunto de datos_b se ejecutan cada hora, por lo que sólo necesitamos los datos procesados más recientemente hasta el momento en que se crea daily_aggregate. Supongamos también que el ETL del conjunto de datos_b suele tardar 15 minutos en ejecutarse, pero un aumento del volumen de datos puede hacer que los procesos tarden hasta 45 minutos en completarse. Inicialmente, daily_aggregate dependía de que tanto dataset_a como dataset_b se completaran antes de comenzar. Sin cambiar ningún SQL, podemos eliminar el conjunto de datos_b del DAG, como se muestra en la Figura 2, a continuación:

Figura 2: La eliminación de dataset_b, que sufría tiempos de procesamiento inusualmente largos, de nuestro trabajo ETL nos ayuda a cumplir nuestro SLA de las 8 de la mañana.

Como sabemos que el conjunto de datos_b está unido por la izquierda al conjunto de datos_a, y sabemos cómo funcionan las uniones por la izquierda, podemos eliminar la dependencia de daily_aggregate del conjunto de datos_b. Esto nos ayuda a cumplir nuestro SLA ahora que no tenemos que esperar a que el conjunto de datos_b se complete a una hora específica. Esto nos ayuda a alcanzar nuestro SLA ahora que no tenemos que esperar a que el conjunto de datos_b se complete en un momento específico.

Reducir las dependencias superfluas en las configuraciones de DAG rompe los DAG excesivamente grandes y reduce las complejidades. Esta optimización es también una forma barata y eficaz de cumplir los SLA. No se requiere potencia de cálculo adicional, como el escalado del almacén Snowflake.

¿Puede hacerse este ETL de forma incremental?

Durante el año pasado, tuvimos que renovar muchos de nuestros ETL para garantizar el procesamiento diario de todos nuestros datos. Antes de nuestras optimizaciones, los trabajos que estaban diseñados para gestionar actualizaciones de tablas completas empezaron a desbordarse al disco. Algunos de nuestros conjuntos de datos se quintuplicaron en poco tiempo, pero nuestros acuerdos de nivel de servicio siguieron siendo los mismos. 

Una forma de cumplir nuestros SLA fue cambiar muchos de nuestros ETL a trabajos incrementales. Ejecutar un ETL increment al significa sólo insertar o actualizar los registros modificados en una tabla en lugar de sustituir toda la tabla. Al hacer trabajos incrementales se evita reprocesar datos que no han cambiado. 

En DoorDash, los atributos relacionados con la logística de Dasher, como el ID de Dasher, la información a nivel de pedido y las tarifas asociadas a los pedidos no suelen cambiar con el tiempo. En este caso, borrar todos los registros y cargar la misma información, una y otra vez, es innecesario. La arquitectura de los trabajos ETL para que Snowflake procese sólo los datos necesarios ayuda a aumentar el rendimiento, ya que menos datos procesados suelen significar tiempos de procesamiento más rápidos. 

¿Podemos reducir el número de columnas en la cláusula SELECT de la consulta? 

Revisar rutinariamente qué columnas se utilizan en un ETL es una buena forma de reducir la cantidad de datos procesados. A medida que cambian las aplicaciones, es posible que las columnas en uso queden obsoletas. Estas columnas pueden eliminarse de la ETL. Dado que Snowflake almacena los datos en formato columnar, evitamos utilizar SELECT * FROM Tablename en cualquier script ETL. Siempre es aconsejable utilizar SELECT sólo en las columnas necesarias de la tabla. 

Limitar SELECT a las columnas necesarias sirve para mejorar el rendimiento de las consultas:

  • Al evitar seleccionar todas las columnas de la tabla se reduce el volumen del conjunto de datos en el momento del procesamiento, lo que aumenta el rendimiento
  • Este enfoque reduce el almacenamiento en caché de datos innecesarios en la memoria

¿Podemos detener el desbordamiento de datos dividiendo las consultas en un conjunto de datos más pequeño?

La mayoría de las veces, el rendimiento de las consultas se ve afectado por el volumen de datos y el clúster en el que se ejecutan. Cuando aumentar el tamaño del clúster no es una opción, tenemos que pensar en gestionar el volumen de datos en el momento de la ejecución.

En ese caso, identificar el cuello de botella es el primer paso para mejorar el rendimiento de las consultas. Utilizamos el perfil de consulta de Snowflake para identificar el problema que causaba la lentitud. 

Cuando se operan ciertas consultas con múltiples uniones u operaciones agregadas pesadas, existe la posibilidad de que el volumen de datos exceda la memoria de cálculo y comience a derramarse en almacenamientos remotos y locales. Trabajar con los datos derramados en el almacenamiento remoto/local lleva mucho más tiempo que trabajar con los datos en memoria. En este caso, podemos procesar los datos dividiendo las consultas en conjuntos de datos más pequeños y combinando el conjunto resultante. 

Reducir la dispersión de datos disminuye el tiempo de cálculo de las consultas. Dividir las consultas también reduce el coste computacional en Snowflake. Una cosa a tener en cuenta es intentar sobreoptimizar una consulta en demasiados pasos. Una buena forma de comprobarlo es utilizar de nuevo el Perfil de consulta para ver cómo difieren los cambios en el plan de ejecución.

¿Podemos aplicar la agrupación?

Si las consultas se ejecutan lentamente o el perfil de consulta sugiere que está escaneando todas las particiones de la tabla, es una indicación de que esta tabla requiere una clave de cluster en la dimensión apropiada. Snowflake soporta claves de cluster para tablas grandes. 

Las claves de cluster ayudan a particionar la tabla basándose en la dimensión definida y ayudan a reducir el escaneo de particiones en tablas grandes. Debemos analizar las consultas y uniones frecuentes que se ejecutan en una tabla de gran tamaño y decidir la dimensión que se agrupará en función del atributo más utilizado en las condiciones de filtrado y unión.

Al adoptar este enfoque, es importante tener en cuenta que:

  • Las claves de agrupación no deben definirse con una cardinalidad baja, uno o dos valores distintos, ni con una cardinalidad alta, demasiados valores distintos, como las marcas de tiempo.
  • La agrupación en clústeres conlleva un coste asociado en recursos informáticos y almacenamiento.

¿Podemos utilizar funciones dentro de Snowflake?

Otra forma de optimizar las consultas es utilizar funciones Snowflake dentro del código. Esto mejora la legibilidad del código y reduce la posibilidad de error con fórmulas de código duro.

Una pregunta habitual en nuestro negocio es "¿A qué distancia está X de Y?". La distancia puede ser difícil de calcular en SQL dada la curvatura de la Tierra. En estos casos, utilizamos la función integrada HAVERSINE de Snowflake. Haversine calcula la distancia ortodrómica entre dos puntos de una esfera dadas sus longitudes y latitudes, con el formato HAVERSINE( lat1, lon1, lat2, lon2 ).

QUALIFY es otra función útil para eliminar valores duplicados de una consulta. Qualify es a las funciones de ventana en Snowflake lo que HAVING es a GROUP BY en SQL: Permite filtrar filas basándose en los resultados de una función de ventana. Esto es especialmente útil para evitar un segundo paso de los datos. He aquí un ejemplo de Snowflake:

Sin QUALIFY 


Con QUALIFY 

Conclusión

Para el equipo de datos de DoorDash, mejorar un 1% cada día significa desplegar nuevos ETL u optimizar los antiguos constantemente. Disponer de nuestra lista de comprobación Orden de optimizaciones nos permite abordar la optimización desde muchos ángulos diferentes. A medida que nuestro equipo crezca, esperamos añadir más niveles de detalle para ofrecer resultados más rápidos y predecibles. 

Nuestro equipo no solo se centra en ofrecer datos rápidos y fiables, sino también en optimizar los conductos de datos. Esto reduce el uso de recursos informáticos y hace que nuestros procesos sean más eficientes. Tener la oportunidad de aprender cómo fluyen los datos a través de nuestras aplicaciones y lo que eso significa para el negocio es una de las partes más emocionantes de nuestro papel.

Si te interesa resolver problemas de datos interesantes, ven a ver un puesto en nuestro equipo.

Agradecimientos

Gracias al equipo de inteligencia empresarial de DoorDash, y un agradecimiento especial a Josh Li por compartir las funciones de Snowflake con el resto del equipo.



Entre las 16:30 PDT y las 18:40 PDT del 19 de junio de 2021, DoorDash experimentó un fallo en todo el sistema durante aproximadamente dos horas que cargó a los comerciantes con comidas sin entregar, hizo que Dasher's no pudiera aceptar nuevas entregas o registrarse para nuevos turnos, y dejó a los consumidores sin poder pedir comida o recibir sus pedidos realizados de manera oportuna a través de nuestra plataforma. La causa fue un fallo en cascada de múltiples componentes en la plataforma de DoorDash, que puso una carga extrema en nuestra infraestructura interna de pagos, causando finalmente su fallo. El análisis actual no muestra ningún indicio principal antes de que el incidente se desencadenara a las 16:30 PDT, y la mitigación tardó mucho más que el estándar al que aspiramos como equipo de ingeniería.

Somos plenamente conscientes de nuestra responsabilidad como motor de comercio cuyo principal objetivo es hacer crecer y potenciar las economías locales. Como equipo de ingeniería, creemos firmemente que la fiabilidad es nuestra característica número uno, y en este caso hemos fallado, simple y llanamente. Se reembolsará a los clientes los pedidos cancelados como consecuencia de esta interrupción. También se abonarán a los comerciantes los pedidos cancelados durante este periodo. Además, se compensará a los usuarios de Dasher por los pedidos que no hayan podido completar y se eliminarán de su historial las valoraciones con menos de cinco estrellas durante la interrupción. Los interesados en conocer las causas técnicas y las iniciativas de mitigación y prevención que hemos emprendido desde el incidente pueden seguir leyendo.

El impacto de la interrupción

Desde las 16:30 PDT hasta las 18:36 PDT, la mayoría de los Dashers no pudieron aceptar nuevas entregas o registrarse para sus turnos, lo que degradó significativamente las capacidades de cumplimiento de entrega de DoorDash. Como resultado, a las 17:19 PDT DoorDash tomó medidas para detener los nuevos pedidos de nuestros clientes y a las 17:22 PDT implementó la misma acción para los socios de Drive de DoorDash. Tras encontrar y solucionar la causa del incidente, DoorDash volvió a habilitar la capacidad de realización de pedidos de los socios de Drive a las 18:22 y volvió a habilitar la capacidad completa de realización de pedidos para los clientes de DoorDash entre las 18:32 PDT y las 18:39 PDT. 

Cronología

Todas las horas PDT del 19/06/2021

  • 16:30 Comienza a aumentar la latencia de algunas API de pagos internos.
  • 16:30 Empieza a aumentar la memoria y la CPU para los despliegues internos relacionados con los pagos.
  • 16:30 Los servicios relacionados con Dasher comienzan a mostrar un aumento de latencia y la aplicación Dasher comienza a presentar errores a los Dashers que les impiden aceptar nuevos pedidos y registrarse para nuevos turnos.
  • 16:35 Se disparan las alertas de todo el sistema y se llama a los técnicos.
  • 16:40 Los sistemas de pago se reducen en un 50% en un intento de aliviar la presión de la CPU y la memoria.
  • 16:59 Se reinician los sistemas de pago, pero no se consigue una recuperación sostenida.
  • 17:01 Los fallos en cascada quintuplican el volumen de llamadas en los pagos.
  • 17:19 DoorDash detiene todos los nuevos pedidos de consumidores.
  • 17:22 DoorDash Drive se ha desactivado para los socios comerciales.
  • 18:12 El equipo de ingeniería fue capaz de identificar el origen del aumento del tráfico en los sistemas Dasher, que a su vez estaba ejerciendo presión sobre nuestros servicios de pago.
  • 18:12 Todo el tráfico a los sistemas Dasher se detuvo en la capa de red para permitir que los sistemas se recuperen.
  • 18:20 Se volvió a habilitar todo el tráfico hacia los sistemas Dasher en la capa de red, pero volvieron a surgir problemas.
  • 18:22 DoorDash Drive se ha vuelto a habilitar.
  • 18:25 Se desplegó una configuración en los sistemas Dasher para evitar las llamadas de pago descendentes, lo que alivió los fallos en cascada.
  • 18:26 Todo el tráfico a los sistemas Dasher se detuvo en la capa de red por segunda vez para permitir que los sistemas se recuperen.
  • 18:28 Se ha vuelto a habilitar todo el tráfico a los sistemas Dasher en la capa de red.
  • 18:29 Dasher y la salud del sistema de pago sostenido.
  • 18:32 Los pedidos de los consumidores vuelven a estar disponibles en un 25%.
  • 18:37 Los pedidos de los consumidores vuelven a estar disponibles en un 50%.
  • 18:38 Los pedidos de los consumidores se vuelven a activar al 100%.

Análisis de las causas

A partir de las 16:30 PDT del 19/6/2021, la infraestructura de pagos comenzó a mostrar una alta latencia al obtener los datos requeridos por la aplicación Dasher y sus sistemas de apoyo. Mientras los equipos diagnosticaban esta alta latencia y los fallos resultantes, los intentos de reintento de los sistemas Dasher agravaron el problema, ya que el tráfico adicional causado por estos reintentos sobrecargó la infraestructura de pago, que ya estaba en mal estado. Esto provocó que los Dashers no pudieran completar los pedidos, causando malas experiencias a todos los consumidores, Dashers y comerciantes. Aunque hemos definido y documentado las mejores prácticas para la interacción entre componentes que nos ayudarían a mitigar estos escenarios, los componentes involucrados en este incidente (pagos y Dasher) no tenían estos patrones implementados.

Una de las causas del problema es la falta de técnicas de programación defensivas, como el desdoblamiento de carga y la ruptura de circuitos, diseñadas para proteger los sistemas distribuidos como el nuestro de fallos catastróficos como el que estábamos experimentando. En este caso, el servidor (infraestructura de pagos) carecía de la implementación de load shedding, que habría evitado que se colapsara debido a un elevado volumen de peticiones como resultado de latencias más altas. El cliente (aplicación y sistemas Dasher) carecía de la implementación de interrupción de circuito, que debería haberse activado para evitar temporalmente su invocación de una dependencia descendente no saludable. 

El equipo de ingeniería de DoorDash ha pasado todas las horas desde la conclusión del incidente implementando acciones correctivas y prácticas de codificación defensivas mientras investiga activamente los orígenes del desencadenante original de la infraestructura de pagos.

Medidas correctoras de DoorDash

El primer cambio introdujo un mecanismo de eliminación de carga en la infraestructura de pagos que desencadenó el incidente. Este mecanismo, implementado con éxito en la producción el 20/6/2021 a las 07:36 PDT, dotó a la infraestructura de pagos de la capacidad de eliminar de forma ordenada el volumen de solicitudes entrantes que superasen los umbrales de capacidad operativa. 

El segundo cambio introdujo mecanismos de ruptura de circuitos en la infraestructura Dasher y en el manual de funcionamiento. Este mecanismo permite a la infraestructura Dasher eludir su dependencia de la infraestructura de pagos en caso de inestabilidad del servicio. Con estos cambios, confiamos en que nuestra infraestructura Dasher pueda soportar una inestabilidad descendente similar con un impacto mínimo o nulo en todo el sistema.

La tercera acción es una auditoría exhaustiva de las interfaces y API de la infraestructura de pagos para garantizar que existe documentación suficiente y que el gráfico de llamadas ascendente se entiende bien y es totalmente diagnosticable.

Creemos que estos cambios inmediatos ayudarán a evitar que ocurran sucesos similares y nos comprometemos a aprovechar este momento para completar una auditoría exhaustiva de nuestros sistemas que garantice que las mejores prácticas y los conocimientos operativos están bien distribuidos y aplicados. Con el tiempo, esperamos recuperar la confianza de quienes hemos perdido y, como siempre, aspiraremos a ser un 1% mejores cada día.

La capacidad de adjuntar metadatos auxiliares a las solicitudes dentro de una gran arquitectura de microservicios permite potentes casos de uso, como la fragmentación a nivel de infraestructura, la localización de idiomas y las pruebas en producción. Añadir este contexto a las solicitudes permite a los servicios y a las bibliotecas de infraestructura tomar decisiones locales, y puede ser utilizado por componentes de infraestructura en el grafo acíclico dirigido que siguen las solicitudes. Aunque habilitar el contexto en las solicitudes de servicio a servicio tiene grandes ventajas, propagar esta información a todos nuestros microservicios es todo un reto. 

Para proporcionar contexto a nuestras solicitudes, DoorDash es pionera en la adopción del proyecto de código abierto OpenTelemetry para resolver los retos de observabilidad de su arquitectura de microservicios diversa y en expansión. OpenTelemetry se basa en la propagación del contexto para unir los datos telemétricos de una solicitud concreta. Dada la escasez de soluciones de código abierto o de terceros para la propagación de contextos personalizados, OpenTelemetry es lo más parecido a una oferta estándar, razón por la que la elegimos de forma pragmática frente a otras opciones.

En este artículo vamos a repasar nuestra experiencia en la adopción y adaptación de OpenTelemetry para propagar el contexto personalizado con el fin de alimentar una serie de casos de uso críticos. Vamos a profundizar en cómo el contexto personalizado mejora nuestros servicios, cómo implementamos la propagación basada en OpenTelemetry, y cómo desplegamos nuevas versiones de OpenTelemetry y manejamos los problemas de seguridad.

Profundizar en los casos de uso de contextos personalizados

DoorDash utiliza el contexto personalizado para impulsar una serie de casos de uso importantes. Las llamadas a procedimientos remotos (RPC) en las que se basan los microservicios para delegar trabajo a otros servicios utilizan un protocolo de transporte estándar como HTTP o HTTP/2, y un formato de codificación como Protobuf, Thrift o JSON para transmitir solicitudes y respuestas por cable. Cada servicio atiende las solicitudes entrantes utilizando los datos proporcionados en la solicitud. Sin embargo, a veces es útil, o en algunos casos incluso necesario, incluir datos adicionales con la solicitud entrante. Un ejemplo de ello es disponer de tokens de autenticación para los actores implicados en una transacción. La autenticación suele tener lugar más cerca del borde de la red y el token resultante puede pasarse como una cabecera de protocolo en lugar de un campo de solicitud independiente para el gráfico de llamada de servicio.

Otro caso de uso es la prueba en producción, que permite que el tráfico de prueba fluya a través de la implantación de producción. Adjuntamos un contexto tenant-id a cada solicitud, distinguiendo el tráfico de prueba del de producción, lo que nos permite aislar los datos para garantizar que el tráfico de prueba no está mutando los datos de producción. El aislamiento de datos se abstrae en las bibliotecas de infraestructura, que utilizan el contexto para dirigir el tráfico a componentes de infraestructura específicos, como bases de datos y cachés. Con los despliegues de microservicios a gran escala, el sector está convergiendo en las pruebas en producción para realizar pruebas fiables con una menor sobrecarga operativa.

Muchos de los casos de uso que dependen de la propagación de contexto son críticos para el funcionamiento normal de nuestras operaciones empresariales. Esto impone estrictos requisitos de fiabilidad y corrección a la infraestructura de propagación de contextos.

Propagación del contexto con OpenTelemetry

Para la propagación, el contexto puede incrustarse en la propia solicitud, por ejemplo, modificando los búferes de protocolo de la solicitud. Sin embargo, un enfoque más flexible es propagar el contexto como una cabecera de protocolo. El uso de cabeceras para propagar el contexto funciona especialmente bien cuando hay un conjunto diverso de servicios implicados y cuando es necesario propagar el contexto para la mayoría de los puntos finales expuestos por los servicios. Otra ventaja de utilizar la cabecera para la propagación es que el emisor de la llamada no necesita añadir explícitamente el contexto a las llamadas salientes, ya que la propagación puede ser implícita, por lo que añadir un contexto se convierte en un cambio menos invasivo. 

OpenTelemetry requiere la propagación de las cabeceras de rastreo. Esto incluye los ID de rastreo y los encabezados específicos del proveedor. OpenTelemetry proporciona auto-instrumentación para ayudar a propagar las cabeceras de rastreo a través de hilos y límites de servicio. La auto-instrumentación cubre una variedad cada vez mayor de bibliotecas y marcos de trabajo a través de diferentes lenguajes. Esto es especialmente cierto para Java / Kotlin, que es utilizado por la mayoría de los servicios de backend DoorDash.

Algunas características notables de la propagación de contexto de OpenTelemetry son que:

  • Está disponible mediante autoinstrumentación.
  • Admite bibliotecas y frameworks en una variedad de lenguajes que utilizamos en DoorDash, incluidos Java/Kotlin, Node, Python y Go.
  • Es un formato de propagación independiente del proveedor, que incluye formatos abiertos como Trace Context y Baggage del W3C.
  • Admite flujos síncronos como HTTP y HTTP/2, y asíncronos como Kafka.

OpenTelemetry soporta múltiples formatos para la propagación de contexto, incluyendo Baggage, un formato diseñado específicamente para la propagación de contexto personalizado. 

Formatos de propagación de OpenTelemetry

OpenTelemetry soporta una variedad de formatos de propagación, como Trace Context, Baggage, Zipkin y B3. En DoorDash estamos estandarizando en Trace Context para rastrear datos. Para la propagación de contexto personalizado estamos estandarizando en Baggage.

Un vistazo a los formatos de propagación de OpenTelemetry

Trace Context define dos cabeceras: traceparent y tracestate.

Una cabecera traceparent, mostrada en la Figura 1, ayuda a identificar de forma única una solicitud entrante. Contiene la versión, trace-id, parent-id y trace-flags. Esta cabecera ayuda a unir los tramos que genera una solicitud a medida que fluye de un componente a otro.

Figura 1: Una cabecera traceparent consta de identificadores opacos utilizados para el rastreo.

La cabecera tracestate, mostrada en la figura 2, contiene un par clave-valor de datos arbitrarios que permiten propagar identificadores adicionales junto con la cabecera traceparent. Esta cabecera contiene pares clave-valor delimitados por comas.

Figura 2: La cabecera Tracestate se formatea como texto libre que contiene pares clave-valor delimitados por comas.

Tracestate puede utilizarse para propagar un contexto personalizado, pero existen algunas limitaciones. La norma recomienda limitar el tamaño de la cabecera. Aunque esto no es un requisito estricto y el límite puede aumentarse posiblemente haciéndolo configurable, si se cambia tendrá que ocurrir para cada servicio.

Baggage, mostrado en la figura 3, está diseñado para propagar contexto personalizado que tiene límites mucho más altos en el tamaño real de los datos que se propagan. Define una cabecera llamada baggage, que es muy similar a tracestate.

Figura 3: La cabecera Baggage se formatea como texto libre que contiene pares clave-valor delimitados por comas.

Como se muestra en la figura 4, el contexto personalizado puede definirse como un par clave-valor similar a tracestate. Además, se pueden definir etiquetas o propiedades para la clave añadiéndoles punto y coma.

Figura 4: Las cabeceras de equipaje pueden contener opcionalmente propiedades adicionales para los pares clave-valor.

Abstraemos el almacenamiento/recuperación del contexto personalizado en bibliotecas de ayuda para todos los lenguajes comunes en uso en DoorDash. Los propietarios de servicios pueden introducir un nuevo contexto personalizado añadiéndolo a una configuración central, mostrada en la Figura 5, que también sirve como lista de permisos. La configuración es un simple JSON que permite a los propietarios de servicios definir ciertas propiedades del contexto.

{
 "test-workspace": {
   "max_length": 16,
   "allowed_from_client": true,
   "short_name": "tws"
 },
 "tenant-id": {
   "max_length": 16,
   "allowed_from_client": true,
   "short_name": "tid"
 },
 ...
}

Figura 5: Este contexto personalizado allowlist muestra dos campos, test-workspace y tenant-id, con tres propiedades cada una que especifican la longitud máxima permitida para el campo, una bandera para indicar si el campo se puede propagar desde los clientes web/móvil, y un nombre corto utilizado para la propagación real.

Mediante la introducción de una biblioteca de contexto personalizado, que se muestra en la figura 6, podemos cambiar la implementación subyacente para la propagación de contexto. Por ejemplo, este enfoque proporciona flexibilidad en el uso de una caché distribuida como Redis para un contexto más grande y propagar sólo la referencia de caché utilizando las cabeceras OpenTelemetry.

Figura 6: La biblioteca de contextos personalizada, utilizada por los servicios para acceder al contexto, abstrae la implementación subyacente del contexto. Utiliza cabeceras OpenTelemetry y una caché distribuida opcional, como Redis, para contextos más grandes.

Con el tiempo nos imaginamos tener la propagación basada en OpenTelemetry directamente desde nuestros clientes móviles y web. Por ahora, utilizamos cabeceras de protocolo sin procesar para propagar el contexto desde los clientes móviles y web. En la figura 7 se detalla el flujo de cabeceras a medida que la solicitud viaja desde los clientes web/móviles a los servicios backend. Utilizamos la instrumentación automática para la incorporación de los servicios compatibles con OpenTelemetry. La propagación basada en OpenTelemetry comienza en los servicios backend-for-frontend (BFF). Además, las cabeceras de protocolo en bruto entrantes se transforman en cabeceras OpenTelemetry, que luego se propagan a los servicios backend utilizando la función de auto-instrumentación OpenTelemetry.

Figura 7: El contexto se propaga utilizando cabeceras de protocolo sin procesar procedentes de clientes móviles/web, que luego se transforman en cabeceras OpenTelemetry en los servicios BFF. Los servicios backend utilizan las cabeceras OpenTelemetry exclusivamente para la propagación.

Es importante señalar que la política de muestreo de las trazas de OpenTelemetry no afecta a la propagación del contexto. Las políticas de muestreo solo afectan a la recopilación y agregación de las trazas.

Nuevas versiones de OpenTelemetry

Siendo uno de los primeros en adoptar OpenTelemetry, tuvimos que mantenernos al día con la rápida rotación de las herramientas de código abierto y las frecuentes versiones, incluyendo cambios incompatibles de la API. Rápidamente nos dimos cuenta de que potencialmente tendríamos varias versiones de las herramientas de OpenTelemetry desplegadas en la producción. Afortunadamente, el formato de propagación abierta ayuda a preservar los formatos de cabecera a través de versiones. Sin embargo, tenemos que lidiar con el seguimiento de las versiones de la biblioteca que dependen de versiones específicas OpenTelemetry. Aumentar la versión de OpenTelemetry a veces requiere aumentar las versiones de las bibliotecas relacionadas con los servicios en masa. Hemos estado explorando herramientas para facilitar la actualización automática de las versiones de la biblioteca, incluyendo algunas herramientas de cosecha propia.

El despliegue de una nueva versión de OpenTelemetry se maneja con cautela dado el rápido desarrollo dentro del proyecto. Con el fin de contener las consecuencias, hemos ideado una manera de desplegar selectivamente una nueva versión a una parte de la flota y poco a poco el aumento a medida que construimos la confianza. Dicho esto, porque los casos de uso críticos se basan en la propagación del contexto, es imperativo que el contexto se propaga independientemente de la versión OpenTelemetry un servicio está utilizando.

Cuestiones de seguridad

Con la autoinstrumentación de OpenTelemetry, las cabeceras se propagan implícita e incondicionalmente. Si bien esto simplifica la adopción, plantea el riesgo de exponer el contexto potencialmente sensible a las entidades de terceros que están siendo llamados. Aunque la autoinstrumentación puede desactivarse para la propagación de bibliotecas, no puede desactivarse de forma selectiva en función de los destinos de red. El riesgo de exposición también se aplica a las entidades de terceros que llaman a DoorDash, que podrían aportar contexto irrelevante que preferiríamos que no se propagara a los servicios de DoorDash. Para solucionar este problema, eliminamos todas las cabeceras de OpenTelemetry que no sean traceparent tanto a la entrada como a la salida de la red de DoorDash. Esto evita la inyección injustificada de contexto desde fuera de la red, así como la exposición del contexto interno a la red exterior.

La abstracción de la biblioteca para el contexto personalizado nos permite cifrar opcionalmente sólo las cabeceras si el tráfico de servicio a servicio no está cifrado. Esto proporciona una capa adicional de seguridad que evita la exposición de datos potencialmente sensibles.

Conclusión

El uso de la propagación de contexto para la propagación transversal y el contexto de negocio requerido con frecuencia es omnipresente en una arquitectura de microservicios de rápido crecimiento. OpenTelemetry ofrece una solución que no sólo permite el rastreo distribuido de una manera independiente del proveedor, sino que también proporciona herramientas de código abierto fáciles de usar para una variedad de lenguajes y plataformas. Con ciertos guardarraíles de seguridad y despliegue en su lugar, la propagación de contexto personalizado a través de OpenTelemetry puede ayudar a acelerar los casos de uso que vienen a depender de él.