Pular para o conteúdo

Design de sistema de sincronização de arquivos: como funciona a sync no estilo Dropbox

Um serviço de sync como o Dropbox: blocos e hashes de conteúdo, journal de metadados com cursores, notificações, conflitos e upload só dos blocos alterados.

· Fundador e desenvolvedor

Publicado em

Um notebook envia sua lista de blocos ao serviço de metadados (journal por namespace no Postgres), que devolve os hashes que faltam. O notebook os envia ao serviço de blocos e ao armazenamento de objetos. Uma notificação acorda o celular, que lista as mudanças e baixa os blocos.

Você salva uma apresentação de 300 MB no notebook e, um minuto depois, ela abre no celular. Aí você corrige um erro de digitação em um slide, e a atualização leva um segundo, porque só a parte alterada viaja. Um serviço de sincronização de arquivos faz isso parecer fácil, mas por baixo há uma separação cuidadosa entre dois tipos de dados muito diferentes, mais um protocolo que tolera redes instáveis, edições offline e dispositivos que acordam dias depois.

Este post projeta um serviço de sincronização no estilo Dropbox: requisitos e números, a separação entre metadados e conteúdo, divisão em blocos e hashes de conteúdo, o protocolo de upload e commit, o journal de metadados e os cursores, notificações, conflitos e coleta de lixo, com um diagrama de arquitetura.

Requisitos

  • Arquivos salvos em um dispositivo aparecem nos outros dispositivos do usuário em segundos ou minutos.
  • Só os dados alterados sobem e descem.
  • Edições feitas offline sincronizam quando o dispositivo se reconecta, e edições concorrentes nunca sobrescrevem umas às outras em silêncio.
  • Pastas compartilhadas funcionam entre contas.
  • Nenhuma perda de dados, nunca; histórico de versões para recuperação.

Capacidade

Suponha 10 milhões de dispositivos ativos, cada um sincronizando 20 mudanças por dia, com uma média de 256 KB de dados novos por mudança após a deduplicação:

Grandeza Cálculo Resultado
Mudanças por dia 10M × 20 200 milhões
Commits de metadados 200.000.000 / 86.400 s ~2.300 por segundo
Dados novos de blocos 200M × 256 KB ~51 TB por dia

A taxa de metadados é modesta; o volume de dados é grande, mas simples de armazenar. Essa assimetria guia o design.

Separe os metadados do conteúdo

A decisão mais importante é tratar o conteúdo e os metadados dos arquivos como dois sistemas diferentes:

  • O conteúdo é armazenado como blocos imutáveis, endereçados pelo hash dos seus bytes. Os blocos são grandes, gravados uma vez e fáceis de replicar no armazenamento de objetos.
  • Os metadados registram qual arquivo, em qual caminho e em qual versão, é composto por quais blocos. São pequenos, mudam o tempo todo e precisam de transações e de ordenação estrita.

O Dropbox descreveu essa separação no post sobre streaming sync: cada arquivo é dividido em blocos de 4 MB, cada bloco recebe um hash SHA-256 e o conteúdo de um arquivo é identificado pela lista desses hashes, a sua lista de blocos. A API pública ainda expõe a mesma ideia como um content hash: o SHA-256 de cada bloco de 4 MB e, depois, o SHA-256 dos hashes de bloco concatenados.

Divisão em blocos: fixa ou definida pelo conteúdo

Blocos de tamanho fixo são simples e rápidos de calcular. A fraqueza aparece quando bytes são inseridos perto do início de um arquivo: toda fronteira de bloco depois da inserção se desloca, e todo hash de bloco muda.

A divisão definida pelo conteúdo (content-defined chunking) coloca as fronteiras onde um hash deslizante sobre o conteúdo bate com um padrão, como fazem o checksum deslizante do rsync e algoritmos mais novos como o FastCDC. As fronteiras se movem com o conteúdo, então uma inserção só muda os pedaços ao redor dela.

Inserir 10 bytes no início de um arquivo de 100 MB Blocos fixos de 4 MB Pelo conteúdo, ~4 MB em média
Pedaços que mudam Todos os 25 Cerca de um
Dados para enviar ~100 MB ~4 MB
Custo de CPU da divisão Baixo Maior: um hash deslizante sobre cada byte

Para arquivos que recebem dados principalmente no fim ou são substituídos inteiros, blocos fixos bastam. Para arquivos editados no meio, como documentos grandes, imagens de disco ou bancos de dados, a divisão pelo conteúdo economiza muito mais banda.

O protocolo de upload e commit

  1. O cliente desktop percebe uma mudança pelo observador de arquivos do sistema operacional, espera até o arquivo parar de mudar e então o divide e calcula os hashes.
  2. Ele envia um commit ao serviço de metadados: este caminho, esta nova lista de blocos, com base na versão pai N.
  3. O serviço de metadados verifica quais hashes já armazena e responde com os que precisa.
  4. O cliente envia só esses blocos ao serviço de blocos, em paralelo, cada um verificado pelo seu hash. Um upload interrompido continua a partir dos blocos que faltam.
  5. O cliente tenta o commit de novo. Agora todos os blocos existem, então o serviço de metadados anexa uma nova versão do arquivo ao journal.

A ordem é a propriedade de segurança: uma versão que referencia blocos nunca é confirmada antes que todos estejam armazenados. Uma queda em qualquer ponto deixa a versão antiga ou a nova, nunca um arquivo apontando para dados que não existem.

O endereçamento por conteúdo também traz deduplicação de graça. Um arquivo copiado para três pastas, ou revertido para uma versão anterior, não envia nada novo. A deduplicação entre contas economiza mais espaço, mas pode revelar que outra pessoa guardou o mesmo arquivo; por isso muitos designs só deduplicam dentro de uma conta ou de uma pasta compartilhada.

O journal de metadados e os cursores

Agrupe os arquivos em namespaces, como a árvore de pastas do próprio usuário ou uma pasta compartilhada. Para cada namespace, o serviço de metadados mantém um journal somente de anexação: cada mudança recebe o próximo número de sequência, com o caminho, a versão e a lista de blocos.

Cada cliente guarda um cursor por namespace: o último número de sequência que aplicou. Sincronizar para baixo vira então uma única pergunta: “me dê tudo neste namespace depois do meu cursor”. O journal torna isso barato, ordenado e retomável. Um dispositivo que ficou offline por uma semana faz a mesma pergunta e recebe uma semana de mudanças. Particione os metadados por namespace, já que as mudanças em um namespace nunca precisam de uma transação com outro.

Notificações: avisar os dispositivos para sincronizar

Consultar o journal a partir de cada dispositivo ocioso seria desperdício. Em vez disso, os clientes desktop ociosos mantêm uma conexão de long poll com um serviço de notificações. Quando o journal de um namespace avança, o serviço responde às requisições em espera com “há mudanças”; cada cliente então lista as mudanças depois do seu cursor e baixa os blocos que não tem. A API pública do Dropbox oferece o mesmo padrão com list_folder/longpoll e list_folder/continue.

Apps móveis não conseguem manter conexões abertas em segundo plano, então dependem das notificações push da plataforma e sincronizam quando o app é aberto.

O lado do cliente

O cliente desktop é metade do sistema. Ele mantém um banco local com o último estado sincronizado de cada arquivo: caminho, versão, lista de blocos, tamanho e data de modificação. Comparar três estados decide cada ação:

Arquivo local vs. último sincronizado Remoto vs. último sincronizado Ação
Alterado Inalterado Enviar
Inalterado Alterado Baixar
Alterado Alterado Conflito
Inalterado Inalterado Nada

Um cache local de blocos evita baixar blocos que o dispositivo já tem em outros arquivos.

Conflitos

Dois dispositivos editam o mesmo arquivo enquanto estão offline. Mais tarde, os dois fazem commit com a versão pai N. O serviço de metadados aceita o primeiro commit, criando a versão N+1, e rejeita o segundo, porque o pai dele não é mais o atual. Em vez de mesclar arquivos binários que não entende, o cliente que perdeu salva a sua versão ao lado do original como cópia em conflito e sincroniza as duas. O usuário decide.

Isso é concorrência otimista: cada commit informa a versão em que se baseou. A mesma verificação cuida das operações de diretório, que na prática são mais difíceis: uma renomeação em um dispositivo e uma edição em outro, ou uma exclusão disputando com uma edição. Escreva esses casos de forma explícita; é neles que os motores de sincronização perdem dados.

Histórico de versões e coleta de lixo

Versões antigas mantêm as suas listas de blocos, então restaurar um arquivo é só fazer commit de uma lista de blocos antiga como a versão mais nova. Os blocos só viram lixo quando nenhuma versão dentro do período de retenção os referencia. Rode a coleta de lixo como mark-and-sweep com um período de carência, nunca como uma exclusão imediata quando some a última referência, porque um cliente pode estar prestes a fazer commit de uma lista de blocos que reutiliza um bloco que o servidor acha que está órfão.

O diagrama de sincronização de arquivos

Um notebook envia sua lista de blocos ao serviço de metadados (journal por namespace no Postgres), que devolve os hashes que faltam. O notebook os envia ao serviço de blocos e ao armazenamento de objetos. Uma notificação acorda o celular, que lista as mudanças e baixa os blocos.

11 componentes · 11 conexões

Abrir no Archboard
Conteúdo do diagrama
  • Laptop (writer)
  • Phone (reader)
  • Sync backend
  • API gateway
  • Metadata service
  • Notification service
  • Block service
  • Journal per namespace
  • journal events
  • Blocks by SHA-256 (4 MB)
  • 1, 3, 4
  • 5. changes available
  • 6. list after cursor, get blocks
  • 1. commit (parent v7)
  • 2. missing hashes
  • 3. upload missing blocks
  • 5. answer long polls
  • 4. append v8
  • namespace changed
  • wake
  • put by hash
Um backend de sync no estilo Dropbox. Os clientes dividem os arquivos em blocos com hash, enviam só os que faltam no servidor e fazem commit de uma lista de blocos num journal de metadados; os outros dispositivos ficam sabendo por long poll e baixam só os blocos que faltam.

Como ler:

  • O notebook à esquerda faz commit de uma lista de blocos no serviço de metadados e recebe de volta os hashes que faltam no servidor.
  • Os uploads de blocos vão para o serviço de blocos e chegam ao armazenamento de objetos, indexados por hash.
  • O serviço de metadados anexa a nova versão ao journal do namespace no Postgres e publica a mudança.
  • O serviço de notificações responde ao long poll do celular; o celular lista as mudanças depois do seu cursor e baixa só os blocos que faltam.
  • O bloco Markdown lista em ordem os seis passos do protocolo.

Desenhar sistemas de sincronização com clareza

  • Desenhe metadados e conteúdo como caminhos separados. Armazenamentos diferentes, escala diferente, modos de falha diferentes.
  • Numere os passos do protocolo nas arestas. Commit, hashes que faltam, upload, novo commit: a ordem é o argumento de correção.
  • Mostre o segundo dispositivo. Sincronização tem a ver tanto com quem lê quanto com quem escreve; um diagrama com um cliente só esconde as notificações e os cursores.
  • Rotule a chave de partição: namespace para os metadados, hash para os blocos.

Para uma abordagem geral, veja o exemplo de diagrama de design de sistemas. O lado de push se parece com o design de sistema de notificações, uploads em blocos para armazenamento de objetos também aparecem no diagrama de arquitetura de streaming de vídeo, e o particionamento dos metadados segue o diagrama de sharding de banco de dados.

Monte no Archboard

  1. Entre com Google ou GitHub e crie um diagrama.
  2. Adicione dois nós Dispositivo do pack Rede: um notebook e um celular.
  3. Adicione uma Moldura do pack Básico para o backend de sincronização, com um Gateway de API do pack Nuvem na borda.
  4. Dentro da moldura, adicione três nós Instância de computação do pack Nuvem para os serviços de metadados, blocos e notificações, um nó Postgres do pack Dados para o journal, um nó Armazenamento de objetos do pack Nuvem para os blocos e um nó Kafka do pack Dados para os eventos do journal.
  5. Numere as arestas na ordem do protocolo: commit, hashes que faltam, upload, novo commit, notificação, listagem e download.
  6. Adicione um bloco Markdown do pack Básico com o protocolo e a capacidade.

O exemplo usa 11 nós de diagrama e fica dentro do plano Free.

Perguntas frequentes

Por que os serviços de sincronização dividem arquivos em blocos? Os blocos permitem que o cliente envie só o que mudou, retome transferências interrompidas e deduplique dados idênticos. O hash de um bloco é a sua identidade, então o servidor sabe na hora quais blocos já tem.

Devo usar blocos de tamanho fixo ou definidos pelo conteúdo? Blocos de tamanho fixo são mais simples e mais baratos de calcular. A divisão pelo conteúdo mantém as fronteiras estáveis quando bytes são inseridos no meio de um arquivo, o que economiza banda em arquivos editados no lugar.

Como um cliente sabe o que mudou no servidor? Ele guarda um cursor por namespace e pede ao serviço de metadados todas as entradas do journal depois dele. Um long poll ou uma notificação push avisa quando perguntar.

Como edições conflitantes são tratadas? Cada commit informa a versão em que se baseou. Se outro dispositivo fez commit antes, o commit falha, e o cliente mantém a sua edição como cópia em conflito em vez de sobrescrever a outra.

A deduplicação funciona entre usuários? Pode funcionar, mas pode revelar que outra conta guardou o mesmo arquivo, e não funciona com criptografia por usuário. Muitos serviços limitam a deduplicação a uma conta ou pasta compartilhada.

Comece grátis — um assento, sem cartão de crédito.

Artigos relacionados

← Voltar ao blog

Abra um canvas em um minuto

Entre com Google ou GitHub. Grátis para um assento. Faça upgrade quando a equipe entrar.