Este post foi escrito por Colin Weld e Connor Adams. Leia-o aqui ou em nosso blog.
Na Modal, construímos sandboxes, entre outras coisas. Agentes rodam em sandboxes, e agentes estão dominando o software. Hoje, a Modal executa milhões de sandboxes por dia, suporta até cinquenta mil sandboxes simultâneas por cliente e oferece suporte a uma variedade de casos de uso em escala, desde aprendizado por reforço até agentes em segundo plano.
Cada vez mais, nossos usuários precisam de mais e mais sandboxes, criadas em taxas cada vez mais altas. O aprendizado por reforço pode exigir a execução de milhões de sandboxes simultaneamente e a criação de picos de centenas de milhares de sandboxes no início das implantações. Da mesma forma, os agentes exigem cada vez mais escala massiva e altas taxas de criação simultânea para lidar com picos de tráfego.
Nossa plataforma de sandbox existente é muito boa, mas não foi projetada para essas escalas; nem qualquer outra solução existente. Somos obcecados por escala e desempenho, e queremos que nossa infraestrutura acelere o crescimento dos agentes, não adicione atrito. Então, voltamos à prancheta.
Nos últimos meses, reconstruímos nossa plataforma principal de sandbox do zero, tanto para escala quanto para confiabilidade. Em nosso novo sistema, os usuários podem executar milhões de sandboxes simultaneamente e criar dezenas de milhares de sandboxes por segundo. Removemos todos os gargalos centrais do nosso plano de controle, para que não haja limites práticos de escalabilidade, e otimizamos cada parte do agendamento e inicialização de contêineres, simplificando o caminho de agendamento para uma camada de balanceadores de carga que criam contêineres diretamente em nossa frota de workers.
Como demonstração do que nossa plataforma é capaz, executamos um milhão de sandboxes simultaneamente, criando todo o 1 milhão em menos de um minuto.

Evidência de que podemos executar muitas sandboxes.
Por que a maioria das soluções não escala
Executar 1 milhão de sandboxes leva qualquer plataforma de contêineres ao limite, tanto pelo número absoluto de contêineres, mas também porque executar tantas sandboxes exige dezenas de milhares de nós de computação. Haverá muitas operações que são O(contêineres), O(nós) ou ambos, o que fará com que as plataformas tradicionais de contêineres atinjam limites de escalabilidade.
Para o Kubernetes, por exemplo:
- o algoritmo de agendamento é O(n x p) para n nós e p pods no pior caso, e o agendamento é serializado por padrão.
- cada pod causa várias gravações no etcd (o armazenamento durável central do Kubernetes) ao longo de seu ciclo de vida, o que pode criar sérios problemas sob altas taxas de criação de pods ou alta rotatividade de pods, e o etcd não é nativamente particionável dentro de um keyspace.
- cada nó deve gravar no etcd pelo menos uma vez por intervalo de heartbeat para sinalizar atividade, então a carga base de gravação no etcd é O(nós), completamente independente da criação de pods.

Aproximação do fluxo de agendamento do Kubernetes. Novos pods são gravados no etcd (um armazenamento durável fortemente consistente) pelo servidor de API. O agendador do Kubernetes monitora novos pods não atribuídos e os atribui a nós por meio de uma chamada ao servidor de API, que novamente grava no etcd; após essa gravação ser concluída, um nó pode iniciar o pod.
O Kubernetes pode ser escalado, mas exige um trabalho sério. Para executar um grande número de nós, o etcd geralmente precisa ser reescrito ou substituído. Suportar alta taxa de transferência de agendamento exige a construção de um sistema complexo de scatter-gather para paralelizar o algoritmo de agendamento enquanto ainda mantém uma única fonte da verdade para o estado do pod. Particionamento e paralelização não são fáceis por padrão, porque o Kubernetes depende de consistência forte como espinha dorsal de seu design.
A arquitetura de sandbox original da Modal tem problemas semelhantes. Assim como o Kubernetes, dependemos de consistência forte em todo o nosso backend, portanto, criar e agendar sandboxes exige coordenação global e gravações O(sandboxes) no Postgres, que não podemos particionar trivialmente.

Arquitetura original do plano de controle de sandbox da Modal. Quando as sandboxes são criadas, elas são colocadas em uma fila e gravadas no Postgres. O agendamento é otimista e executado em paralelo, com coordenação central necessária para evitar conflitos. Atribuir uma sandbox a um worker (nó de computação) requer uma gravação adicional no Postgres.
Como não construímos sobre o Kubernetes, conseguimos escalar muitas partes deste sistema. Por exemplo, o agendamento é paralelizado por padrão, o que nos permite alcançar taxas de criação de sandbox em pico muito altas. Mas, à medida que escalávamos para números cada vez maiores de nós e sandboxes, continuamente encontrávamos novos gargalos decorrentes de operações que eram O(sandboxes) ou O(nós), mas não eram fáceis de escalar horizontalmente.
Por exemplo, executamos um workflow durável para cada sandbox que termina, então altas taxas de rotatividade de sandboxes criariam enormes acúmulos de eventos. Constantemente esbarávamos em RPCs chamados a uma taxa de O(sandboxes) que causavam problemas de carga inesperados em todo o sistema. E o número absoluto de nós necessários para executar um grande número de sandboxes causava múltiplos problemas downstream no gerenciamento de nós e no auto-scaling. Por fim, mesmo que pudéssemos contornar isso, deixar uma instância não particionada do Postgres no caminho crítico de toda criação e agendamento de sandboxes havia se mostrado uma má ideia.
Desbloqueando escala infinita
Rapidamente percebemos que alcançar a escala desejada exigia repensar nossa arquitetura do zero. Queremos executar milhões de sandboxes e criar dezenas de milhares de sandboxes por segundo, o que exige propriedades de escalabilidade muito melhores do que qualquer coisa existente. Em vez de tentar evoluir o que tínhamos, acreditamos que o caminho mais rápido e limpo era começar do zero.
Para otimizar para escala, decidimos que tudo que gerasse carga O(sandboxes) ou O(nós) deve ser escalável horizontalmente por padrão, o caminho de criação de sandbox deve ser o mais simples possível, e todo o resto deve ser secundário. A solução a que chegamos é notavelmente diferente dos sistemas existentes. Abandonamos completamente qualquer tipo de coordenação central e trocamos consistência global por escalabilidade e desempenho em todos os lugares no caminho crítico para executar e criar sandboxes. Veja como funciona:
- Em vez de um único agendador serializado, executamos uma frota de servidores de agendamento que lidam com solicitações de criação de sandbox simultaneamente. Para lidar com uma solicitação de criação, um servidor de agendamento executa um algoritmo de agendamento rápido contra dados em cache na memória. O resultado é que o agendamento escala horizontalmente e se parece mais com balanceamento de carga do que com agendamento tradicional de contêineres.
- Em vez de um armazenamento de dados durável central atuando como fonte da verdade para o estado da sandbox e do worker, que é como a maioria das plataformas de contêineres funciona, cada worker em nosso novo sistema é sua própria fonte da verdade. Workers publicam seu estado periodicamente em um stream Redis. Os servidores de agendamento consomem esse estado de forma assíncrona e o usam para tomar decisões de agendamento. Uma vez que um servidor de agendamento decide em qual worker criar uma sandbox, ele contata o worker diretamente via RPC para solicitar a criação de uma sandbox. Os workers aceitam a solicitação de agendamento se tiverem recursos livres ou a rejeitam caso contrário.
- Não temos armazenamentos de dados no caminho crítico de criação de sandbox, o que melhora a escalabilidade e a confiabilidade. Embora precisemos gravar metadados e resultados de sandbox em armazenamento durável, fazemos isso amplamente de forma assíncrona.
- Além das criações de sandbox, não temos RPCs que sejam O(sandboxes). Workers agrupam mensagens de controle para múltiplas sandboxes em RPCs únicos, no espírito das ideias do design orientado a dados.

Nosso design final, a primeira vez que o esboçamos no quadro branco.

Caminho de criação de sandbox na arquitetura de sandbox v2 da Modal. As solicitações de criação de sandbox são tratadas por servidores de agendamento escalados horizontalmente, que então selecionam um worker com um algoritmo rápido de balanceamento de carga em memória e contactam o worker (nó de computação) diretamente para criar uma sandbox. Os objetos de sandbox são armazenados no Redis, mas não no caminho crítico.
O resultado é que o caminho de criação de sandbox requer apenas dois saltos de rede e uma operação de CPU barata. Não há gargalos centrais ou custos de coordenação, nenhum ponto único de falha e, consequentemente, nenhum teto prático para a escala agregada de sandbox ou taxa de transferência de criação de sandbox. Podemos adicionar mais agendadores ou workers conforme necessário. O gargalo mais iminente é que todos os workers publicam estado em um único stream Redis, mas os testes de carga sugerem que isso permanece viável até bem mais de 100.000 workers; e não dependemos de ordenação no stream de qualquer forma, então seria fácil adicionar mais streams. Por design, evitamos os problemas que impedem as soluções existentes de escalar.
Construir esta solução não foi fácil! Todo o processo de desenvolvimento levou meses de trabalho, abrangendo a maioria dos principais sistemas em nosso backend. Passamos horas em quadros brancos. Quatro de nós se mudaram para uma casa alugada em Miami Beach para construir um protótipo do novo sistema que queríamos, sem distrações. Passamos oito dias escrevendo código até não aguentarmos mais fisicamente, jogando xadrez rápido para nos recuperar, pulando no oceano e depois voltando direto para o código, lutando para deixar nosso novo sistema limpo e funcional.

Nosso melhor engenheiro relaxando em Miami Beach.
Depois que conseguimos fazer as peças principais funcionarem (e voltamos para Nova York), também precisamos reimplementar cada recurso de Sandbox e toda a observabilidade de Sandbox sobre nosso novo sistema. Este projeto também exigiu mudanças em nossa pilha principal de gerenciamento de workers, bem como em nosso runtime de contêineres. Por exemplo, um problema interessante que encontramos é que nossos novos agendadores de sandbox podiam enviar contêineres para os workers tão rapidamente que muitos contêineres iniciando ao mesmo tempo competiriam pelo lock rtnl no kernel do Linux ao configurar as regras de rede do contêiner e levariam dezenas de segundos para iniciar, então tivemos que mudar nossa configuração de rede de contêineres para sandboxes apenas para que nossos workers não explodissem quando inundados com criações de sandbox.
Como nosso desempenho se compara
Fizemos benchmark do nosso sistema criando 1 milhão de sandboxes o mais rápido possível. Em um alto nível, podemos criar um milhão de sandboxes em menos de um minuto, onde o principal gargalo é o próprio benchmark. O tempo individual de sandbox até a interatividade permanece consistentemente baixo, e não vemos degradações reais com a escala.

Distribuição e eCDF das solicitações de criação de sandbox. Uma solicitação de criação de sandbox retorna quando nossos servidores de agendamento atribuíram com sucesso uma sandbox a um worker e ela começou a ser iniciada.
Acreditamos que isso é esperado, dado nosso design. Não há coordenação no caminho de agendamento, então o agendamento deve permanecer muito rápido, independente da concorrência e escala. No que nos diz respeito, não temos limites sérios para agendamento simultâneo de sandbox ou escala fora da capacidade disponível, e gerenciar capacidade é algo que já fazemos bem.

Gráfico de dispersão de 10 mil tempos de inicialização de sandbox do nosso teste de 1M de sandboxes, selecionados aleatoriamente do 1 milhão de sandboxes.
Os tempos de inicialização de sandbox em nosso novo sistema (a latência desde quando o cliente tenta criar uma sandbox pela primeira vez até quando a sandbox pode executar o código do usuário) são inferiores a meio segundo na mediana e permanecem sólidos em escala. Eles também são substancialmente mais rápidos do que em nosso sistema antigo, em grande parte porque o agendamento é muito mais rápido — agora leva apenas dezenas de milissegundos. A cauda longa da latência é um pouco maior do que gostaríamos. Atribuímos grande parte dessa cauda à contenção do kernel e de rede (incluindo a contenção do lock rtnl mencionada anteriormente) quando muitas sandboxes iniciam simultaneamente no mesmo worker, e estamos trabalhando para reduzi-la. Além disso, a cauda em escala é real. Esperamos que isso melhore à medida que otimizamos o caminho de inicialização do contêiner.
No geral, estamos muito satisfeitos com esses números de desempenho. À medida que os agentes dominam o mundo, claramente podemos escalar com eles.
Veja você mesmo
Em breve, este novo sistema será a base de todo o agendamento de sandbox na Modal, mas já está disponível em Beta. Você pode optar por ele com uma simples alteração em seu código. Se precisar executar muitas sandboxes, experimente e fale conosco!
Agradecimentos
Muitas pessoas contribuíram com sangue, suor e lágrimas para este projeto. Nosso POC em Miami foi construído por Colin Weld (eu), Daniel Shaar, Walter Tang e Gleb Posobin, e depois levado à produção por Walter, Colin, Connor Adams, Akshay Balwally, Tom Wildenhain, Scott Hao e Taylor Baldwin.





