Hoje foi um daqueles dias raros em que se trabalha o dia inteiro e, no final, a sensação não é de cansaço — é de propósito. Eu estou há mais de vinte anos dentro da área de Disaster Recovery. Backup, restore, replicação, integridade, soak testing, cadeias de chunks, tabelas de hash, RocksDB, ESM, manifestos. É uma área que tem o luxo cruel de só importar quando dá errado — e quando dá errado, ela importa mais do que qualquer feature de marketing já existiu.
Hoje passei o dia inteiro mergulhado no plugin PodHeitor de Global Deduplication para Bacula, escrito do zero em Rust. E eu queria contar por que ele ainda não foi lançado em GA — e por que isso é, na minha leitura, o exato oposto de fraqueza.
O que está pronto, em números reais
O plugin opera em dois modos. Modo de Storage: o Storage Daemon do Bacula intercepta os registros de dados, faz content-defined chunking via FastCDC, e armazena chunks únicos em um content-addressable store local. Em laboratório, sobre /usr/bin (408,8 MiB de cliente), o volume final em disco ficou com 4,45 MiB — 1,09% dos bytes do cliente. Compressão de 91×.
Modo Bothsides (deduplicação na ponta do cliente, com troca de hashes via TCP autenticado e cifrado): em um warm cache, sobre 420 MB de dados reais, transferimos pela rede 1,55 MB. Economia de banda medida: 99,63%. E em soak (R5/R6, dias consecutivos, ciclos backup→restore→md5 round-trip), o dedup_ratio bate consistentemente 1,0000 em corpora estáveis — todo chunk apresentado já era conhecido. Steady-state perfeito.
Os benchmarks micro do chunk-store, em uma máquina virtual modesta (8 vCPU QEMU, 3 GB RAM, virtio rotacional), mostram o store ingerindo a ~1,18 GB/s (chunks novos) e ~1,22 GB/s (chunks já deduplicados), com latência de recall em torno de 265 µs para 4 MB — equivalente a ~15 GB/s efetivos quando o cache de leitura ajuda. Esses números, em laboratório controlado, já superam implementações escritas em C que são referência no mercado. Rust não é mágica — é o resultado de zero overhead de runtime, layout de memória previsível, e o compilador te impedindo de cometer cinco classes de bug simultaneamente.
As técnicas que fazem o plugin ser rápido
Performance em deduplicação não vem só da linguagem — vem do que não precisamos fazer. Algumas decisões de arquitetura que estamos rodando:
- Bloom filter multi-camada (hot + cold). Antes de qualquer lookup em RocksDB, o chunk passa por um filtro de Bloom escalável memory-mapped. Camada quente: chunks recentes em RAM. Camada fria: todo histórico, mmap’d de disco. False-positive rate configurável (padrão 0,1%) — false-negative impossível por construção. Resultado: a esmagadora maioria dos chunks “definitivamente novos” nem chega a tocar SSD. Em workloads com 99% dedup, isso elimina ~99% das I/Os de índice que um esquema ingênuo faria.
- Segment Locality Tracking. Os chunks de uma mesma stream de backup (mesmo job, mesmo arquivo) são agrupados em segmentos contíguos no container físico. Quando o restore chega, o primeiro recall de um segmento aquece o cache; os subsequentes retornam instantaneamente. É o mesmo padrão de “dedup container locality” que sistemas comerciais consagrados usam, implementado nativamente, sem patches. Restore acelera por ordem de magnitude em corpora reais.
- Adaptive Chunk Tuner. O tuner amostra continuamente o
dedup_ratioem uma janela deslizante e ajusta o tamanho dos chunks: dedup alto (>80%) → chunks menores (granularidade mais fina); dedup baixo (<20%) → chunks maiores (menos overhead de índice). O sistema se ajusta sozinho ao perfil real dos dados do cliente, sem tuning manual. - AEAD framing customizado, sem TLS. Para Mode B, descartamos TLS 1.3 com mTLS e até TLS 1.3 PSK. Mantivemos as primitivas (HKDF-SHA256 + ChaCha20-Poly1305), removemos a state-machine TLS. Resultado: handshake em 0,1–0,3 ms contra 3–5 ms do cert-based TLS. Mesma confidencialidade e integridade. 45 KB de binário extra por host em vez de 200 KB. Um arquivo de 32 bytes por site, sem CA, sem renovação anual.
- RocksDB com WAL durável + containers append-only com CRC-32. Cada chunk tem CRC verificada na leitura. Se um disco corromper silenciosamente um byte, o sistema detecta na hora — não “oito meses depois quando o restore falha”.
- Vacuum / GC com integrity scan. Operação periódica que itera todo o índice, lê e re-verifica CRC de cada chunk. Detecta bit-rot, marca
ChunkStatus::Corrupted, e o operador é avisado antes do dia em que precisar do dado. - FastCDC variable-length chunking. Boundaries determinadas por conteúdo, não offset fixo. Insere um byte no meio de um arquivo? Apenas um chunk muda — não a cadeia inteira. Isso é o que faz dedup de VMs incrementais ter ratio de 99% em vez de 30%.
Por que ainda não lancei
Aqui é onde o post fica desconfortável de escrever — porque a resposta honesta é a oposta da que vende.
O componente é extremamente delicado. Deduplicação global toca em três coisas que não toleram bug:
- Reentrância sob stress. O daemon de dedup roda em paralelo com o File Daemon e o Storage Daemon do Bacula. Já encontramos e corrigimos casos onde uma sessão stale do daemon, em vez de falhar fechado, deixava 37.000 arquivos passarem como registros de 0 bytes — com status final Backup OK. Isso é o pior tipo de bug em backup: silent data loss com cara de sucesso. Foi corrigido (commit
21bdfa5, 30/04), validado com round-trip md5 em 25.071/25.071 arquivos, e blindado com fail-closed em três caminhos de fallback. - Crash recovery. Backup com FD morto no meio. SD morto no meio. Daemon de dedup reiniciado durante a ingestão. Cada um desses cenários foi exercitado em laboratório, com restore subsequente, com verificação byte-a-byte. Levamos meses para fechar B3a/B3b/B3c.
- Soak de 7 dias. Não é benchmark de uma hora. É rodar ciclo backup-restore-md5 todo dia por uma semana, em produção sintética, e provar que
RSSnão vaza,/gddnão cresce sem motivo, wire savings não regride, md5 não falha. Estamos no R6 — soak de 7 dias em curso. Day 1 fechado. Faltam 6.
Lançar antes disso seria covardia comercial vestida de agilidade.
A diferença que ninguém escreve em apresentação
Eu trabalhei carreira inteira ao lado, e às vezes contra, fornecedores europeus de backup que tratam General Availability como evento de marketing. Empresas que dependem de avaliações pagas para sustentar a percepção pública, que entregam usabilidade ruim sob a desculpa de “é assim que sempre foi”, que ameaçam comercialmente parceiros que ousam questionar uma decisão técnica, e que jogam código para produção em GA sabendo que tem regressão — porque o calendário de release importa mais que o cliente que vai restaurar amanhã.
Esse não é o jogo da PodHeitor. Eu não tenho acionistas estrangeiros pedindo trimestre. Eu tenho cliente final. Cliente que, num dia ruim, vai depender de que o backup que rodou ontem efetivamente restaure. Eu não vou olhar pra esse cliente daqui a um ano sabendo que assinei um GA prematuro porque uma planilha de OKR pedia.
O que vem agora
O R6 termina dia 7 de maio. Com round-trip clean nos 7 dias, RSS estável e /gdd sem crescimento descontrolado, o GDD entra em GA. Não antes. Mais alguns dias num projeto de 20 anos não me custam nada — uma falha silenciosa custaria muito a quem confia.
Quando lançar, lança certo: byte-exact roundtrip validado, soak de 7 dias documentado, fail-closed em todos os caminhos de fallback, chunk store com fsck próprio, runbook de DR para o próprio store de dedup (porque sim, fizemos um plano de recuperação de desastre do sistema de recuperação de desastre — é desse tipo de paranoia que estamos falando).
Esse é o trabalho. É lento. É detalhista. É chato pra quem quer o release de amanhã. E, vinte anos depois, é o único trabalho que ainda me realiza fazer.
— Heitor Faria
podheitor.com | [email protected]
Disponível em:
Português
English (Inglês)
Español (Espanhol)