Projetando Armazenamento de Arquivos em Escala: Como Funciona a Sincronização e o Upload de Dropbox e Google Drive
Criar uma plataforma de armazenamento de arquivos na nuvem parece um desafio simples. "Basta disponibilizar um gateway HTTP, fazer upload de um blob para o Amazon S3 e salvar a URL na tabela de metadados do banco", muitos pensam.
No entanto, essa abordagem de "MVP" desmorona rapidamente diante da realidade física das redes e dispositivos de usuários reais. Quando um usuário está fazendo upload de um arquivo de 10 GB no celular e entra em um túnel sem conexão aos 95% do progresso, ele espera que o sistema continue exatamente de onde parou. Quando um colaborador renomeia uma pasta contendo 500.000 arquivos no desktop, o sistema não pode mover terabytes de dados fisicamente nem colapsar o banco de dados atualizando 500.000 linhas de forma síncrona.
Neste artigo, vamos analisar como projetar a engenharia de um workspace de arquivos resiliente (estilo Dropbox e Google Drive), capaz de lidar com petabytes de ingress diários, resolver conflitos de sincronização e economizar armazenamento por meio de deduplicação inteligente.
1. Separação de Planos: Control Plane vs Data Plane
Em alta escala, o primeiro princípio de design é nunca trafegar bytes de arquivos por meio dos servidores de aplicação de metadados.
Separamos a arquitetura em dois planos operacionais independentes:
- Metadata Plane (Control Plane): Trata apenas do namespace, estrutura de pastas, dados de revisões, cotas e permissões. Geralmente expõe endpoints REST ou gRPC rápidos e lida com dados estruturados.
- Blob Plane (Data Plane): Lida exclusivamente com a transferência massiva de bytes (uploads, downloads, geração de previews e compactação de chunks).
Ao fazer upload, o cliente primeiro obtém autorização e uma rota no gateway de metadados. Em seguida, ele envia os bytes diretamente para o Upload Gateway do Data Plane, que os deposita em células de armazenamento de objetos (como S3 ou minio/Magic Pocket). Uma vez que todos os bytes são salvos, o cliente faz um commit rápido no Metadata Plane para tornar a nova revisão do arquivo oficial.
2. Uploads Resumíveis (Resumable Uploads) e Chunking
Para suportar arquivos gigantes (10 GB+) e conexões instáveis, o sistema utiliza uma estratégia de Chunking:
- O arquivo é dividido localmente em blocos imutáveis chamados chunks (tipicamente de 8 MB).
- Cada chunk é endereçado de forma única usando o hash criptográfico do seu conteúdo (SHA-256).
- O cliente inicia uma sessão de upload fornecendo o tamanho total e o hash geral do arquivo.
- O cliente faz upload dos chunks individualmente.
Se a conexão cair no meio, o cliente chama um endpoint de status (GET /upload-sessions/{id}/status). O servidor retorna a lista de ranges de bytes ou hashes que já foram gravados fisicamente. O cliente então envia apenas os blocos ausentes.
Arquivo de 24 MB ──► Divisão ──► Chunk 0 (8MB) ➔ Upload OK
──► Chunk 1 (8MB) ➔ Conexão Caiu (Reenviar)
──► Chunk 2 (8MB) ➔ staged
Uma vez preenchidos todos os blocos do arquivo sem lacunas ou sobreposições, a aplicação executa o Commit. O commit associa a lista ordenada de hashes de chunks (o manifest) a uma nova revision_id de metadados do arquivo.
3. Sync Engine: O Loop de Sincronização por Cursores
Sincronizar arquivos entre múltiplos dispositivos (laptops, celulares e web) em tempo real é uma fonte clássica de problemas de concorrência. Websockets de conexão persistente e notificações push são ótimos atalhos de tempo real, mas são inerentemente instáveis e perdem mensagens constantemente.
O modelo sênior de sincronização trata notificações apenas como dicas de invalidade. A fonte de verdade das mudanças é um Change Log append-only distribuído mantido pelo servidor.
Cada alteração de metadados (como criar, mover ou deletar um arquivo) anexa um evento monotônico ao log associado ao namespace do usuário. Cada dispositivo armazena localmente o último ID de evento processado (o Cursor).
Fluxo de sincronização resiliente:
- O servidor avisa o dispositivo: "Há novas mudanças disponíveis" (via Push).
- O dispositivo faz uma requisição HTTP perguntando: "O que mudou a partir do meu cursor
cur_998812?" (GET /changes?cursor=cur_998812). - O servidor responde com uma lista paginada de eventos.
- O dispositivo baixa os chunks dos novos arquivos, cria as pastas necessárias localmente e atualiza os arquivos modificados.
- Após consolidar com sucesso as alterações no disco local, o dispositivo salva o novo cursor (
cur_998815) em seu banco de dados local.
Se a notificação push falhar, o dispositivo eventualmente fará um polling simples na API para garantir que não perdeu nenhum evento, mantendo a consistência.
4. Deduplicação de Dados Segura (Deduplication)
Deduplicação de dados em nível de chunk economiza até 30% do custo físico de armazenamento. Se dois usuários enviam o mesmo PDF de 50 MB, o sistema armazena apenas um único blob físico no Object Store, apontando ambas as referências lógicas para o mesmo endereço de hash.
O Risco de Segurança (Side-Channel Attack)
Se o cliente pergunta ao servidor: "Você já tem o hash SHA-256 X?" antes de fazer o upload, um atacante pode usar isso para descobrir se um arquivo específico (por exemplo, um documento confidencial vazado) existe na conta de outro usuário.
Como resolver de forma sênior: O cliente sempre envia os bytes do chunk. O Upload Gateway verifica o hash do payload enviado contra o banco de chaves existentes (Content Index) internamente. Se houver um acerto (dedup hit), o gateway apenas incrementa a referência lógica e conclui a operação sem gravar um novo objeto físico. O cliente apenas descobre que seu upload foi bem-sucedido, sem saber se o arquivo já existia de forma global.
5. Resolução de Conflitos e Overwrites Silenciosos
Quando Alice e Bob estão offline e editam o arquivo relatorio.docx ao mesmo tempo a partir da revisão rev_0042, ao voltarem online ambos tentarão fazer o commit do upload.
Se Bob confirmar primeiro, a revisão atual do arquivo no banco virará rev_0043. Quando o upload de Alice tentar commitar declarando a base anterior rev_0042, o servidor detectará o descompasso de versões.
Ações recomendadas do Servidor:
- Recusar o overwrite silencioso: Retornar status de conflito HTTP 409.
- Preservar ambos os arquivos: O sistema de Alice commitará uma nova entidade de arquivo com um sufixo descritivo (ex:
relatorio (cópia em conflito de Alice).docx). - Gerar eventos de change log: Ambos os dispositivos receberão as atualizações e sincronizarão as duas versões do arquivo, deixando que os usuários decidam visualmente qual manter ou mesclar.
6. Anti-Patterns Críticos para Evitar em System Design
- Utilizar Paths (Caminhos) como Chaves Primárias: Caminhos de diretórios como
/financeiro/2026/gastos.xlsxmudam constantemente quando pastas superiores são renomeadas ou movidas. Use sempre IDs estáveis (file_id) gerados pelo backend. O path deve ser apenas uma propriedade de exibição computada a partir das referências das pastas. - Processar Previews e Escanear Malware Sincronamente: Gerar previews (PDFs, thumbnails) e rodar antivírus em arquivos grandes consome muita CPU e banda de rede. Esses fluxos devem ser totalmente assíncronos e executados por workers isolados (em sandboxes seguras) alimentados por filas de mensagens.
- Deletar Blocos de Dados Imediatamente: Fazer um delete físico direto no Object Store no instante em que um arquivo é apagado impede a recuperação na lixeira e quebra referências compartilhadas. A exclusão de blocos físicos de dados deve ser gerenciada de forma assíncrona por um processo de Garbage Collection (GC) que analisa dependências lógicas e janelas de retenção de segurança.
Quer ler a análise detalhada com estimativas de banda de IOPS, diagramas de sequência dos fluxos de escrita, design de herança de ACL e seguranças avançadas de tiering hot/cold?
Confira aqui: https://lemon.dev.br/pt/blog/file-storage-system-design