21 pontos por GN⁺ 2025-10-27 | 3 comentários | Compartilhar no WhatsApp
  • A Nanit usava o AWS S3 em um pipeline de processamento de vídeo para analisar o sono de bebês, mas com milhares de uploads por segundo, o custo das requisições PutObject passou a representar a maior parte do custo total
  • Além disso, por causa da retenção mínima de 1 dia das regras de Lifecycle do S3, era necessário pagar por 24 horas de armazenamento para vídeos que, na prática, eram processados em cerca de 2 segundos
  • Para resolver isso, a empresa construiu o N3, um sistema de armazenamento em memória baseado em Rust, e passou a usar o S3 apenas como buffer de overflow
  • O N3 é totalmente compatível com o pipeline de processamento existente por meio do SQS FIFO, mantendo garantia estrita de ordem e confiabilidade
  • Como resultado, a empresa obteve uma economia anual de cerca de US$ 500 mil, além de uma arquitetura simples e estável

Contexto

Visão geral do pipeline de processamento de vídeo

  • A arquitetura era tal que as câmeras da Nanit gravavam chunks de vídeo, solicitavam uma URL pré-assinada do S3 ao Camera Service e faziam upload direto para o S3
  • O AWS Lambda publicava a chave do objeto em uma fila SQS FIFO (com shard por baby_uid), e os pods de processamento de vídeo consumiam do SQS, baixavam do S3 e executavam a inferência do estado de sono
  • Vantagens dessa configuração
    • O pouso no S3 + o enfileiramento no SQS desacoplavam o upload da câmera do processamento de vídeo, evitando perda de vídeos mesmo durante manutenção ou indisponibilidade temporária
    • Não era necessário gerenciar diretamente disponibilidade e durabilidade por conta do S3
    • Com SQS FIFO + group ID, a ordem por bebê era preservada, e os nós de processamento podiam permanecer em sua maioria stateless
    • As regras de Lifecycle do S3 cuidavam da coleta de lixo, dispensando rastrear vídeos já processados

Por que era necessário mudar

  • O custo do PutObject era dominante: os vídeos eram objetos de vida curta que ficavam na área de pouso por apenas alguns segundos até serem processados, mas em uma escala de milhares de uploads por segundo, o custo por requisição de objeto tornou-se o principal fator de custo
    • Se a empresa aumentasse a frequência de chunking (enviando mais chunks menores) para reduzir a latência, o custo cresceria linearmente, porque cada chunk adicional significaria outra requisição PutObject
  • O armazenamento era uma segunda cobrança: mesmo quando o processamento terminava em cerca de 2 segundos, as regras de exclusão do Lifecycle ainda implicavam aproximadamente 24 horas de custo de armazenamento
  • Era necessário um desenho que mantivesse confiabilidade e garantia estrita de ordem, mas evitasse o custo por objeto no caminho normal e minimizasse o armazenamento pelo qual se “paga para esperar”

Plano

  • Princípios de projeto

    • Simplicidade por arquitetura: remover complexidade no nível do design, e não com implementações espertas
    • Correção: ser um substituto completo e transparente para o restante do pipeline
    • Otimizado para o caminho normal: projetar para o caso comum e usar o S3 como rede de segurança para edge cases; como o algoritmo de processamento tolera lacunas ocasionais, a simplicidade teve prioridade sobre a construção de garantias complexas
  • Fatores que guiaram o design

    • Objetos de vida curta: os segmentos existem na área de pouso por apenas alguns segundos
    • Ordem: sequenciamento estrito por bebê (sem processar o mais recente primeiro)
    • Vazão: milhares de uploads por segundo, com 2–6 MB por segmento
    • Limitações do cliente: as câmeras têm número limitado de tentativas de retry, então não se pode assumir reenvio
    • Operação: era necessário tolerar backlogs de milhões de itens durante manutenção ou scale-up
    • Sem mudança de firmware: precisava funcionar com as câmeras existentes
    • Tolerância a perda: lacunas muito pequenas são aceitáveis, e o algoritmo as mascara
    • Custo: evitar o custo por objeto do S3 no caminho normal e minimizar armazenamento de “pagar para esperar”

Visão geral do design (caminho normal do N3 + overflow para o S3)

  • Arquitetura

    • O N3 é uma área de pouso customizada que mantém vídeo em memória apenas pelo tempo necessário para o processamento drenar (cerca de 2 segundos), usando o S3 somente quando o N3 não consegue absorver a carga
    • Dois componentes
      • N3-Proxy (stateless, interface dupla)
        • Externo (conectado à internet): aceita uploads das câmeras por meio de URLs pré-assinadas
        • Interno (privado): emite URLs pré-assinadas para o Camera Service
      • N3-Storage (stateful, somente interno): armazena os segmentos enviados em RAM e os coloca em uma fila SQS com uma URL de download endereçável ao pod
    • Os pods de processamento de vídeo consomem do SQS FIFO e fazem download do armazenamento indicado pela URL (N3 ou S3)
  • Fluxo normal (Happy Path)

    • A câmera solicita uma URL de upload ao Camera Service
    • O Camera Service solicita uma URL pré-assinada à API interna do N3-Proxy
    • A câmera envia o vídeo para o endpoint externo do N3-Proxy
    • O N3-Proxy encaminha o upload ao N3-Storage
    • O N3-Storage mantém o vídeo em memória e o coloca no SQS com uma URL de download apontando para si mesmo
    • O pod de processamento baixa do N3-Storage e processa
  • Fallback em duas camadas

    • Camada 1: fallback no nível do proxy (por requisição)
      • Se o N3-Storage não conseguir receber o upload por pressão de memória, backlog de processamento, falha de pod etc., o N3-Proxy faz o upload para o S3 em nome da câmera
      • A câmera já havia recebido uma URL do N3 antes que a falha fosse detectada
    • Camada 2: redirecionamento no nível do cluster (todo o tráfego)
      • Se o N3-Proxy ou o N3-Storage estiverem não saudáveis, o Camera Service para de emitir URLs do N3 e retorna diretamente uma URL pré-assinada do S3
      • Todo o tráfego passa a ir para o S3 até que o N3 se recupere
  • Por que separar em dois componentes

    • Raio de falha: se o storage cair, o proxy ainda consegue rotear para o S3; se o proxy cair, apenas o tráfego daquele nó é afetado, e o cluster de storage como um todo continua intacto
    • Perfil de recursos: o proxy é intensivo em CPU/rede (terminação TLS), enquanto o storage é intensivo em memória (retenção de vídeo), com tipos de instância e requisitos de escala diferentes
    • Segurança: o storage nunca entra em contato com a internet
    • Segurança de rollout: ao atualizar o proxy (stateless), o storage (que mantém dados ativos) não é tocado

Validação do design

  • O que precisava ser validado

    • Capacidade e dimensionamento: a duração real dos uploads em diferentes redes de clientes, além do compute necessário e do tamanho do buffer de upload
    • Modelo de armazenamento: se era possível manter tudo em RAM ou se seria necessário disco
    • Resiliência: como fazer load balancing barato e lidar com nós com falha
    • Políticas operacionais: exigências de GC, expectativa de retries e se excluir no GET seria suficiente
    • Desconhecidos desconhecidos: quais edge cases surgiriam quando a ideia encontrasse a realidade
  • Abordagem 1: teste sintético de estresse

    • Foi construído um gerador de carga para empurrar o sistema até o limite, com diferentes níveis de concorrência, clientes lentos, carga contínua e indisponibilidade do processamento
    • Objetivo: encontrar pontos de quebra, identificar gargalos inesperados e obter uma linha de base determinística para planejamento de capacidade
  • Abordagem 2: PoC em produção (modo espelho)

    • Testes sintéticos não conseguiam reproduzir o comportamento real das câmeras: Wi‑Fi instável, versões variadas de firmware e condições de rede imprevisíveis
    • Modo espelho: o n3-proxy primeiro gravava no S3 (preservando produção) e depois também gravava no N3-Storage do PoC (ligado a um SQS canário e ao processador de vídeo)
    • Coorte alvo: por versão de firmware / lista de Baby-UIDs
    • Paridade de dados: comparação do estado de sono entre o PoC e a produção, investigando divergências
    • Observabilidade: dashboards por caminho (N3 vs S3), profundidade de fila, latência/RPS, orçamento de erro e análise de egress
    • Feature flags (com Unleash) foram essenciais: permitiam trocar coortes em tempo real sem deploy, testar fatias estreitas (firmware antigo, câmeras com Wi‑Fi fraco) e reverter imediatamente em caso de problema
  • O que foi descoberto

    • Gargalos: a terminação TLS consumia a maior parte da CPU, e o burstable networking da AWS sofria throttling após esgotar créditos
    • Storage só em memória era viável: pela distribuição real do tempo de upload e da concorrência, confirmou-se que era possível manter o working set em RAM com folga de segurança, sem precisar de disco
    • Overhead de TCP timestamps: cerca de 85% do total de bytes transmitidos eram frames ACK; ao desativar TCP timestamps (sysctl -w net.ipv4.tcp_timestamps=0), houve economia de 12 bytes por ACK
      • Risco: em transmissões com alto volume de bytes no mesmo socket, poderia haver wrap do número de sequência e corrupção por remontagem incorreta de pacotes atrasados
      • Mitigação: (1) novo socket por upload, (2) reciclar sockets entre n3-proxy ↔ n3-storage após cerca de 1 GB transferido
    • Vazamento de memória: após o lançamento inicial, a memória do n3-proxy crescia continuamente
      • O profiling com jemalloc mostrou crescimento nos buffers hyper BytesMut por conexão
      • Algumas conexões de clientes paravam no meio da transferência e não eram limpas, deixando buffers acumulados e aumentando a memória continuamente
      • Correção: tornar os sockets de curta duração e aplicar limites de tempo
        • Keep-alive desativado: cada conexão era encerrada logo após a conclusão do upload
        • Timeouts mais rígidos: timeouts de header/socket passaram a encerrar uploads travados e liberar buffers

Armazenamento

  • Armazenamento em memória

    • A equipe começou pelo caminho mais simples: armazenamento em memória, evitando tuning de I/O e usando estruturas de dados intuitivas
    • Os vídeos eram armazenados em Arc<DashMap<Ulid, Bytes>>; cada upload aumentava bytes_used, e cada download apagava o vídeo e decrementava esse valor
    • Ao atingir cerca de 80% ou mais da capacidade, o sistema começava a recusar uploads para evitar OOM, sinalizando ao n3-proxy que parasse de assinar URLs de upload
    • Um handle control permitia pausar manualmente uploads e garbage collection
  • Reinício gracioso

    • Como o storage era somente em memória, era necessário evitar perda de dados em andamento durante reinícios
    • Processo de reinício gracioso
      • O pod recebia SIGTERM (com o StatefulSet fazendo rolling um de cada vez)
      • O pod entrava em estado Not Ready e saía do Service (sem novos uploads)
      • Continuava servindo downloads para vídeos já enviados
      • Quando os downloads paravam (sem leituras recentes → drenagem do processamento)
      • Aguardava a conclusão das requisições abertas
      • Depois reiniciava e passava para o próximo pod
    • Em operação normal, os pods drenavam em poucos segundos
  • GC

    • Foram usados dois mecanismos de limpeza
      • Excluir ao baixar: o vídeo era removido logo após o download; no PoC, confirmou-se zero redownloads, e como o processador de vídeo já fazia retries internamente, não era necessário manter dados nem rastrear estado de “processado”
      • TTL GC para os que ficaram para trás: excluir no download não cobria segmentos que o processador pulava (não baixados → não excluídos)
        • Foi adicionado um TTL GC leve: varredura periódica do DashMap em memória e remoção de itens mais antigos que um limiar configurável (por exemplo, algumas horas)
    • Modo de manutenção: durante indisponibilidades planejadas do processamento, era possível pausar o GC via controle interno, evitando exclusão de vídeos enquanto o consumo estivesse parado

Conclusão

  • Principais resultados

    • Usando o S3 como buffer de fallback e o N3 como área principal de pouso, a empresa alcançou economia de cerca de US$ 500 mil por ano, mantendo o sistema simples e confiável
    • Insight central: a maioria das decisões de “construir vs comprar” foca em funcionalidade, mas na escala, a economia muda a conta
      • Para objetos de vida curta (cerca de 2 segundos em operação normal), não era necessária replicação nem durabilidade sofisticada; um armazenamento simples em memória funcionava
      • Quando o processamento atrasava ou a manutenção prolongava a vida dos objetos, eram necessárias as garantias de confiabilidade do S3
      • Melhor dos dois mundos: o N3 tratava o caminho normal com eficiência, enquanto o S3 fornecia durabilidade quando os objetos precisavam viver mais tempo
      • Se algo desse errado no N3 (pressão de memória, crash de pod, problema no cluster), os uploads faziam failover de forma transparente para o S3
  • Fatores de sucesso

    • Definir claramente o problema antes: restrições, premissas e limites evitaram expansão de escopo
    • Validar cedo com um PoC em modo espelho: isso revelou gargalos (TLS, throttling de rede) e validou premissas antes de comprometer a solução
      • Evitando overengineering e retrabalho
  • Quando vale construir algo assim

    • Vale considerar infraestrutura customizada quando há escala suficiente para gerar economia relevante e quando restrições específicas permitem uma solução simples
    • O esforço de engenharia para construir e manter o sistema precisa ser menor do que o custo de infraestrutura eliminado
    • No caso da Nanit, requisitos específicos (armazenamento temporário, tolerância a perda, fallback para S3) permitiram construir algo simples o bastante para manter baixo o custo de manutenção
    • Sem esses dois fatores, o melhor é ficar com serviços gerenciados
    • Fariam de novo? Sim, porque o sistema roda de forma estável em produção e o design com fallback permite evitar complexidade sem sacrificar confiabilidade

3 comentários

 
click 2025-10-28

Fico me perguntando se não daria para simplesmente o próprio EC2 ou os pods do EKS receberem o upload dos vídeos e processarem diretamente
Se chegaram ao ponto de criar até um proxy, parece que daria bem para lidar também com o auto scaling do EKS conforme a carga dos pods
No processamento de vídeo, normalmente não é preciso carregar o arquivo inteiro em memória; se tivessem criado arquivos temporários no SSD local de cada instância e processado por ali, dá a sensação de que nem precisariam de fallback para o S3

 
t7vonn 2025-10-28

Parece um exemplo de uso errado de serverless e do S3.
Mas a solução também parece ainda mais estranha.

 
GN⁺ 2025-10-27
Comentários do Hacker News
  • Foi um texto realmente muito útil. Gosto muito quando compartilham esse processo de abordagem técnica
    Mesmo sem eu passar exatamente pelo mesmo problema, só de ver com que linha de raciocínio chegaram à solução já dá para aprender bastante

  • Sinceramente, isso provavelmente teria ficado muito mais limpo se não tivessem usado serverless desde o começo
    Parece que tentaram encaixar à força dados de poucos segundos no paradigma serverless da AWS, e isso acabou gerando custo e complexidade desnecessários
    Ainda assim, migrar para uma solução baseada em memória foi uma boa escolha

    • No fim, acabaram rodando instâncias pesadas para garantir throughput de rede e RAM, mas quase sem usar CPU
      Disseram que o handshake TLS consumia bastante CPU, mas não parece ser o principal gargalo
      Mesmo assim, foi interessante ver essa tentativa de projetar um sistema sob medida para o workflow
  • Na prática, mais do que “implementar o S3 por conta própria”, isso era uma arquitetura com cache em memória na frente do S3
    É bacana, mas não chega a ser um substituto completo do S3

    • Sim, mas como provavelmente era difícil mudar os clientes existentes, parece que eles precisavam imitar a API do S3
      Independentemente do título, foi um projeto interessante
    • Não entendi por que o cache precisava ser em memória. Armazenamento local provavelmente já teria sido suficiente
  • Falando no estilo HN, quero comentar sobre a própria empresa, a Nanit
    A Nanit opera uma câmera de monitoramento de bebês baseada em nuvem. Todo o vídeo e áudio é enviado sem E2EE
    O hardware é caro e, sem assinatura, mal dá para usar. Além disso, é preciso comprar um suporte de $200 para liberar o recurso de rastreamento do sono
    É uma pena que essa estrutura acabe reforçando esse modelo de dependência da nuvem
    Ainda assim, foi uma boa decisão reduzir a dependência do S3 e migrar para armazenamento próprio, como neste texto

    • Eu sou um cliente satisfeito. Escolhi a Nanit simplesmente porque “funcionava bem”
      Outros produtos tinham apps instáveis. Seria bom ter uma solução local-first + E2EE, mas na prática a usabilidade era mais importante
    • Sobre E2EE, neste caso não é só armazenamento, e sim análise de vídeo na nuvem, então E2EE em si não é viável
      Se alguém quiser E2EE de verdade, a análise teria de ser feita localmente, com apenas os resultados sendo enviados
    • Eu mesmo fiz um suporte de madeira e usei assim; na época não havia limitação por software. Fico curioso se isso mudou agora
    • Não concordo com a ideia de que “hospedar vídeo por conta própria não é difícil”. Para o usuário comum, isso nem entra em consideração
    • Na verdade, nem sei se um monitor de bebê precisa mesmo de nuvem. De qualquer forma, há um adulto confiável por perto
  • O texto passou uma sensação de comemorar por ter resolvido um problema que a própria empresa criou
    Teria sido mais simples e barato vender hardware com armazenamento local desde o início
    Esse tipo de design centrado em nuvem já parece uma abordagem de 2015

  • O texto foi ótimo, mas também fiquei curioso sobre a economia de custos ao implementar ‘delete on read’ no S3
    Se o S3 fosse cobrado em intervalos de segundos, a economia poderia ter sido considerável
    Além disso, essa solução na prática lembra a opção de ‘reduced redundancy’ do S3

  • Dizem que economizaram $500 mil, mas não sabemos qual era o custo total
    Faz diferença se foi $500.001 em um total de $500 mil ou $500 mil em um total de $55 milhões

    • Sim, na verdade esse tipo de problema também pode ser resolvido não com técnica, mas com negociação de preços com a AWS (PPA)
    • Ainda assim, o S3 continua sendo um serviço com ótimo custo-benefício, então, mesmo se sairmos da AWS, pretendemos manter S3 e SQS
  • Parece que escolheram uma arquitetura errada desde o início e depois tentaram remendar com cache
    Não há motivo para enviar vídeos de 2 segundos em média para o S3, além de armazenamento duplicado
    Se tivessem processado isso diretamente no servidor, poderiam ter eliminado S3, SQS e Lambda de uma vez

    • Exato, o S3 é para armazenamento, não para processamento
      Não entendo por que transformaram um problema tão simples em algo tão complicado
  • Parece aquela lição clássica de “foque no desenvolvimento do app e mantenha a infraestrutura simples”
    Teria sido melhor colocar o cache diretamente dentro do servidor de processamento de vídeo

  • Um título mais preciso talvez fosse “usamos o S3 do jeito errado”

    • Sério, não entendo por que tentaram armazenar no S3 milhões de arquivos curtos e temporários
      No fim, construíram um armazenamento em memória próprio, quando poderiam simplesmente ter usado algo como Redis
      Se o sistema deles cair, os vídeos simplesmente desaparecem?
      Se tivessem enviado isso para o Kinesis ou SQS desde o começo, teria sido muito melhor