Viajes Simulados: Cómo usa Lyft pruebas de carga para garantizar un servicio confiable durante picos de actividad.

Elihu A. Cruz
Lyft Engineering en Español
16 min readAug 14, 2023

--

Este artículo fue publicado originalmente el 27 de Marzo de 2023 en eng.lyft.com por Remco van Bree y fue traducido por Elihu Cruz

Autores: Remco van Bree, Ben Radler

Colaboradores: Alex Ilyenko, Ben Radler, Francisco Souza, Garrett Heel, Nathan Hsieh, Remco van Bree, Shu Zheng, Alex Hartwell, Brian Witt

“Las pruebas de carga en producción son buenas”

Sabemos lo que estás pensando — probar en producción es uno de los pecados capitales en el desarrollo de software. Sin embargo, en Lyft hemos llegado a la conclusión que las pruebas de carga en producción son una poderosa herramienta para preparar los sistemas ante ráfagas inesperadas de tráfico y picos de actividad. Exploraremos porque Lyft necesitó un “framework” de pruebas de rendimiento personalizado que funcione en producción, cómo construimos una solución multifuncional, y cómo hemos continuado mejorando esta plataforma de pruebas desde su lanzamiento en 2016.

¿A qué nos referimos exactamente con “prueba de carga”? En el contexto de este artículo nos referimos a cualquier herramienta que genere tráfico para sistemas de pruebas de estrés y observe cómo se desempeña en los límites de su capacidad.

Lyft debe operar sin problemas incluso cuando la demanda se dispara

Es imperativo para Lyft ser altamente disponible durante eventos de alta demanda como Noche de Brujas (Halloween) y la víspera de Año Nuevo. Los usuarios dependen de nosotros para sus necesidades de viajes, y los conductores dependen de nosotros para ganarse la vida, especialmente en los días más ocupados del año. Históricamente los picos de actividad suponían un gran reto para Lyft, ya que experimentamos un tráfico sin precedentes que a menudo se moldeaba en patrones inusuales comparados a una semana normal de trabajo. Además, debido a la tremenda velocidad a la que Lyft está creciendo, el tráfico ha estado interactuando con nuevos servicios que no existían durante el anterior pico de actividad.

Antes de adoptar la simulación consideramos una variedad de estrategias de pruebas de carga

Históricamente, uno de nuestros grandes cuellos de botella durante estos picos de actividad era la escritura a las bases de datos. Al principio buscamos herramientas de código abierto y herramientas estándar de la industria para ayudarnos a resolver estos problemas de escalabilidad.

Las herramientas de grabado y reproducción no son aptas para pruebas probabilísticas

Una de las formas estándar de la industria para pruebas de carga es usando grabados y reproducción, herramientas tales como “Gatling”, “K6”, y “Bees With Machine Guns”. Con estas herramientas puedes grabar una sesión, hacer algo de programación simple para lidiar con escenarios repetitivos como inicio de sesión, y luego reproducir este tráfico en la red desde sesiones pregrabadas.

Esta es una buena solución para servicios que son completamente deterministas; como un comercio electrónico (e-commerce) donde un sitio generalmente permitirá a un cliente ordenar más de un producto.

Un mercado bilateral (two-sided marketplace) como Lyft es mucho más complicado: solicitar un viaje solo funciona si hay un conductor presente. Los conductores necesitan ser emparejados con un cliente y tener una ubicación lo suficientemente cercana a un cliente para ser capaz de iniciar el viaje, entre otras cosas. Aun así, muchos de los sistemas en Lyft no son deterministas — por ejemplo, no hay garantía que un conductor particular y un cliente sean emparejados incluso si están ubicados lo suficientemente cerca uno del otro.

Capturar y reproducir tráfico de producción es tosco, lento e incompleto

En 2016, Lyft estuvo presionando su cluster de base de datos principal más allá de sus solicitudes por segundo (QPS, queries per second) y límites de conexión. Nuestra primera estrategia de prueba carga siguió la ruta que podrías esperar: iniciamos grabando tráfico de la base de datos y reproduciendo contra una réplica de la base de datos que no atendía usuarios reales. Esto nos permitió ver qué pasa si incrementamos aún más las solicitudes por segundo y/o conexiones.

Este enfoque involucra código personalizado, y rápidamente notamos múltiples problemas — era difícil y costoso en tiempo crear y restaurar un enorme respaldo de base de datos (esto requería un ticket de soporte con nuestro proveedor en cada ocasión). Además, si algo salía mal, teníamos visibilidad limitada del porqué. Por ejemplo, ¿grabamos el tráfico correcto? El ciclo de retroalimentación de este enfoque era muy largo también, solicitándonos iniciar la prueba de carga desde el inicio si cualquier error ocurría durante la prueba.

Además, se plantearon preocupaciones sobre el futuro desarrollo del patrón de grabado/repetición. Dada la naturaleza de estado de los servicios de Lyft, el tráfico reproducido podría causar problemas, como la doble facturación de clientes, o ser detectado por las protecciones de repetición y eludir los flujos de código que causan la degradación en primer lugar.

Consideramos la posibilidad de reproducir el tráfico de producción contra el entorno de pruebas (staging), pero dado el coste de escalar el ensayo para que coincida con el de producción durante estas pruebas y debido a la naturaleza altamente estadística y probabilística de estos sistemas, este enfoque no parecía ofrecer mucho beneficio. Además, los entornos de pruebas a menudo contienen mucha basura o datos de prueba, lo que significa que las pruebas en este entorno pueden dar lugar a falsos positivos o negativos.

Dada la falta de éxito con este enfoque de grabación/reproducción, además de la creciente preocupación por estarnos perdiendo de probar todas las partes interesantes por encima de la base de datos (como nuestro proxy Envoy y la malla de servicios), nos decidimos por el camino de la simulación. Esto nos ayudó a dejar de centrarnos en las bases de datos y a trabajar en un producto que nos ayudaría a identificar los fallos en cascada que podrían producirse cuando uno o más servicios se degradan o experimentan una carga inusualmente alta.

Necesitábamos un marco de pruebas de desempeño que fuera más allá de la reproducción histórica del tráfico

Creamos un sistema que imita a escala los picos del mundo real: Viajes Simulados

Arquitectura de Viajes Simulados

Cambiamos nuestra forma de pensar hacia un sistema en el que dictaríamos alguna configuración para un escenario que quisiéramos probar (lo que denominamos una “simulación”), y los propietarios del servicio medirían el comportamiento resultante de esta simulación. Es decir, el resultado de estas simulaciones podría proporcionar señales en nuestros diversos sistemas de observabilidad, como métricas, alarmas, registros, etc. Esto puede pensarse como un “ingeniero de control de calidad automatizado” (automated QA Engineer) que intenta todas las combinaciones de flujos y entradas en un software, buscando casos inusuales en los que las combinaciones de flujos no funcionan como se esperaba.

El servicio de Viajes Simulados (SimulatedRides) es un servicio de orquestación. Gestiona una flota de Clientes, y simula las interacciones que un usuario real podría hacer en la app de Lyft. Ofrecemos una interfaz a Viajes Simulados tal que puede ser tratada como una plataforma por los propietarios de servicios (otros equipos) en Lyft. Estos equipos pueden optar por contribuir a la “cobertura” con un par de líneas de código en Python.

Hay cuatro conceptos clave para entender cómo funciona Viajes Simulados: Simulaciones, Clientes, Acciones y Comportamientos

Simulaciones: cómo definimos nuestros escenarios de prueba

Cada Simulación se define mediante una única entrada en una tabla DynamoDB. Esta configuración dicta cómo funcionará la simulación, incluyendo detalles como el número de pasajeros y conductores, el ritmo al que queremos añadir y eliminar estos clientes en los sistemas Lyft, el área geográfica en la que queremos que se generen, etc. Una configuración de simulación más o menos se parece a esto:

{
"nombre": "chicago",
"configuracion_de_cliente": {
"region": "chicago",
"porcentaje_usuario_cierra_aplicacion_despues_de_verificar_precio": 1,
"porcentaje_usuario_cancela_despues_de_acceptar_viaje": 10,
"porcentaje_conductor_cancela_despues_de_aceptar_viaje": 5,
},
"composicion_de_cliente": [
{
"tipo_de_cliente": "usuario",
"numero": 50,
"comportamientos": {
"viaje_compartido": 25,
"viaje_estandar": 65
"viaje_de_lujo": 5,
"viaje_de_lujo_suv": 5,
}
},
{
"tipo_de_cliente": "conductor",
"numero": 50,
"comportamiento": {
"viaje_estandar": 100
}
}
],
}

Nuestras simulaciones están compuestas por tres inteligentes abstracciones que nombramos: Clientes, Acciones y Comportamientos.

Cómo interactuan Clientes, Acciones y Comportamientos

Clientes: dispositivos físicos en la red de servicios de Lyft, como una aplicación móvil o una bicicleta eléctrica

Los clientes son una representación programática de un dispositivo físico. Tenemos clientes para las aplicaciones iOS/Android del piloto y el conductor, así como para bicicletas y scooters, que tienen un dispositivo IoT on-board que se comunica con nuestros servidores. Al igual que nuestras aplicaciones, los clientes tienen estado y son el único lugar en el que escribimos el estado en nuestras simulaciones.

Exigimos que todas las solicitudes realizadas por los clientes utilicen endpoints públicos para imitar las aplicaciones móviles nativas lo más fielmente posible. Al requerir que nuestras simulaciones no utilicen puertas traseras, nos aseguramos de que sean realistas y no dejamos ningún endpoint clave de nuestro flujo principal (golden path) sin probar.

Comportamientos: interacción de los usuarios con los dispositivos — un árbol de decisión probabilístico

Los comportamientos son árboles de decisión probabilísticos que pueden considerarse una representación programática del usuario que utiliza físicamente el cliente (aplicación o dispositivo móvil). Esta abstracción es donde el poder de la permutación nos permite descubrir la clase de casos de uso inesperados en los servicios de Lyft que previamente han causado incidentes atroces y difíciles de descubrir. Por ejemplo, podemos configurar que haya un 5% de posibilidades de que un conductor cancele un trayecto después de haber sido emparejado con un pasajero, o que un pasajero se conecte sólo para comprobar los precios de los trayectos y posteriormente cierre la aplicación el 50% de las veces. Con cada nueva posibilidad introducida, el número de escenarios posibles cubiertos crece exponencialmente.

Los comportamientos inspeccionan el estado escrito en un Cliente a través de una Acción y eligen probabilísticamente qué hacer a continuación. Por ejemplo, un usuario que acaba de abrir la aplicación Lyft podría optar por cambiar la configuración, comprobar los precios de viaje, solicitar un viaje, cerrar la sesión, y así sucesivamente. Cualquier decisión tomada aquí abriría un nuevo conjunto de posibles elecciones, revelando así la estructura tipo árbol de un comportamiento.

Puede ser más fácil pensar en los Comportamientos como “escenarios” que un Cliente ejerce en una simulación. Estos Comportamientos no siguen un pequeño conjunto de escenarios limitados de punto a punto, sino que se ramifican probabilísticamente entre diferentes flujos. Como resultado, podemos influir en las probabilidades de que se produzca un determinado resultado a través de las configuraciones de la simulación.

Debido a la complejidad de este sistema, mantener el realismo de estos flujos y llamadas de red depende de que los propietarios de los servicios mantengan actualizadas las integraciones. Este mantenimiento es a la vez positivo y negativo: requiere trabajo manual, pero también garantiza que los propietarios de los servicios sean proactivos a la hora de probar sus servicios y funciones. Ha permitido a Lyft cambiar la cultura de ingeniería, alejándose del tratamiento reactivo de incidentes y, en su lugar, ser abrumadoramente proactivo. Este cambio no se produjo de la noche a la mañana y requirió un esfuerzo coordinado de documentación, comunicación y educación.

Acciones: el resultado de la acción de un usuario al interactuar con un dispositivo — casi siempre una solicitud de red.

Las acciones son el lugar donde los ingenieros de Lyft comúnmente contribuyen a la cobertura de sus servicios. Los clientes realizan acciones. La mayoría de las acciones son una sola llamada de red, como “RequestRide” (solicitar viaje) o “AcceptRide” (aceptar viaje). Esto significa que escribiendo sólo unas pocas líneas de código en una Acción, el propietario del servicio puede empezar inmediatamente a enviar tráfico sintético a cualquier endpoint de sus servicios.

Las acciones también son muy configurables. A través de un simple bloque de configuración, un ingeniero dicta cuándo se puede invocar una Acción. Por ejemplo, podemos definir que un Cliente no debe estar en un viaje para que se invoque la Acción “RequestRide (Solicitar viaje)”, o establecer que hay un 5% de probabilidad de que cada vez que se invoque la Acción “AddCouponForRide (Agregar cupón de viaje)” se ejecute realmente la lógica de negocio que contiene. Esta capacidad de configuración nos ayuda a desplegar tráfico sintético a un nuevo endpoint de forma lenta y deliberada y a ajustar los patrones de tráfico.

Las acciones son la única abstracción del sistema que puede escribir el estado en un Cliente; por ejemplo, una acción puede sondear un endpoint para mantener el estado actual del trayecto sincronizado con el servidor, y cada vez que sondea escribe la nueva información del trayecto en el Cliente.

Un servicio adicional de manejo de recursos expone una interfaz para utilizar recursos de prueba en las simulaciones.

A medida que aumentaba la adopción de Viajes Simulados y se hacía evidente su valor, surgieron problemas con los usuarios de prueba. Crear y destruir objetos como usuarios, bicicletas y scooters es costoso en tiempo y causa efectos en cascada en los servicios posteriores. Viajes Simulados no crea ni destruye intencionalmente estos objetos que participan en una simulación. Para garantizar que estos usuarios de prueba se restablecieran a un punto de control coherente, creamos un servicio independiente conocido como Manejador de Recursos (ResourceManager). Este sistema resuelve el duro problema de manejo de datos de prueba mediante la gestión de un conjunto fijo de “recursos”, un nombre generalizado para los recursos de prueba que necesitamos para las simulaciones, como usuarios, bicicletas, scooters, etc.

El Manejador de Recursos expone una interfaz para adquirir, arrendar y restaurar la salud de los recursos (usuarios, bicicletas, scooters). Cualquier servicio que necesite recursos de prueba, como simulaciones de Viajes Simulados o Pruebas de Aceptación, puede arrendar un recurso de prueba por un tiempo determinado. Una vez que el activo de prueba deja de ser necesario o expira su contrato de arrendamiento, el gestor de recursos hace pasar el recurso a través de una “Plataforma de Restauración de Salud” (Health Restoration Platform). Se trata de un sistema sencillo que ejecuta una serie de comprobaciones y correcciones para garantizar que esos recursos se restablezcan a un buen estado conocido. Por ejemplo, esto puede eliminar usuarios de viajes huérfanos, asegurar que las tarjetas de crédito que están en el archivo sean válidas, y aprobar usuarios para conducir en un área geográfica particular.

La Plataforma de Restauración de Salud también es tratada como una interfaz en la que los propietarios de servicios pueden aportar nuevas comprobaciones y correcciones en caso de que su simulación mute el estado de los objetos subyacentes.

El servicio escala inteligentemente para satisfacer la demanda de simulaciones de gran escala

La arquitectura de este sistema es interesante y bastante diferente del enfoque de microservicio flask/gunicorn que se emplea en gran parte en Lyft y con el que podrías estar familiarizado con otros microservicios más convencionales. En su lugar, Viajes Simulados hace un uso extensivo de la biblioteca asyncio de Python a través de trabajadores (workers) y el bucle de eventos (event loop) con el fin de aprovechar la concurrencia en el código. Esto permite que el servicio se adapte a los grandes volúmenes de tráfico necesarios para ejecutar grandes simulaciones mientras se hace un uso eficiente de la potencia cómputo. Además, este diseño significa que nuestros trabajadores de simulación reparten dinámicamente el trabajo entre todos los nodos (pods) del clúster para equilibrar la carga — ningún servidor que ejecute simulaciones será responsable de una carga de trabajo mayor o menor que otro.

Ofrecemos un producto hermano que utiliza algunas de las mismas abstracciones y código de Viajes Simulados — nuestra plataforma de pruebas deterministas, conocida como “Marco de Pruebas de Aceptación” (Acceptance Test Framework), puede consultarse a detalle en nuestra entrada de blog aquí (en inglés).

Lyft utiliza Viajes Simulados para probar el rendimiento en entornos de producción y pruebas

Las pruebas de carga planificadas en producción añaden tráfico simulado a los sistemas en tiempo real de Lyft, revelando exactamente cómo responderían durante un pico de actividad en el mundo real.

Realizamos pruebas de carga de producción periódicas. Previo a los picos de actividad, modificamos los objetivos de estas pruebas para que se parezcan, y en la mayoría de los casos superen con creces, a los patrones de tráfico esperados durante los próximos eventos. Históricamente, hemos medido los objetivos de nuestras pruebas de carga en cierres de viajes por minuto (ride drop-offs per minute), aunque en nuestras previsiones también intervienen muchas otras métricas. Nuestras pruebas de carga de producción se realizan según un calendario y están altamente automatizadas. Antes de iniciar una prueba de carga, nuestro bot de automatización envía un mensaje de Slack con información sobre la duración prevista, enlaces para observar la prueba y una mención “@” al conductor de la prueba de carga de guardia.

El conductor de la prueba de carga es la persona directamente responsable que tiene autoridad para detener una prueba en caso de incidente externo. Aunque las pruebas de carga están muy automatizadas, mantenemos a una persona involucrada durante las pruebas de carga, ya que una prueba de carga fuera de control podría tener fácilmente un gran impacto en el negocio y el factor humano es el elemento adaptable de los sistemas complejos.

Existe una interfaz que permite ajustar con precisión tanto la forma como el tamaño de los patrones de tráfico durante estas pruebas. Esto significa que podemos realizar una prueba de carga que, por ejemplo, imite Halloween 2019 o modele la carga prevista para un evento próximo como el domingo del Super Bowl. O podemos generar miles de usuarios en un área pequeña que se conectan al mismo tiempo para comprobar los precios de los viajes, simulando un evento como el de miles de personas intentando salir de Times Square en Nueva York justo después de la medianoche de víspera de Año Nuevo.

Nuestras pruebas de carga inyectan usuarios simulados en los sistemas de producción de Lyft, aumentando hasta lograr la carga deseada, la cual se aproxima a la previsión del tráfico esperado para el siguiente pico de actividad. Una vez que alcanzamos la carga objetivo, la mantenemos durante un tiempo, lo que internamente se conoce como “remojo”, antes de reducirla de nuevo y, en última instancia, eliminar todos los usuarios simulados de Lyft.

Mientras se ejecutan las pruebas de carga, los ingenieros en guardia de toda la organización recibirán información inmediata de nuestras plataformas robustas de observabilidad. Si la prueba de carga afecta a la salud de sus sistemas, esto se verá rápidamente a través de una métrica de negocio u objetivo de nivel de servicio (SLO, “service level objective”), y los equipos apropiados responderán a cualquier degradación o comportamiento inesperado en un servicio tal y como lo harían durante un pico de actividad real. Hay una distinción importante: a diferencia de un evento de pico real, una prueba de carga puede detenerse inmediatamente a petición de cualquier persona de la empresa.

Sin entrar a detalle, tenemos implementados sistemas estrictos para garantizar que los datos de prueba no contaminen nuestras métricas de negocio.

La carga constante en el ambiente de pruebas revela problemas de escenarios complejos que serían difíciles de encontrar manualmente

Los Viajes Simulados proporcionan un valor increíble a Lyft también fuera del ambiente de producción. En los ambientes de prueba, proporcionan carga 24/7/365 a cualquier servicio que use la plataforma. Debido a que utilizamos las tasas de éxito para medir la salud del servicio en Lyft, el tráfico es necesario para generar una referencia de la tasa de éxito. Esto significa que los propietarios de servicios que ejecutan simulaciones en el ambiente de pruebas pueden detectar problemas en un entorno de pre-producción en un ciclo de retroalimentación rápido antes de liberar su código a la producción. En la práctica, esto significa que el código que se despliega en el entorno de pruebas hará saltar rápidamente las alarmas si hay errores o malas configuraciones sin necesidad de realizar ninguna prueba manual.

El valor de tener una solución configurable y probabilística supera los inconvenientes

Los viajes simulados son una herramienta poderosa que ha tenido un impacto positivo a lo largo de los años en Lyft.

  • Junto a otros equipos de infraestructura, este producto ha ayudado a liderar un cambio de cultura de ingeniería durante varios años, alejándose de los patrones reactivos y acercándose al diseño proactivo de sistemas de “esperar que todo falle” en su lugar.
  • A través de programas de mantenimiento total (TPM, “Total Productive Maintenance”) dirigidos para la preparación de picos de actividad, “brown bags”, actualizaciones en plantillas de documentos de diseño técnico, documentación y otras iniciativas educativas, este sistema está ampliamente disponible y ha sido adoptado por cientos de propietarios de servicios a través de Lyft.
  • Los propietarios de servicios pueden rápida y fácilmente agregar carga a un endpoint específico en sus sistemas.
  • El tráfico sintético no tiene que ser una copia perfecta del tráfico real. Con un realismo lo suficientemente bueno, podemos estresar nuestros servicios de producción e identificar anomalías y cuellos de botella.
  • Los propietarios de los servicios pueden ver cómo sus servicios se desempeñan bajo presión y estarán más preparados para momentos en los que el tráfico sea real y no pueda ser detenido con el click de un botón.
  • Tener tráfico constante en el ambiente de pruebas nos permite saber que los cambios en el código probablemente no degradarán o romperán la experiencia de los usuarios, sin desplegar los cambios en producción.
  • Dado que no hay necesidad de ejecutar pruebas de carga en el ambiente de pruebas, nuestro ambiente de pruebas no necesita ser una copia perfecta de la infraestructura en producción (con múltiples disponibilidades de zonas, etc.)
  • La naturaleza probabilística de nuestras simulaciones nos permite probar comportamientos emergentes. Esto significa que no solo probamos un número limitado de escenarios predefinidos, sino también descubrir escenarios poco comunes sin definirlo explícitamente en el código.
  • Cuando probamos o desplegamos nueva infraestructura, usamos viajes simulados para garantizar que la nueva infraestructura pueda manejar los niveles de tráfico en producción.

Una vez dicho esto, también existen algunos inconvenientes significativos:

  • La curva de aprendizaje de esta herramienta es compleja para los ingenieros que trabajan en la herramienta.
  • El hecho de que esta herramienta sea a medida significa que no podemos contratar personas con experiencia en la misma.
  • La integración con los servicios requiere seguimiento y mantenimiento.

Los viajes simulados llegarán a ser completamente automatizados y ofrecer oportunidades más robustas de prueba.

Algunos planes futuros para viajes simulados son:

  • Más seguimiento de las métricas de negocio (número de viajes, ingresos, etc.) en lugar de sólo el éxito de las acciones y los errores del servidor. Esto dará a los desarrolladores más confianza de que sus cambios no tienen un impacto negativo en los resultados.
  • Permitir a los desarrolladores enviar un porcentaje del tráfico de pruebas a través de una rama de funciones (feature branch) no integrada o un servicio local en ejecución, tal que puedan enviar tráfico real a sus servicios mientras desarrollan y depuran una funcionalidad.
  • Eliminar la necesidad de un conductor humano como parte de la prueba de carga automatizada.

El rendimiento de Lyft durante picos de actividad está preparado para el futuro

Realizar pruebas de carga a sistemas complejos es un problema difícil. Hemos encontrado la mayor fidelidad en producción. Si bien existe cierto riesgo al realizar pruebas de carga de esta forma, este riesgo puede ser mitigado con buenas herramientas y protecciones. La plataforma de viajes simulados ha sido decisiva al darnos confianza para que nuestros sistemas puedan lidiar con un gran crecimiento y picos de actividad, pues no hay un mejor lugar para probar cómo tu sistema se mantendrá bajo carga real que en producción.

Como siempre, ¡Lyft está contratando! Si te apasiona desarrollar sistemas vanguardistas, únete a nuestro equipo.

--

--