Este post fue escrito por Colin Weld y Connor Adams. Léelo aquí o en nuestro blog.
En Modal, construimos sandboxes, entre otras cosas. Los agentes se ejecutan en sandboxes, y los agentes se están comiendo el software. Hoy, Modal ejecuta millones de sandboxes por día, soporta hasta cincuenta mil sandboxes concurrentes por cliente, y respalda una variedad de casos de uso a escala, desde aprendizaje por refuerzo hasta agentes en segundo plano.
Cada vez más, nuestros usuarios requieren más y más sandboxes, creados a tasas cada vez más altas. El aprendizaje por refuerzo puede requerir ejecutar millones de sandboxes de forma concurrente, y generar ráfagas de cientos de miles de sandboxes al inicio de los despliegues. Del mismo modo, los agentes exigen cada vez más una escala masiva y altas tasas de creación concurrente para manejar picos de tráfico.
Nuestra plataforma de sandboxes existente es realmente buena, pero no fue diseñada para estas escalas; tampoco lo es ninguna otra solución existente. Somos obsesivos con la escala y el rendimiento, y queremos que nuestra infraestructura acelere el crecimiento de los agentes, no que añada fricción. Así que volvimos a la pizarra.
En los últimos meses, hemos reconstruido nuestra plataforma central de sandboxes desde cero, tanto para escala como para confiabilidad. En nuestro nuevo sistema, los usuarios pueden ejecutar millones de sandboxes de forma concurrente y crear decenas de miles de sandboxes por segundo. Hemos eliminado todos los cuellos de botella centrales de nuestro plano de control, por lo que no hay límites prácticos de escalado, y hemos optimizado cada parte de la programación y el inicio de contenedores, simplificando la ruta de programación a una capa de balanceadores de carga que crean contenedores directamente en nuestro grupo de workers.
Como demostración de lo que nuestra plataforma puede hacer, ejecutamos un millón de sandboxes de forma concurrente, creando todo el millón en menos de un minuto.

Evidencia de que podemos ejecutar muchos sandboxes.
Por qué la mayoría de las soluciones no escalan
Ejecutar 1 millón de sandboxes lleva al límite a cualquier plataforma de contenedores, tanto por la enorme cantidad de contenedores, como porque ejecutar tantos sandboxes requiere decenas de miles de nodos de cómputo. Habrá muchas operaciones que son O(contenedores), O(nodos), o ambas, lo que hará que las plataformas tradicionales de contenedores alcancen límites de escalado.
Para Kubernetes, por ejemplo:
- el algoritmo de programación es O(n x p) para n nodos y p pods en el peor de los casos, y la programación está serializada por defecto.
- cada pod genera múltiples escrituras en etcd (el almacén durable central de Kubernetes) durante su ciclo de vida, lo que puede crear serios problemas bajo altas tasas de creación de pods o alta rotación de pods, y etcd no se puede fragmentar de forma nativa dentro de un espacio de claves.
- cada nodo debe escribir en etcd al menos una vez por intervalo de latido para indicar que está vivo, por lo que la carga de escritura base de etcd es O(nodos), completamente independiente de la creación de pods.

Aproximación del flujo de programación de Kubernetes. Los nuevos pods se escriben en etcd (un almacén durable fuertemente consistente) a través del servidor API. El programador de Kubernetes monitorea nuevos pods no asignados, y los asigna a nodos mediante una llamada al servidor API, que nuevamente escribe en etcd; después de que esta escritura se completa, un nodo puede iniciar el pod.
Kubernetes se puede escalar, pero requiere trabajo serio. Para ejecutar un gran número de nodos, etcd generalmente debe ser reescrito o reemplazado. Soportar un alto rendimiento de programación requiere construir un sistema complejo de scatter-gather para paralelizar el algoritmo de programación mientras se mantiene una única fuente de verdad para el estado de los pods. La fragmentación y paralelización no es fácil por defecto, porque Kubernetes se basa en la consistencia fuerte como columna vertebral de su diseño.
La arquitectura original de sandboxes de Modal tiene problemas similares. Al igual que Kubernetes, dependemos de una consistencia fuerte en todo nuestro backend, por lo que crear y programar sandboxes requiere coordinación global, y O(sandboxes) escrituras en Postgres, que no podemos fragmentar trivialmente.

Arquitectura original del plano de control de sandboxes de Modal. Cuando se crean sandboxes, se colocan en una cola y se escriben en Postgres. La programación es optimista y se ejecuta en paralelo, con coordinación central necesaria para evitar conflictos. Asignar un sandbox a un worker (nodo de cómputo) requiere una escritura adicional en Postgres.
Debido a que no construimos sobre Kubernetes, hemos podido escalar muchas partes de este sistema. Por ejemplo, la programación está paralelizada por defecto, lo que nos permite alcanzar tasas de creación de sandboxes muy altas en ráfagas. Pero a medida que escalamos a números cada vez mayores de nodos y sandboxes, nos encontramos continuamente con nuevos cuellos de botella derivados de operaciones que eran O(sandboxes) u O(nodos) pero no eran fáciles de escalar.
Por ejemplo, ejecutamos un workflow durable para cada sandbox que finaliza, por lo que las altas tasas de rotación de sandboxes creaban enormes acumulaciones de eventos. Nos encontrábamos repetidamente con RPCs llamados a una tasa de O(sandboxes) que causaban problemas de carga inesperados en todo el sistema. Y la enorme cantidad de nodos necesarios para ejecutar un gran número de sandboxes causaba múltiples problemas secundarios en la gestión de nodos y el autoescalado. Por último, aunque podíamos solucionarlo, dejar una instancia de Postgres sin fragmentar en la ruta crítica de toda creación y programación de sandboxes había demostrado ser una mala idea.
Desbloqueando escala infinita
Rápidamente nos dimos cuenta de que alcanzar la escala que queríamos requería repensar nuestra arquitectura desde cero. Queremos ejecutar millones de sandboxes y crear decenas de miles de sandboxes por segundo, lo que requiere propiedades de escalado mucho mejores que cualquier cosa existente. En lugar de intentar evolucionar lo que teníamos, creímos que el camino más rápido y limpio era empezar de nuevo.
Para optimizar la escala, decidimos que todo lo que generara carga O(sandboxes) u O(nodos) debía ser horizontalmente escalable por defecto, la ruta de creación de sandboxes debía ser lo más simple posible, y todo lo demás debía ser secundario. La solución a la que llegamos es notablemente diferente a los sistemas existentes. Prescindimos por completo de cualquier tipo de coordinación central, e intercambiamos consistencia global por escalabilidad y rendimiento en todas las rutas críticas para ejecutar y crear sandboxes. Así es como funciona:
- En lugar de un único programador serializado, ejecutamos un grupo de servidores de programación que manejan solicitudes de creación de sandboxes de forma concurrente. Para manejar una solicitud de creación, un servidor de programación ejecuta un algoritmo de programación rápido contra datos en caché en memoria. El resultado es que la programación escala horizontalmente, y se parece más a un balanceo de carga que a la programación tradicional de contenedores.
- En lugar de un almacén de datos central y durable que actúe como fuente de verdad para el estado de los sandboxes y workers, que es como funcionan la mayoría de las plataformas de contenedores, cada worker en nuestro nuevo sistema es su propia fuente de verdad. Los workers publican su estado periódicamente en un stream de Redis. Los servidores de programación consumen este estado de forma asíncrona y lo usan para tomar decisiones de programación. Una vez que un servidor de programación decide en qué worker crear un sandbox, contacta al worker directamente mediante RPC para solicitar que se cree un sandbox. Los workers aceptan la solicitud de programación si tienen recursos libres, o la rechazan en caso contrario.
- No tenemos almacenes de datos en la ruta crítica de creación de sandboxes en absoluto, lo que mejora la escalabilidad y la confiabilidad. Aunque necesitamos escribir metadatos y resultados de sandboxes en almacenamiento durable, lo hacemos en gran medida de forma asíncrona.
- Aparte de las creaciones de sandboxes, no tenemos RPCs que sean O(sandboxes). Los workers agrupan mensajes de control para múltiples sandboxes en RPCs individuales, siguiendo el espíritu de las ideas del diseño orientado a datos.

Nuestro diseño final, la primera vez que lo dibujamos en la pizarra.

Ruta de creación de sandboxes en la arquitectura v2 de sandboxes de Modal. Las solicitudes de creación de sandboxes son manejadas por servidores de programación escalados horizontalmente, que luego seleccionan un worker con un algoritmo de balanceo de carga rápido en memoria, y contactan al worker (nodo de cómputo) directamente para crear un sandbox. Los objetos de sandbox se almacenan en Redis, pero no en la ruta crítica.
El resultado es que la ruta de creación de sandboxes requiere solo dos saltos de red y una operación de CPU barata. No hay cuellos de botella centrales ni costos de coordinación, ningún punto único de falla y, en consecuencia, no hay un límite práctico para la escala agregada de sandboxes o el rendimiento de creación de sandboxes. Podemos agregar más programadores o workers según sea necesario. El cuello de botella más inminente es que todos los workers publican estado en un único stream de Redis, pero las pruebas de carga han sugerido que esto sigue siendo viable hasta mucho más de 100,000 workers; y de todos modos no dependemos del orden en el stream, por lo que sería fácil agregar más streams. Por diseño, evitamos los problemas que impiden que las soluciones existentes escalen.
¡Construir esta solución no fue fácil! Todo el proceso de desarrollo ha tomado meses de trabajo, abarcando la mayoría de los sistemas principales de nuestro backend. Pasamos horas frente a pizarras. Cuatro de nosotros nos instalamos en una casa de alquiler en Miami Beach para construir un prototipo del nuevo sistema que queríamos, sin distracciones. Pasamos ocho días escribiendo código hasta que físicamente no podíamos más, jugando ajedrez rápido para recuperarnos, saltando al océano, y luego volviendo directo al código, luchando para que nuestro nuevo sistema quedara limpio y funcional.

Nuestro mejor ingeniero relajándose en Miami Beach.
Una vez que logramos que las piezas centrales funcionaran (y regresamos a Nueva York), también necesitamos reimplementar cada característica de Sandbox y toda la observabilidad de Sandbox sobre nuestro nuevo sistema. Este proyecto también requirió cambios en nuestra pila central de gestión de workers, así como en nuestro runtime de contenedores. Por ejemplo, un problema interesante que encontramos es que nuestros nuevos programadores de sandboxes podían enviar contenedores a los workers tan rápido que muchos contenedores iniciándose al mismo tiempo competían por el bloqueo rtnl en el kernel de Linux al configurar las reglas de red de los contenedores, y tardaban decenas de segundos en iniciarse, por lo que tuvimos que cambiar la configuración de red de nuestros contenedores para sandboxes solo para que nuestros workers no explotaran cuando se inundaran con creaciones de sandboxes.
Cómo se compara nuestro rendimiento
Evaluamos nuestro sistema iniciando 1 millón de sandboxes lo más rápido posible. A alto nivel, podemos crear un millón de sandboxes en menos de un minuto, donde el principal cuello de botella es la propia prueba de referencia. El tiempo individual de sandbox hasta la interactividad se mantiene consistentemente bajo, y no vemos degradaciones reales con la escala.

Distribución y eCDF de las solicitudes de creación de sandboxes. Una solicitud de creación de sandbox retorna cuando nuestros servidores de programación han asignado exitosamente un sandbox a un worker, y este ha comenzado a iniciarse.
Creemos que esto es esperado, dado nuestro diseño. No hay coordinación en la ruta de programación, por lo que la programación debería permanecer muy rápida independientemente de la concurrencia y la escala. Por lo que a nosotros respecta, no tenemos límites serios para la programación concurrente de sandboxes o la escala más allá de la capacidad disponible, y gestionar la capacidad es algo que ya hacemos bien.

Diagrama de dispersión de 10k tiempos de inicio de sandboxes de nuestra prueba de 1M de sandboxes, seleccionados aleatoriamente de los 1M de sandboxes.
Los tiempos de inicio de sandboxes en nuestro nuevo sistema (la latencia desde que el cliente intenta crear un sandbox por primera vez, hasta que el sandbox puede ejecutar código de usuario) son inferiores a medio segundo en la mediana, y se mantienen sólidos a escala. También son sustancialmente más rápidos que nuestro sistema antiguo, en gran parte porque la programación es mucho más rápida: ahora solo toma decenas de milisegundos. La cola larga de latencia es algo más larga de lo que nos gustaría. Atribuimos gran parte de esta cola a la contención del kernel y la red (incluida la contención del bloqueo rtnl mencionada anteriormente) cuando muchos sandboxes se inician simultáneamente en el mismo worker, y estamos trabajando para reducirla. Además, la cola a escala es real. Esperamos que esto mejore a medida que optimicemos la ruta de inicio de contenedores.
En general, estamos muy contentos con estos números de rendimiento. A medida que los agentes toman el mundo, claramente podemos escalar con ellos.
Compruébalo tú mismo
Pronto este nuevo sistema respaldará toda la programación de sandboxes en Modal, pero ya está disponible en Beta. Puedes optar por él con un simple cambio en tu código. Si necesitas ejecutar muchos sandboxes, pruébalo y ¡habla con nosotros!
Agradecimientos
Muchas personas contribuyeron con sangre, sudor y lágrimas a este proyecto. Nuestro POC de Miami fue construido por Colin Weld (yo), Daniel Shaar, Walter Tang y Gleb Posobin, y luego llevado a producción por Walter, Colin, Connor Adams, Akshay Balwally, Tom Wildenhain, Scott Hao y Taylor Baldwin.





