DEV Community

Matheus de Camargo Marques
Matheus de Camargo Marques

Posted on

Modelo de Atores da BEAM vs. Scheduler Agent Supervisor: Mera Coincidência? O que a Ericsson com Erlang já resolveu no passado?

Olá, me chamo Matheus de Camargo Marques. Ao aprofundar meus estudos em conceitos modernos de Cloud, deparei-me com o Scheduler Agent Supervisor pattern e tive uma sensação imediata de: "eu já vi isso antes".

Trazendo um contexto mais formal: se você trabalha com arquitetura de microsserviços distribuídos na nuvem moderna, certamente já se deparou com a necessidade de orquestrar sagas, gerenciar falhas transientes e garantir consistência eventual. O padrão Scheduler Agent Supervisor, documentado pelo Azure Architecture Center e teorizado originalmente por Clemens Vasters, propõe uma solução robusta combinando filas, bases de estado NoSQL e workers serverless.

Contudo, quando dissecamos os pilares de baixo nível da máquina virtual BEAM (o tempo de execução do Erlang e Elixir criado pela Ericsson na década de 1980), deparamo-nos com um fato estarrecedor: a nuvem moderna está recriando em macroescala de rede exatamente a mesma arquitetura que a BEAM já executa de forma nativa e em memória há mais de 40 anos. Neste artigo, analisamos desde a matemática do Modelo de Atores e os detalhes microscópicos em C da BEAM até a comparação quantitativa de latência, densidade computacional, custos financeiros e casos reais de empresas como WhatsApp, Bleacher Report e Discord.


1. O Paradoxo da Nuvem Moderna: A Reinvenção da Roda Distribuída

Em sistemas distribuídos contemporâneos, a falha não é um evento fortuito; é uma certeza estatística. Latência de rede flutuante, indisponibilidade temporária de bancos de dados, gargalos de I/O e exaustão de memória não são anomalias, mas condições operacionais cotidianas.

Para sobreviver a esse ambiente caótico, os arquitetos de nuvem abraçaram o padrão Scheduler Agent Supervisor. A proposta é elegante em teoria:

  1. O Scheduler (Agendador): Mantém o estado da transação (workflow) em uma base de dados durável e dispara etapas via filas de mensagens.
  2. O Agent (Agente): Executa uma regra de negócio isolada (ex: uma função Serverless ou container) e notifica o Scheduler sobre o sucesso.
  3. O Supervisor: Um processo que corre periodicamente em background (polling via cron) inspecionando o banco de dados em busca de tarefas órfãs ou estouradas por timeout, tomando medidas de re-tentativa (retry) ou acionando transações compensatórias.

Porém, ao implementar essa arquitetura no Azure ou AWS, o desenvolvedor precisa colar pelo menos quatro serviços gerenciados distintos (Azure Functions / AWS Lambda, Service Bus / SQS, CosmosDB / DynamoDB, e sistemas de Leader Election).

A pergunta inevitável é: Por que a engenharia de software precisa montar uma infraestrutura física tão pesada e cara para atingir a resiliência que o Erlang e a BEAM entregavam dentro de um único processo de máquina virtual nos anos 80?


2. Fundamentos Teóricos: De Carl Hewitt à Ericsson de 1986

Para entender por que a BEAM é imune à maioria das dores de cabeça dos sistemas distribuídos modernos, precisamos retornar às suas origens matemáticas e industriais.

2.1 O Modelo de Atores (Carl Hewitt, 1973)

Proposto no MIT por Carl Hewitt, Peter Bishop e Richard Steiger, o Modelo de Atores rejeitou o modelo sequencial da Máquina de Turing. Ele postula que o bloco fundamental da computação é o Ator:

  • Uma entidade isolada que encapsula estado, comportamento e processamento.
  • Comunica-se exclusivamente por passagem de mensagens assíncronas.
  • Ao receber uma mensagem, um ator só pode fazer três coisas: criar novos atores, enviar mensagens a atores conhecidos (via PID) e designar o comportamento para a próxima mensagem.

O trunfo absoluto do Modelo de Atores é a ausência total de memória compartilhada. Sem memória compartilhada, a BEAM eliminou de uma só vez as maiores pragas do desenvolvimento concorrente: trincos (locks), semáforos, condições de corrida (race conditions) e impasses circulares (deadlocks).

2.2 A Necessidade da Ericsson e os "Nine Nines"

Na década de 1980, Joe Armstrong, Robert Virding e Mike Williams pesquisavam nos laboratórios da Ericsson como construir os comutadores telefônicos (switches) da série AXE. Os requisitos eram colossais:

  • Suportar centenas de milhares de chamadas concorrentes;
  • Permitir atualização de código sem parar o sistema (hot code reloading);
  • Atingir 99,9999999% de disponibilidade (o lendário padrão de 9 noves).

A resposta foi a criação do Erlang e da plataforma OTP (Open Telecom Platform), rodando sobre a máquina virtual BEAM.

2.3 A Filosofia "Let it Crash" vs. Programação Defensiva

Na sua tese de doutorado em 2003 ("Making reliable distributed systems in the presence of software errors"), Joe Armstrong criticou severamente a programação defensiva tradicional.

Em linguagens como Java, C# ou Python, os desenvolvedores cercam o código com blocos try/catch gigantescos tentando prever exceções. O problema é que, ao capturar uma exceção imprevista, o processo continua rodando com estado volátil corrompido, gerando vazamento de memória ou corrupção de banco.

Armstrong defendeu o oposto:

"Se um processo encontrar um estado anômalo, não tente adivinhar como se recuperar. Morra imediatamente. Deixe que um processo supervisor limpo cuide da recuperação."

Como os processos BEAM são totalmente isolados, o colapso de um ator não afeta a memória dos vizinhos. O Supervisor, imune ao erro por não executar regras de negócio, detecta a morte do processo e o reinicia a partir de um estado estável e garantido.


3. Mapeamento Arquitetural: Cloud Padrão SAS vs. Primitivas BEAM

A tabela a seguir demonstra a equivalência direta entre a infraestrutura montada na nuvem e o que já vem embutido nativamente na máquina virtual BEAM:

Padrão Cloud (Azure / AWS) Equivalente Nativo BEAM (Elixir / Erlang OTP)
Agendador (Durable Functions / Step Functions) gen_statem (Máquina de Estados) / GenServer
Agente (Lambda / Azure Function / Container) Processo Ator verde em RAM (~2,4 KB de pegada)
Fila de Mensagens (Service Bus / SQS) Mailbox interna lock-free do processo BEAM
Banco de Estado (CosmosDB / DynamoDB) Heap privado local, Tabelas ETS ou Mnesia
Supervisor (Cron Polling + DLQ) Árvore de Supervisão reativa (Links e Monitors)

A Grande Diferença: Polling em Rede vs. Sinalização Reativa

No padrão de nuvem, o Supervisor precisa rodar uma rotina agendada (CRON) que faz queries periódicas no banco de dados para checar se algum agente estourou o prazo (LockedUntil). Isso gera consumo desnecessário de I/O e latência na detecção da falha.

Na BEAM, a supervisão é puramente reativa. Quando um processo filho falha, a própria máquina virtual emite instantaneamente um sinal de interrupção (:EXIT) via links ou monitors diretamente para o processo Supervisor. A recuperação ocorre em microssegundos, sem despesa com chamadas de rede ou consultas a bancos.


4. Anatomia Microscópica da BEAM: O Subsolo da VM em Código C

Para entender como a BEAM dispensa a infraestrutura pesada da nuvem, precisamos olhar para as especificações internas do seu motor. Tudo se resume a como o tempo de execução em linguagem C (erl_process.h) gerencia a memória física de forma isolada.

4.1 O Process Control Block (PCB)

Cada ator Erlang é representado internamente pela estrutura C process (referenciada pelo ponteiro c_p). Enquanto uma thread nativa de sistema operacional aloca entre 512 KB e 8 MB de memória, a criação de um processo BEAM consome apenas 327 a 338 palavras (words) — aproximadamente 2,5 a 3 KB em arquiteturas de 64 bits.

O PCB aloja os metadados essenciais:

  • id (PID): Um identificador de 28 bits que segmenta o índice da tabela de processos, um número de série de 13 bits (que impede a reutilização imediata de PIDs de processos mortos) e o nó do cluster distribuído.
  • Apontadores de Memória (stop / htop / hend): Salvaguardam o topo da pilha e do monte. Durante a execução, esses valores são mantidos diretamente nos registradores físicos da CPU através do compilador JIT (introduzido no Erlang/OTP 24, que proporcionou um ganho de performance massivo e consolidou a BEAM para tarefas de alto processamento).
  • Filas de Sinalização (sig_inq / sig_qs): A caixa de correio externa lock-free e a fila de entrada.
  • ErlOffHeap: A estrutura que ancora a lista MSO para gerenciamento de binários globais.

4.2 A Física da Memória Privada: Crescimento Bidirecional

Diferente de outras linguagens que alocam áreas soltas de memória exigindo chamadas pesadas ao SO (malloc), a BEAM aloca um único bloco contíguo de memória privada para cada processo:

+-----------------------------------------------------------------------+
|  Endereço Baixo                                       Endereço Alto   |
|  [ HEAP (Cresce para CIMA ^) ]  ---> <--- [ STACK (Cresce para BAIXO v) ]|
|  |--- htop ------------------------ stop (E) ------------------------|
+-----------------------------------------------------------------------+
                                   ^
                           Ponto de Colisão
                        (Dispara o GC Isolado)
Enter fullscreen mode Exit fullscreen mode
  • O Heap (Cresce para cima): Armazena estruturas dinâmicas como listas consulares (2 words por nó), tuplas, inteiros grandes e floats.
  • O Stack (Cresce para baixo): Armazena registradores, variáveis locais e o Ponteiro de Continuação (CP) via instrução allocate.

Quando o topo do heap (htop) colide com a base da pilha (stop), a BEAM sabe no exato milissegundo que a memória local acabou. Isso dispara uma rotina de Garbage Collection per-processo, totalmente isolada.

4.3 Garbage Collection de Cheney Per-Processo (Zero Stop-The-World)

Na JVM ou no Go, o Garbage Collector precisa analisar o estado global da aplicação, o que frequentemente aciona as temidas pausas Stop-The-World (onde toda a aplicação congela por milissegundos).

Na BEAM, não existe Garbage Collector global. Cada processo possui seu próprio GC independente baseado no Algoritmo de Cópia de Cheney:

  1. Os dados vivos são copiados do From-Space para um novo To-Space, desfragmentando listas para maximizar a localidade de cache da CPU.
  2. O espaço antigo é descartado de uma vez só.
  3. Os dados sobreviventes ultrapassam o ponteiro high_water e são promovidos ao Old Heap.

Se um processo é de curta duração (ex: atende uma requisição e morre), ele é destruído junto com toda a sua memória privada de uma só vez, sem sequer precisar acionar o Garbage Collector!

4.4 Taxonomia Avançada de Binários e a Lista MSO List

Copiar grandes payloads (como arquivos JSON de 10 MB ou imagens) entre atores destruiria a performance. Para resolver isso, a BEAM categoriza os binários em quatro tipos:

  1. Heap Binary ($\le 64$ bytes): Fica diretamente no heap privado do ator.
  2. Refc Binary ($> 64$ bytes): Fica fora dos processos, no Binary Heap Global. É gerenciado por Contagem de Referências (Reference Counting).
  3. Sub Binary: Um slice/offset apontando para um Refc Binary sem duplicar bytes.
  4. Match Context: Ponteiro de estado efêmero para Pattern Matching.

Quando dois atores trocam um binário grande (Refc Binary), a BEAM copia apenas o ProcBin (um pequeno descritor de 5 palavras no heap privado) e incrementa atomicamente o contador global. A estrutura ErlOffHeap no PCB mantém a MSO List (Mark and Sweep Object List): quando o GC privado do processo limpa o ProcBin morto, ele decrementa atomicamente a referência no Binary Heap Global.


5. Concorrência Extrema, Lock-Free e o Motor de 2000 Reduções

5.1 Fila de Entrada Lock-Free (sig_inq) e message_queue_data = :off_heap

Para suportar milhares de emissores enviando mensagens para o mesmo ator sem bloqueios, a BEAM utiliza uma fila de entrada externa totalmente lock-free (sig_inq).

Sob torrentes brutais de mensagens, a BEAM permite configurar o processo com a flag message_queue_data = :off_heap. Nesse modo, as mensagens recebidas permanecem na fila exterior, sem passar pelo Garbage Collector geracional do ator, e só são fundidas à memória privada no momento exato em que a cláusula receive faz um Pattern Match. Isso impede que servidores fiquem lentos por causa de mensagens acumuladas.

5.2 O Motor Escalonador (2000 Reduções)

Ao contrário do Go (onde o escalonador cooperativo dependia originalmente de pontos de checagem manuais) ou da JVM (que depende das threads pesadas do SO), a BEAM adota uma preempção cooperativa a nível C (process_main em beam_emu.c).

  • A BEAM aloca exatamente 1 Thread Escalonadora por núcleo físico/lógico da CPU ($N:M$).
  • Cada processo recebe um orçamento estrito de 2000 Reduções (time slice budget).
  • Cada chamada de função, BIF (Built-in Function) ou passo de GC decrementa o contador.
  • Ao chegar a 0 reduções, o processo sofre um Yield Point: salvaguarda seus registradores no Stack/PCB e cede o lugar voluntariamente na Run Queue.

Isso garante equidade perfeita e resposta Soft Real-Time: nenhuma rotina de usuário consegue monopolizar a CPU ou travar os outros atores.

5.3 Task Stealing, Dirty Schedulers e Erlang Monotonic Time

  • Task Stealing: Escalonadores ociosos roubam processos runnable das filas de núcleos sobrecarregados.
  • Dirty Schedulers (CPU_BOUND / IO_BOUND): Isolam chamadas pesadas em C/Rust (NIFs) em threads separadas, impedindo que funções C sem reduções parem o tempo de execução da VM.
  • Erlang Monotonic Time & Time Warp: A BEAM desvincula seu tempo interno do relógio POSIX de parede. Se o relógio do SO for alterado via NTP, a BEAM aplica uma dilatação imperceptível de até 1% na frequência de seu relógio interno, evitando saltos bruscos que quebrariam timeouts e cronômetros de supervisão.

6. A Metáfora Bio-Inspirada: O Sistema de Memória Cognitiva

Podemos entender a arquitetura da BEAM e o padrão Cloud através da lente da Neurociência e dos Sistemas de Memória Cognitiva:

  1. Memória de Curto Prazo (STM / Working Memory): No córtex pré-frontal, mantém a informação volátil ativa. Na BEAM, é o heap isolado do GenServer e a caixa de correio. Na nuvem, é o contexto em RAM de uma Função Lambda.
  2. Memória de Longo Prazo (LTM / Consolidada): O hipocampo consolida informações duráveis no neocórtex. Na BEAM, é o banco de dados in-memory ETS, o Mnesia ou o Postgres. Na nuvem, é o CosmosDB/DynamoDB.
  3. Homeostase e Reidratação (Supervisão e Recall): Diante de um estresse ou trauma (crash), a homeostase do organismo restaura o equilíbrio. Na BEAM, a Árvore de Supervisão captura a morte do processo, consulta a LTM (ETS/Mnesia) e reidrata o ator com o último estado consolidado em microssegundos.

7. A Matemática Implacável: Latência, Densidade e Custos Financeiros

Para o arquiteto de software, a teoria precisa ser respaldada por números. Vamos comparar as duas abordagens em três eixos críticos:

7.1 A Matemática da Latência (Tempo)

  • Nuvem (Pulos de Rede): Em uma arquitetura Cloud SAS, cada interação entre o Scheduler, a Fila, a Função Serverless e o CosmosDB cruza a rede do datacenter. Mesmo otimizado com gRPC (cerca de 130 µs), cada salto leva entre 0,5 ms e 1,0 ms. O ciclo completo de uma etapa consome de 2 ms a 50 ms.
  • BEAM (Troca de Mensagens em RAM): O envio de uma mensagem entre dois atores na mesma VM ocorre via cópia de ponteiros em RAM, levando aproximadamente 1 µs — ou seja, 1000 vezes mais rápido que a nuvem.

7.2 Densidade Computacional (Memória)

  • Modelos Tradicionais (Threads de SO): Cada thread em Java ou C# consome ~1 MB de RAM. Um servidor típico começa a passar mal com poucas dezenas de milhares de conexões simultâneas.
  • BEAM (Green Threads): Com uma pegada inicial de ~2,4 KB (309 words), um único servidor bare-metal consegue rodar confortavelmente centenas de milhares a milhões de atores simultâneos.

7.3 A Matemática dos Custos Financeiros (Dinheiro)

  • Nuvem Serverless (Faturamento Microtransacional): Você paga por cada requisição HTTP, cada invocação de Lambda, cada leitura/escrita em filas (SQS/Service Bus), cada RU (Request Unit) do CosmosDB e pelo tráfego de saída (Egress). Se a aplicação receber 50 milhões de acessos no day, a fatura estoura.
  • BEAM em Servidor Dedicado / VPS (Custo Fixo): Um monólito distribuído (Majestic Monolith) em Elixir roda em uma máquina virtual (VPS) de US$ 40/mês. Processar 1 milhão ou 1 bilhão de mensagens internamente nessa máquina custará exatamente os mesmos US$ 40 no final do mês.

8. Evidências Empíricas da Indústria (Casos de Estudo)

A superioridade teórica da BEAM é comprovada pelos casos históricos das maiores plataformas do planeta:

Plataforma Arquitetura Adotada Resultado Métrico
WhatsApp Erlang c/ Atores por Usuário + Mnesia + Kernel FreeBSD ajustado (kern.ipc.maxsockets = 2.4M) > 2 Milhões conexões ativas por servidor. Apenas 50 engenheiros para 2 bilhões de usuários.
Bleacher Report Migração de Ruby on Rails para Elixir / Phoenix Redução brutal de 150 instâncias AWS EC2 para apenas 5 servidores nos picos de tráfego.
Discord Elixir (WebSockets) + Rust NIFs (Rustler) substituindo Go no serviço Read States Latência despencou de 27.000 µs (pausas de GC do Go) para 4 µs com > 11M conexões.

8.1 WhatsApp: 2 Milhões de Conexões por Servidor

O WhatsApp sustentou mais de 2 bilhões de usuários ativos com um time de infraestrutura de apenas 50 engenheiros.

  • Como? Criando 1 Ator Erlang por usuário conectado. Com a pegada de 2 KB por processo, milhões de conexões permaneceram ativas na memória RAM.
  • Tuning de Kernel: Ajustaram o kernel FreeBSD (kern.ipc.maxsockets = 2.400.000 e kern.maxfiles = 3.000.000), atingindo o recorde histórico de mais de 2 milhões de conexões TCP ativas em um único servidor bare-metal com 85% de uso de CPU.

8.2 Bleacher Report: De 150 para 5 Servidores na AWS

O Bleacher Report sofria com picos de acessos durante transmissões de eventos esportivos. A arquitetura original em Ruby on Rails não aguentava o tráfego devido ao bloqueio do GIL (Global Interpreter Lock).

  • Ao migrar o sistema para Elixir e Phoenix, a empresa reduziu a sua infraestrutura na AWS de 150 instâncias EC2 para apenas 5 servidores, economizando milhões de dólares.

8.3 Discord: De 27.000 µs para 4 µs de Latência (Go vs. Rust/Elixir)

O Discord opera mais de 11 milhões de usuários simultâneos em WebSockets controlados pelo Elixir.

  • O serviço Read States (que rastreia mensagens lidas) foi escrito originalmente em Go. Porém, a cada 2 minutos (120s), o Garbage Collector global do Go acionava um mark-and-sweep, gerando picos de latência de 27.000 µs (27 ms).
  • A engenharia reescreveu o núcleo do serviço em Rust, integrando-o ao Elixir via NIFs (Rustler). A latência caiu de 27.000 µs para 4 µs, sem abrir mão das Árvores de Supervisão do Elixir.

9. Conclusão: Coincidência ou Sabedoria Esquecida?

O padrão Scheduler Agent Supervisor promovido pelos provedores de nuvem modernos não é uma mera coincidência — é a prova inequívoca de que os desafios da computação distribuída são universais. A nuvem chegou às mesmas conclusões que Joe Armstrong e a equipe da Ericsson chegaram em 1986.

A diferença fundamental reside no nível de abstração:

  • A Nuvem tenta resolver o problema colando peças de infraestrutura pela rede (filas, bancos de dados, lambdas, CRONs), pagando um imposto altíssimo em latência, complexidade operacional e custos financeiros.
  • A BEAM resolve o problema no nível da linguagem e do tempo de execução, utilizando a memória RAM, processos verdes, contagem de reduções e supervision trees reativas.

Se a sua empresa opera microsserviços em nuvem e sofre com custos astronômicos de Serverless, latências de rede acumuladas ou complexidade excessiva de orquestração, talvez seja hora de parar de tentar recriar a BEAM com infraestrutura de nuvem... e simplesmente começar a usar a BEAM.


Vamos Continuar a Conversa?

E você, já enfrentou desafios de latência acumulada ou custos astronômicos de infraestrutura ao orquestrar microsserviços na nuvem? Já experimentou o Modelo de Atores no Elixir ou Erlang?

Fique à vontade para deixar suas impressões, dúvidas ou relatar suas experiências nos comentários! Se quiser explorar projetos onde aplico esses conceitos na prática — desde agentes bio-inspirados e pipelines RAG até web crawlers de altíssima performance —, confira meus repositórios no GitHub ou conecte-se comigo no LinkedIn.

Top comments (0)