DEV Community

Wagner Mattei
Wagner Mattei

Posted on

Criando a melhor arquitetura possível para o site de velas virtuais do Padre Marcelo Rossi

Passei as últimas 12 horas desenhando a arquitetura perfeita para o site de velas virtuais do Padre Marcelo Rossi.
E quero compartilhar algumas reflexões.

Muita gente olha um problema como esse e pensa: “é só um CRUD, joga num Postgres, faz um cron pra apagar depois de 7 dias, deploya na Vercel.” Com todo respeito, isso é pensamento de júnior.

Vamos aos números. Brasil tem 203 milhões de habitantes. Se 1% acende uma vela por semana, são ~2 milhões de velas simultâneas em qualquer instante. Cada vela carrega: nome, data de nascimento, telefone, endereço, bairro, cidade, UF, país, e-mail, e o motivo da oração. Naïvely, isso dá ~180 bytes por registro. Com overhead de Postgres (tuple header, MVCC, alinhamento, índices), você facilmente chega a 400+ bytes para guardar informação que, bem empacotada, ocupa menos de 100. Isso é pecado. Literalmente.

A arquitetura que cheguei depois de muito café:

Linguagem:

Zero runtime, zero GC. Rust para o core, com #![no_std] nos hot paths. Considerei C, mas a santidade do código importa — queremos memory safety antes de apresentar o sistema ao Altíssimo.

Schema binário da vela (onde mora o milagre):

Cada campo foi auditado individualmente. Programador médio armazena tudo como string UTF-8 com tamanho variável. Péssimo. Cada campo tem estrutura semântica e a estrutura é exploração livre:

  • Data de nascimento: u16 representando dias desde 1900-01-01. Cobre até o ano 2079. Se Padre Marcelo ainda estiver ativo em 2079, faço migração pra u32. 2 bytes.
    • Telefone: BR tem exatamente 11 dígitos (DDD + 9 + 8). Guardado como inteiro, ocupa 34 bits. Arredondo pra 5 bytes alinhados. 5 bytes. (Sim, perdi 6 bits. Reservei pra flag de “tem WhatsApp” e outras 5 flags futuras.)
    • Cidade: IBGE cataloga 5.570 municípios brasileiros. Cabe em 13 bits. Uso u16 com código IBGE direto — permite até join com base externa sem guardar a string. 2 bytes.
    • UF: 27 estados. 5 bits. Empacotado no mesmo byte do país (abaixo).
    • País: ISO 3166-1 numeric tem ~250 códigos. 8 bits. Junto com UF num u16 compartilhado. 2 bytes totais pra UF+país.
    • Endereço + Bairro: strings, mas comprimidas com dicionário Zstd treinado em CEPs brasileiros. “Rua”, “Avenida”, “Jardim”, “Vila”, “Centro” aparecem em 90% dos registros — comprimem pra tokens de 1 byte. Média: ~26 bytes combinados.
    • Nome + e-mail: mesma compressão Zstd com dicionário de nomes/provedores brasileiros. ~25 bytes combinados.
    • Motivo da oração: esse é o campo difícil — texto livre, sem estrutura previsível. Mas há padrão: análise de amostra mostra que 70% dos motivos cabem em 40 caracteres (“saúde da minha mãe”, “aprovação no vestibular”, “pelo casamento”). Zstd comprime pra ~25 bytes. Motivos longos vão pro overflow pool (próxima seção). ~25 bytes inline.

Slot final: 128 bytes (exatamente 2 cache lines, alinhamento preservado), com header de 8 bytes (timestamp de acesão + flags) e ~88 bytes de payload comprimido + 32 bytes de padding reservado pra futuros sacramentos digitais.

Storage:

Ring buffer mmap’d em disco, append-only, TTL fixo de 7 dias. Slots de 128 bytes alinhados. Nada de Postgres, nada de MongoDB. A estrutura do problema é um ring buffer — forçar um RDBMS em cima disso é violência arquitetural.

Pecadores com dados longos (overflow handling):

E se chegar um “Maria Eduarda Gonçalves de Albuquerque Cavalcanti”, morando na “Rua Desembargador Antônio Carlos Figueiredo Jobim”, com um motivo de oração de 800 caracteres descrevendo três gerações de conflito familiar? Não podemos recusar — seria teologicamente questionável negar uma vela por prolixidade. Mas também não podemos inflar o slot de 128 bytes e destruir o alinhamento de cache line por causa de outliers.

  • Solução: overflow pool segmentado por campo. Cada campo de tamanho variável (nome, endereço, motivo) tem, no slot principal, 1 byte de marcador + 3 bytes de offset apontando pra uma heap dedicada àquele campo. Se o valor comprimido couber inline, fica inline. Se não, o marcador vira 0xFF (escolhido por motivos óbvios) e o valor real vai pra heap do respectivo campo. Separar por campo é importante: permite que o motivo longo não arraste endereço junto e vice-versa, e mantém localidade de referência nas leituras. Na prática <2% dos campos vão pro overflow pool (motivo é o mais comum). O hot path continua cache-friendly. Pecadores extensos são servidos com a mesma dignidade, apenas com uma indireção a mais — o que, convenhamos, é metáfora bonita.

Expiração:

Nada de cron job. O ring buffer é particionado em 7 buckets diários. No início de cada dia, o bucket de 7 dias atrás é simplesmente sobrescrito, e os segmentos correspondentes dos overflow pools são truncados em bloco. Expiração em O(0). As velas se apagam sozinhas, como na vida real.

Frontend:

SSR com streaming. A animação da chama é um único SVG de 400 bytes com puro — zero JavaScript. Se o fiel tem JS desabilitado, a vela ainda acende. A fé não depende do V8.

Observabilidade:

Não é necessário em software santificado.

O bottleneck do kernel (e por que tivemos que escrever nosso próprio OS):

No benchmark local eu estava saturando em 400K velas/segundo, e ao instrumentar descobri o óbvio: o kernel do Linux estava no caminho crítico. Syscalls, context switches, TLB flushes, scheduler CFS decidindo dar tempo de CPU pra um systemd-timesyncd no meio de uma prece. Inaceitável.
Pior: Linux roda dezenas de daemons em background. A palavra vem dos anos 60 no MIT, referência ao demônio de Maxwell da termodinâmica — mas eu, como arquiteto responsável, não posso assinar um sistema que literalmente executa demônios em segundo plano enquanto o fiel acende uma vela. O simbolismo é péssimo. Imagina explicar isso pro Padre.
A solução foi construir um unikernel dedicado, que batizei de SeraphOS. Inspirado em MirageOS e Unikraft, mas enxuto pro caso de uso. Características:

  • Zero daemons. Não há cron, não há systemd, não há sshd, não há nada rodando em background. O binário da aplicação é o kernel. Um único processo, no ring 0, sem separação userspace/kernelspace (não precisa — só roda nosso código auditado). Boot em 30ms direto no hypervisor KVM.
  • Sem scheduler preemptivo. Cooperative scheduling com green threads em cima do runtime Tokio modificado. Sem context switch involuntário, sem jitter.
  • Network stack customizado. Bypass completo do kernel Linux via DPDK-style polling nas NICs. Pacote chega, é parseado, resposta sai — tudo em userspace (que na prática é o único space). Latência p99 caiu de 400µs pra 12µs.
  • Sem filesystem tradicional. O ring buffer mmap’d É o storage. Não existe VFS, não existe ext4, não existe inode. Escrita direta em bloco via NVMe submission queues.
  • Sem usuários, sem permissões, sem shell. Ninguém faz login no SeraphOS. Se você precisa debugar, você reinicia com uma build de debug. Em produção, o sistema é uma caixa-preta consagrada.

Resultado do novo benchmark: 3.1M velas/segundo no mesmo t3.micro. O novo bottleneck é a largura de banda da rede, que é um problema honesto de físico, não de software.

Versionamento do código:

GitHub é da Microsoft. Microsoft comprou a Activision, distribui Diablo, e distribui Candy Crush — um jogo desenhado por psicólogos comportamentais pra viciar fiéis em gula digital. Não vou hospedar o código que serve as orações do povo brasileiro num servidor do príncipe deste mundo.
A escolha foi Fossil — sistema de controle de versão criado pelo autor do SQLite, distribuído, auto-hospedado, com wiki, bug tracker e forum embutidos num único binário de 4MB. O nome é teologicamente apropriado: remete ao que é antigo, duradouro, testemunho do que veio antes. Rodando em servidor próprio, atrás de um Nginx, longe da nuvem de qualquer BigTech. O repo é espelhado via fossil sync pra três máquinas físicas — Pai, Filho, e Espírito Santo (hostnames reais).

Alguns vão dizer que isso é overengineering. Discordo. Overengineering é adicionar complexidade sem propósito. Aqui cada decisão responde a uma restrição real: memória é finita, CPU custa dinheiro, e cada byte economizado é um byte que pode ser usado pra servir mais um fiel.

A computação nasceu da contagem de almas em censos. Voltar às origens, às vezes, é o caminho.
Amém 🕯️

Top comments (0)