A medida que rediseñábamos la plataforma de DoorDash, la migración del complejo sistema de gestión de sesiones de nuestra base de código monolítica a nuestra nueva arquitectura de microservicios resultó ser uno de los retos de ingeniería más difíciles a los que nos enfrentamos. Este sistema crítico reconoce a cada usuario único de la experiencia web del consumidor de DoorDash y tiene múltiples dependencias. Para complicar aún más las cosas, necesitábamos completar la migración sin tiempo de inactividad.
Comenzamos el proceso de migración de DoorDash de una base de código monolítica a una arquitectura de microservicios en 2019. Este cambio fundamental nos proporcionó una plataforma más escalable capaz de satisfacer las crecientes necesidades empresariales de DoorDash. Gran parte del trabajo consistió en extraer la lógica de la base de código anterior y reescribirla como un servicio en la nueva arquitectura.
Al migrar el sistema de gestión de sesiones, no sólo tuvimos que desenredar las dependencias existentes, sino que también descubrimos dependencias ocultas. Mantener la funcionalidad exigía comprender la lógica de las dependencias e identificar a sus propietarios.
Nuestra estrategia de migración implicaba construir un sistema de gestión de sesiones duplicado en nuestra arquitectura de microservicios. El nuevo sistema tenía que coexistir con el anterior en nuestra plataforma y requería componentes que pudieran soportar nuestra carga de producción. Para completar la migración sin tiempo de inactividad, era necesario conceder sesiones a los usuarios de forma silenciosa en el nuevo sistema para reducir cualquier fricción mientras los desconectábamos del antiguo.
Este proyecto demostró el compromiso de nuestro equipo con la resolución a gran escala de problemas de ingeniería complejos que mejoran la experiencia de nuestros clientes.
Nuestra gestión de sesiones monolito es una dependencia implícita
Aunque nuestra nueva arquitectura de microservicios ofrece muchas ventajas en términos de productividad de los desarrolladores, su principal objetivo era mejorar la fiabilidad a medida que el negocio de DoorDash crecía rápidamente. Lamentablemente, mientras nuestra lógica de sesión web permaneció en el monolito, nuestra funcionalidad de identificación de usuarios también permaneció en esta arquitectura heredada. Dado que esta funcionalidad es la primera acción para procesar cualquier solicitud web entrante, nuestra fiabilidad general estaría ligada al rendimiento del monolito, que ya considerábamos que no era lo suficientemente escalable como para soportar nuestro crecimiento futuro. A pesar de nuestro creciente conjunto de microservicios, tendríamos que escalar el monolito para mantener nuestro crecimiento hasta que pudiéramos migrar el servicio de sesión.
¿Qué es exactamente una sesión?
Cuando hablamos de sesiones, nos referimos a una serie de llamadas HTTP realizadas por el mismo usuario a un servidor. En el lado del servidor, un sistema de gestión de sesiones normalmente identifica y autentica a un usuario cuando inicia sesión, y realiza un seguimiento de cuándo cierra sesión. Para profundizar mucho más en la gestión de sesiones, cómo interactúa con la autenticación y el control de acceso, consulta esta excelente hoja de trucos sobre gestión de sesiones del Open Web Application Security Project.
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.
Introduzca una dirección de correo electrónico válida.
Gracias por suscribirse.
Extracción de alto riesgo
La gestión de sesiones de DoorDash se sitúa en medio del proceso de autenticación/autorización y, tras solicitar la opinión de diferentes equipos internos, nos dimos cuenta de que su conexión con la seguridad, las herramientas internas y la captación de clientes le confiere un gran impacto empresarial.
La gestión de sesiones impulsa directamente la experiencia del cliente
La parte más importante de la migración fue mantener las sesiones existentes antes, después y durante el proceso de autenticación, y nuestra capacidad para atenderlas en el sitio web. Muchos canales de venta diferentes llevan a los nuevos clientes al sitio web, lo que les permite iniciar su recorrido como clientes. Si no podemos ofrecer un nuevo sistema que funcione al menos tan bien como nuestro módulo actual, corremos el riesgo de alterar la experiencia del cliente.
La invalidación de la sesión es una importante herramienta de seguridad
Invalidar una sesión, cuando creemos que la sesión de un usuario debe cerrarse, es una medida de seguridad importante. Las empresas a menudo necesitan finalizar la sesión de un usuario, ya sea en respuesta a un incidente de seguridad, como en el caso de este incidente de seguridad de GitHub, o para eliminar inicios de sesión obsoletos. Como parte fundamental del éxito de la migración, necesitábamos garantizar que la herramienta de invalidación de sesión siguiera funcionando, reduciendo la exposición al riesgo para nuestro negocio en general.
Las herramientas internas dependen de la lógica de sesión del monolito
Nuestro monolito alimenta muchas de nuestras herramientas internas, desde la configuración de nuestros mercados y tiendas hasta el alojamiento de nuestra infraestructura de experimentación, todas las cuales dependen de nuestro módulo de sesión heredado. Los problemas con la migración del sistema de gestión de sesiones podrían bloquear al equipo de sistemas críticos. Esto les impediría mitigar las cambiantes condiciones del mercado y podría crear un efecto cascada que afectara negativamente a nuestro negocio.
Alcance de la migración de sesiones
Uno de los principales retos a los que nos enfrentamos fue comprender la interacción entre los sistemas de gestión de sesiones y otros módulos críticos. Sabemos que cualquier sistema de gestión de sesiones funciona en tándem con los módulos de autenticación e identidad. Sin embargo, nos enteramos de que otros equipos habían modificado previamente el módulo de gestión de sesiones para poder lanzar productos con rapidez.
Averiguar todas las dependencias de los módulos y los usos de la API requirió una profunda inmersión en las funcionalidades existentes, explorando el sistema internamente y realizando pruebas con los usuarios. Navegamos por el sitio, realizando acciones como haría un cliente típico, mientras sondeábamos el sistema, inspeccionábamos los registros y leíamos los códigos para comprender los comportamientos del backend. A través de esta exploración, el equipo inspeccionó los códigos fuente de la biblioteca, rastreó rutas de código creadas hace más de cinco años y colaboró con diferentes equipos para identificar desde el principio diversos retos y posibles problemas o escollos.
El módulo de sesión monolito heredado carece de propiedad
Al principio de este proceso, necesitábamos determinar a qué equipo pertenecía cada funcionalidad incorporada en el sistema de gestión del sistema. En DoorDash, hemos estado aprovechando el módulo de sesión incorporado de Django, que permite a los desarrolladores almacenar datos arbitrarios para cada usuario, y hace uso de cookies para almacenar una clave de ID de sesión para asignar a dichos datos. Al inspeccionar el almacén de sesiones, encontramos un montón de datos extraños junto con datos que son críticos para la experiencia de pedido de un consumidor. Al explorar la base de código, encontramos una serie de problemas, como código espagueti, falta de documentación y soluciones desechables en torno a los datos en cuestión.
Estos descubrimientos significaban que parte de la migración tendría que implicar desenrollar otras funcionalidades de la lógica del monolito. Para ello, tendríamos que trabajar con otros equipos para revisar toda la funcionalidad del módulo, asignar la propiedad y determinar si formaría parte de la migración o necesitaría un nuevo hogar en el monolito.
El sistema puede emitir por error dos sesiones para dos sistemas diferentes
Durante nuestra exploración, descubrimos que algunas páginas web seguían siendo servidas por nuestro monolito, lo que podía resultar en la creación de dos IDs de sesión diferentes para un usuario. Si un usuario navegaba por una de estas páginas desde el monolito, el gestor de sesiones de Django adjuntaría su propia cookie de ID de sesión, y el usuario estaría manteniendo dos cookies de ID de sesión diferentes en su navegador, una para el sistema de sesiones del monolito y otra para el nuevo sistema de sesiones.
El resultado de tener múltiples cookies de ID de sesión es que las peticiones posteriores contendrán dos ID de sesión diferentes, haciendo difícil saber en cuál se debe confiar. Lo que puede ocurrir a menudo es que diferentes componentes del sistema sean incapaces de manejar los dos ID de sesión proporcionados en la misma petición, causando errores de ejecución. Una de estas páginas problemáticas es la página de recepción, a la que nuestros usuarios son redirigidos inmediatamente después de realizar el pago. En este caso, nuestros usuarios saldrán e inmediatamente se les pedirá que proporcionen sus credenciales para volver a iniciar sesión, creando una experiencia de usuario negativa.
La experiencia web fue gestionada por el módulo de sesión monolito
En un flujo de autenticación típico, la sesión web de un usuario existe como sesión de invitado antes del paso de autenticación. Cuando el usuario inicia sesión, ésta se convierte en una sesión autenticada. La figura 1 ilustra cómo un sistema de sesiones interactúa con las sesiones de invitado, la autenticación y el control de acceso. Las sesiones de invitado son exclusivas de nuestra experiencia web, ya que los usuarios de las aplicaciones para iOS y Android deben iniciar sesión para navegar por los comercios y entregar artículos.
En DoorDash, tenemos varios equipos de plataforma que poseen y operan nuestro módulo de autenticación, pero el seguimiento de las sesiones de los huéspedes no forma parte de sus dominios. En nuestro flujo de compras web normal, los clientes pueden navegar por las tiendas y los menús, y añadir artículos a su cesta. Pero deben iniciar sesión antes de poder pagar. Nuestra reimplementación del nuevo sistema de gestión de sesiones debe garantizar el mismo control de acceso que nuestro sistema actual, permitiendo las sesiones de invitados, pero también integrarse con el proceso de autenticación de nuestra plataforma para permitir las transacciones comerciales.
Planificación del despliegue, la migración y la observabilidad
Nuestro sistema de gestión de sesiones sustitutivo tenía que ofrecer la misma funcionalidad y el mismo rendimiento que el módulo de sesiones monolito. Pero esto no era más que el principio. Nuestra fase de exploración reveló una situación difícil en la que nuestra lógica heredada era fundamental para garantizar una experiencia fluida del cliente, tenía múltiples dependencias y era un componente central de otras partes de la plataforma. Al mismo tiempo, nuestra exploración puso de manifiesto una falta de propiedad y, en cierta medida, una falta de observabilidad y conocimiento del sistema existente.
Según estas conclusiones, no bastaría con crear un sistema de gestión de sesiones funcionalmente equivalente. Durante el despliegue, el nuevo sistema concedería sesiones a una parte de nuestros usuarios, mientras que otros tenían sesiones del sistema monolito, y este periodo de superposición de sistemas de sesiones fue la parte más complicada del proyecto. Necesitábamos funciones adicionales y una lógica específica para este periodo de transición. Al final, nos decidimos por tres principios rectores, que denominamos "metacaracterísticas", para nuestro despliegue:
- El despliegue tendría que ser seguro para nuestros clientes, ya sea con el monolito existente o con el sistema de sesión de sustitución; si uno de ellos funcionara mal, podríamos trasladar con seguridad a los clientes afectados al otro sistema.
- La implantación debía ser observable y mensurable. Necesitábamos saber en todo momento cuántas personas utilizaban el nuevo sistema de sesiones y cómo era su experiencia en comparación con los usuarios del sistema monolito.
- La migración debía ejecutarse sin tiempo de inactividad y con una interrupción mínima, ya que la empresa y muchos equipos dependían de su eficacia.
Las meta características para un despliegue seguro, observando el rendimiento y logrando una interrupción mínima eran engañosamente simples y directas. Sin embargo, la complejidad del problema y su repercusión en todas las solicitudes entrantes nos llevaron a deliberar cuidadosamente sobre cómo lograr esta migración.
Garantizar un despliegue seguro
Sabiendo que avanzar es fácil, pero retroceder es difícil, tuvimos que averiguar cómo decirle a los navegadores que mantuvieran un ID de sesión diferente si avanzábamos, y borrar su ID de sesión si retrocedíamos. En DoorDash, desplegar una nueva ruta de código a través de nuestro marco de experimentación no es algo nuevo; sin embargo, estas rutas de código suelen estar en el lado del servidor o en el lado del cliente. En nuestro caso, queríamos sincronizar el despliegue tanto en el lado del servidor como en el lado del cliente. Afortunadamente, ya teníamos un handshake entre el cliente y el servidor, que ocurre antes de que nuestra aplicación React de una sola página comience a renderizar. Decidimos ampliar este único punto de entrada para sincronizar el estado de despliegue del cliente y el servidor.
En este único punto de entrada, pudimos centralizar la lógica para comprobar el estado de un experimento. Como no queríamos que nuestros usuarios tuvieran dos ID de sesión diferentes, si detectábamos que el usuario estaba en el grupo de tratamiento del experimento, pedíamos al navegador que borrara sus cookies de sesión existentes y aceptara un nuevo conjunto de cookies de sesión.
Al mismo tiempo, implementamos un interruptor de corte. Si activábamos este interruptor, el navegador debía borrar todas las cookies relacionadas con la sesión y el servidor volvía a enviar al navegador las cookies de sesión gestionadas por el monolito, con lo que volvíamos a nuestro estado predeterminado anterior a la migración. Después de probarlo entre los miembros de nuestro equipo y un subgrupo del personal, confirmamos que el nuevo interruptor de corte se comportaba como deseábamos y que podíamos volver a la sesión gestionada por el monolito si el nuevo sistema se comportaba mal. El diagrama de decisión simplificado de la Figura 2, a continuación, ilustra las condiciones que comprobamos para determinar qué sistema de sesión se aplicaba al usuario.
También creamos una cookie especial, a la que nos referimos como cookie mágica, que permitía a cualquiera entrar en el nuevo flujo de sesión. En su nivel más básico, esta cookie actuaba como una bandera para una petición HTTP entrante. Si el servidor detectaba el indicador durante el proceso de establecimiento de la sesión, invocaba una lógica personalizada para evitar la experimentación o la comprobación del interruptor de corte, y permitía directamente a su navegador asociado utilizar nuestro nuevo sistema de gestión de sesiones.
Esta cookie nos permitió aprovechar nuestro conjunto de pruebas de extremo a extremo existente, o realizar pruebas manuales, para poder validar continuamente que nuestro nuevo sistema de sesiones era compatible con el resto de la arquitectura de DoorDash, que cambiaba rápidamente.
Garantizar una implantación observable y mensurable
Al principio del proyecto de migración, colaboramos con nuestro equipo de análisis de datos para elaborar un plan experimental que garantizara que la tasa de conversión de la plataforma no se viera afectada cuando los clientes migraran al nuevo módulo de sesiones. El resultado de esta colaboración fue un informe analítico que realizaba un seguimiento de todos los pasos de nuestro recorrido del cliente, comparando el compromiso de los clientes en cada paso. El equipo obtuvo una visibilidad de alto nivel del rendimiento del nuevo sistema de sesiones en comparación con el flujo existente, lo que nos permitió identificar rápidamente las etapas de bajo rendimiento y diagnosticar posibles problemas.
Desde el punto de vista de la ingeniería, también aprovechamos nuestra decisión de crear un único punto de entrada de sesión añadiendo declaraciones de registro de fallos y enviando datos métricos, lo que nos permitió conocer el estado y el rendimiento de todos nuestros componentes. Gracias a estos controles basados en datos, pudimos eliminar las conjeturas del plan de despliegue, lo que nos ayudó a decidir cuándo debía ser nuestro próximo incremento y cuánto podíamos aumentar con confianza.
Perturbaciones mínimas
Para garantizar una experiencia fluida a los clientes que navegan entre páginas web alojadas en nuestra arquitectura de monolito y microservicio, hemos añadido una nueva lógica a nuestro monolito que descodifica las solicitudes entrantes que contienen una nueva cookie de ID del sistema de gestión de sesiones y evita que se emitan cookies de ID de sesión del monolito si existe una nueva ID de sesión. Aunque parece contradictorio añadir nueva lógica a un monolito que estamos dejando obsoleto, el equipo creyó que era una solución sensata.
Nuestro monolito, escrito en Django, adjunta su propia sesión durante una petición HTTP como un comportamiento de librería, y no queríamos deshabilitar este comportamiento globalmente ya que podría llevar a otros errores inesperados. Podríamos haber movido toda la funcionalidad de renderizado de plantillas fuera de nuestro monolito para evitar completamente este problema, pero necesitábamos centrarnos en nuestra migración de gestión de sesiones para desacoplarnos de nuestro monolito lo más rápido posible.
Después de que nuestro monolito se hiciera compatible con nuestro nuevo sistema de sesiones, los usuarios podrían navegar entre todas las páginas, independientemente del sistema de sesiones que concediera su sesión, lo que nos permitió aplazar la migración de la renderización de páginas y tratarla como un proyecto independiente. Resultó que nuestro monolito seguiría involucrado con el nuevo sistema de gestión de sesiones.
Parte de la migración consistió en simular cargas pesadas para validar que nuestros nuevos componentes estaban preparados para gestionar nuestro volumen de tráfico de producción. Utilizamos loadtest, una biblioteca de código abierto, para crear tráfico sintético y observar el comportamiento de nuestro sistema en condiciones de estrés. Detectamos algunos problemas de fugas de memoria y cuellos de botella que se habrían manifestado cuando empezamos a recibir una cantidad de tráfico no trivial, y evitamos ralentizar a nuestros clientes en un entorno de producción en vivo.
Como parte de esta migración, comunicamos a otros equipos los cambios que se avecinaban. Nos aseguramos de que cada equipo fuera consciente de este cambio y de su posible impacto en nuestras herramientas internas, lo que podría obstaculizar directamente la capacidad de cada uno para hacer su trabajo. Esta comunicación fue importante, ya que nos dio la oportunidad de interactuar con los equipos que estaban preocupados por cómo los cambios en la sesión afectarían a nuestra capacidad para realizar un seguimiento de los comportamientos de los usuarios durante los experimentos. Para disipar las preocupaciones generales, también compartimos nuestros gráficos y cuadros de mando con nuestros equipos de producto, que a su vez nos ayudaron a vigilar los gráficos, porque esas métricas afectaban directamente a todas las funciones basadas en la web.
Hacer que nuestro código funcione mejor para eliminar fricciones con nuestros clientes.
Tras meses de planificación y ejecución, desplegamos el nuevo flujo de sesiones al uno por ciento de nuestros clientes. Utilizando el análisis proporcionado por nuestro equipo de análisis, observamos un pequeño pero detectable descenso de las conversiones. Dado el pequeño porcentaje de despliegue, podría haber sido sólo ruido en los datos. Tras analizar los registros y los gráficos, y utilizar nuestra cookie mágica para realizar pruebas manuales, estábamos seguros de que el nuevo sistema funcionaba como esperábamos.
Sin embargo, cuando el mismo patrón se repitió tras aumentar al dos y luego al cinco por ciento de todos los clientes, tuvimos que investigar más a fondo. Una vez más, no pudimos encontrar nada incorrecto en los registros de errores ni en los gráficos.
Llegados a este punto, nos planteamos la hipótesis de que la agrupación de más y más usuarios en el nuevo flujo de sesiones creaba una fricción adicional para nuestros usuarios, ya que empezaban como invitados desconectados en el nuevo sistema de gestión de sesiones. Este es un efecto secundario de una política que introdujimos para garantizar que ningún usuario recibiera dos conjuntos diferentes de cookies de sesión.
En nuestra búsqueda de una migración verdaderamente libre de interrupciones, volvimos a la mesa de dibujo para resolver este problema para nuestros clientes. Como nuestro monolito comprendía en ese momento tanto la sesión nueva como la heredada, implantamos un nuevo mecanismo de intercambio de sesiones. Cuando detectábamos un cliente que, según nuestro marco de experimentación, debería estar utilizando la nueva sesión, pero tenía una sesión heredada, suponíamos que ese cliente estaba visitando el sitio por primera vez después de haber sido incluido en el grupo de tratamiento. La lógica de esta comprobación se muestra en la figura 3:
Dentro de nuestro único punto de entrada, llamamos al punto final de intercambio de sesiones monolith, que devuelve todos los datos almacenados en el sistema de sesiones monolith. A continuación, seleccionamos la información relevante para almacenarla en el nuevo sistema de sesiones. En otras palabras, preparamos nuestro nuevo sistema de gestión de sesiones con los datos existentes del cliente, de modo que sus solicitudes posteriores puedan seguir autenticándose en el nuevo sistema de sesiones. La figura 4, a continuación, muestra el flujo de red revisado en un diagrama simplificado.
Tras probar este nuevo mecanismo de intercambio y confirmar que funcionaba, desplegamos la funcionalidad adicional antes de nuestro siguiente aumento. Con esta nueva función, observamos que nuestra tasa de conversión se había recuperado, volviendo al mismo nivel que los clientes con la sesión anterior.
Conclusión
Migrar una parte crítica de la funcionalidad a un nuevo sistema, especialmente uno integrado en el marco existente, es complicado. Las nuevas empresas, en particular, pueden considerar que los marcos de trabajo completos, como Django, ofrecen facilidad de uso y una rápida creación de prototipos de productos. Por otro lado, optar por un marco de trabajo concreto puede hacer que los equipos aplacen sus decisiones de arquitectura, dejando a las empresas con piezas de funcionalidad que no poseen, conocimiento tribal y otras sorpresas bajo el capó.
Aunque esta migración supuso un reto, también fue una oportunidad para poner a prueba las habilidades interpersonales de nuestros equipos de ingenieros. El éxito depende de la capacidad de trabajar con otros equipos para identificar a los responsables, delegar responsabilidades si es necesario y comunicar los próximos cambios. Durante un proceso tan crítico, es importante actuar con buen criterio y resolver los conflictos entre compañeros o escalarlos hacia arriba.
El despliegue de cualquier funcionalidad básica arriesgada podría tener un gran impacto en las métricas empresariales. No sólo es necesario un plan de despliegue, sino que su validación será crucial para un resultado satisfactorio. Nuestro enfoque en la observabilidad, tanto desde el punto de vista de la ingeniería como de la empresa, nos dio la seguridad y confianza necesarias para todo el despliegue y, en última instancia, para una migración sin incidentes.
En última instancia, la obsesión del equipo por el cliente es lo que nos ha llevado al éxito. Desde el principio, supimos que teníamos que ofrecer una experiencia de transición fluida a nuestros clientes, tanto internos como externos. Indagamos en los detalles, tratando de descubrir dependencias ocultas en el sistema monolítico de gestión de sesiones; vigilando de cerca cualquier impacto empresarial en nuestros usuarios finales; preguntándonos si podíamos ofrecer una experiencia mejor; y acabamos con nuestro mecanismo de intercambio de sesiones para reparar nuestro lastre de conversión. Al final, no hubo mejor elogio que la falta de aviso de nuestros clientes.
Agradecimientos
Los autores agradecen a Jie Qin, Li Pei, Dananjayan Thirumalai, Rohit Raghunathan, Ivar Lazzaro, Dmitriy Dunin, Bri Lister, Ram Prasanna, Vlad Protsenko, Michael Sitter, Esha Mallya, Corry Haines, Hang Yin, Kevin Chen, Robert Lee, Arun Kumar Balasubramani y Kyle Mogilev su contribución y ayuda a lo largo de este trabajo.