NubliVaultby Nublify
Blog

· 6 min de leitura

Fizemos backup de 510 mil arquivos e o banco de dados não cresceu uma linha

Um teste de escala com números, método e limites declarados — incluindo o que não funcionaria no seu caso.

Quem faz backup de object storage com muitos arquivos pequenos esbarra em dois custos, e o segundo é o que estraga o mês: o dinheiro que vai embora em requisições, e o banco de dados que cresce junto com o número de arquivos até virar o gargalo.

Este texto é o relato de um teste que fizemos para resolver o segundo problema. Tem os números, o método e — a parte que mais importa — o que não funcionaria em outro contexto.

O problema do índice

Para recuperar um arquivo individual de dentro de um pacote arquivado, é preciso saber em qual pacote ele está e em que posição. Esse índice tem uma entrada por arquivo. Com 510 mil arquivos, são 510 mil entradas. Com um bilhão, um bilhão.

A saída óbvia é uma tabela no banco. Ela funciona, e nós a usamos por um bom tempo: com Postgres bem dimensionado, medimos 16 a 17 mil inserções por segundo de forma estável até 30 milhões de linhas. O detalhe que quase passou batido é que a estabilidade dependia da RAM — com 15 GB a taxa degradava ao longo da carga; com 32 GB ficava constante. Ou seja: funciona, mas te obriga a crescer o banco junto com o acervo do cliente, para sempre.

A alternativa que testamos foi tirar o índice do banco. O manifesto de cada pacote vira um objeto no próprio armazenamento, ao lado dos dados, e o banco guarda só o que é pequeno e muda pouco: clientes, jobs, pacotes. A busca lê o manifesto sob demanda e mantém um cache local.

O que foi medido

Uma VM de 4 vCPU e 32 GB de RAM na Oracle Cloud, lendo de buckets OCI e gravando na AWS. Sem simulação em nenhuma ponta — origem real, destino real.

Massa sintética de 510 mil arquivos de ~150 KB, distribuída em três clientes e três buckets, sendo um deles de propósito espalhado por dois buckets diferentes, para testar isolamento.

Empacotamento

Arquivos processados 510.000
Pacotes gerados 16
Falhas 0
Cliente de 300 mil arquivos 40min52s (~122 arquivos/s)
Cliente de 150 mil arquivos 21min (~118 arquivos/s)

O banco durante tudo isso

CPU do Postgres 0,06%
Linhas na tabela de índice 1.227, antes e depois
Tamanho da tabela 16 MB, antes e depois

É esse o número do título. Meio milhão de arquivos entraram no cofre e a tabela de índice não recebeu uma linha. O banco ficou praticamente parado enquanto a máquina empacotava, comprimia e subia.

Recuperação

Índice fora do banco só vale se a recuperação continuar rápida. Medimos os dois casos:

  • Um arquivo específico, dentro de um cliente com 300 mil arquivos e nenhuma linha no banco: resposta em segundos.
  • Cliente inteiro: 30.000 de 30.000 arquivos em 2min20s, cerca de 260 arquivos por segundo, todos confirmados no destino.

Ensaio de desastre

O teste que interessa não é o backup, é o dia em que a origem não existe mais. Restauramos a partir apenas do arquivo na AWS, sem tocar na nuvem de origem, e comparamos o sha256 de cada arquivo restaurado com o checksum gravado no manifesto. Idênticos, bit a bit.

Quando um servidor morre no meio

O empacotamento roda em frota de máquinas preemptíveis — as que a nuvem pode desligar a qualquer momento, e que custam uma fração do preço. Matamos duas de propósito durante a carga. O vigia detectou e lançou substitutas em 35 segundos, e o trabalho continuou de onde estava.

Até onde isso escala

A ~120 arquivos por segundo por worker, um bilhão de arquivos levaria cerca de 96 dias com um worker, ou 10 dias com dez. Numa campanha separada, com 1 TB e ~7 milhões de arquivos, medimos ~110 arquivos/s por processo e ~330/s com quatro processos na mesma máquina de 4 núcleos — o gargalo ali é CPU na concatenação, não rede nem disco.

O que isto não prova

Esta é a parte que costuma faltar nos benchmarks publicados, e sem ela os números acima valem pouco.

Os arquivos eram sintéticos e de tamanho parecido (~150 KB). Acervo real tem distribuição irregular, e arquivos muito grandes mudam o comportamento do empacotamento. Espere números diferentes.

Tirar o índice do banco tem um preço, e ele aparece na primeira busca. Se o manifesto não estiver no cache local, é preciso buscá-lo do armazenamento antes de responder. Em cache frio a primeira consulta é mais lenta que um SELECT. Numa revisão de código deste próprio trabalho encontramos um defeito exatamente aí — a busca em cache frio não reidratava o manifesto — que foi corrigido e revalidado antes de considerarmos o teste válido. Se o seu padrão de uso for busca constante e espalhada, a tabela no banco pode ser a escolha certa.

A taxa por worker é o que é. ~120 arquivos/s não é rápido em termos absolutos; o ganho vem de rodar muitos workers baratos em paralelo, não de cada um ser veloz. Se você precisa de janela curta com poucos nós, esta arquitetura não te serve.

Não medimos custo em dinheiro neste teste. A economia em requisições vem da agregação — 510 mil objetos viraram 16 — e essa conta é aritmética direta, mas não é o que este teste mediu. Não confunda uma coisa com a outra.

Como reproduzir

O caminho é o mesmo para qualquer ferramenta que agregue arquivos em pacotes:

  1. Gere massa sintética com tamanho e quantidade conhecidos, em mais de um bucket, com pelo menos um cliente espalhado entre dois — é onde falhas de isolamento aparecem.
  2. Meça o banco antes e depois: contagem de linhas, tamanho em disco e CPU durante a carga. Sem o "antes", o "depois" não significa nada.
  3. Teste a recuperação nos dois extremos: um arquivo só e o acervo inteiro.
  4. Faça o ensaio de desastre desligando a origem, e compare checksums. Backup que nunca foi restaurado não é backup.
  5. Mate um nó no meio da carga, de propósito.

O passo 5 é o que quase ninguém faz, e é o que separa um número de laboratório de uma arquitetura que aguenta produção.


Este teste foi feito com o NubliVault, da Nublify. Os números são do nosso laboratório, não de um cliente — justamente para que possam ser questionados e refeitos. Se você rodar algo parecido e chegar a números diferentes, queremos saber.