Whitepaper técnico — PodHeitor HPC Backup Plugin para Bacula

Whitepaper técnico — PodHeitor HPC Backup Plugin para Bacula. Arquitetura interna, modelo de paralelismo, sharding, integração com filesystems paralelos (Lustre, GPFS, BeeGFS, CephFS, WekaFS), benchmarks de campo e topologias de deployment para clusters HPC com bilhões de arquivos.

Documento técnico complementar à página do plugin PodHeitor HPC. Para download em PDF do whitepaper executivo: PodHeitor HPC Whitepaper (PDF).

1. Problema: Bacula não foi desenhado para HPC

O File Daemon stock do Bacula caminha o filesystem single-threaded. Em findlib/find_one.c, a recursão é breaddir/readdir seguida de save_file() síncrono. Esse design é razoável para um servidor de aplicação típico — mas em um Lustre com 1 bilhão de arquivos, a aritmética não fecha:

  • Custo médio de readdir+lstat em Lustre quente: ~150 µs/arquivo (limitado por RPC ao MDT).
  • 1 × 10⁹ arquivos × 150 µs = ~41 horas só de scan de metadados, antes mesmo de ler 1 byte de data.
  • Bacula Enterprise 18.2 ships plugins para HDFS, Quobyte, NDMP, NetApp e Nutanix — nenhum para os filesystems paralelos que de fato sustentam HPC moderno.

O PodHeitor HPC Backup Plugin endereça essa lacuna com paralelismo agressivo dentro do job, sharding de namespace cross-job, e drivers de changelog específicos por filesystem.

2. Modelo arquitetural

O plugin segue o padrão PodHeitor de cdylib + backend standalone, com PTCOMM (length-tagged framing em stdin/stdout) entre os dois processos. A motivação é tripla:

  1. Crash isolation. Um panic no walker mata o backend, não o bacula-fd. O cdylib observa EOF na pipe, reporta o job como falhado, e o FD continua atendendo outros jobs.
  2. Liberdade de paralelismo. O backend pode spawnar arbitrárias threads rayon sem violar o contrato Bacula de “uma thread por bpContext“.
  3. License firewall. O loader do Bacula gateia em info->plugin_license em runtime. O subprocesso nunca toca o ABI do Bacula; só o cdylib o faz, e exclusivamente via crate bacula-fd-abi com declarações extern "C" independentes — sem source AGPLv3 do Bacula vinculado estaticamente.

2.1 Crate map

Crate Papel Fase
hpc-cdylib .so carregado pelo bacula-fd. Adapter metaplugin-rs minimalista. 1
hpc-backend Binário Rust standalone. Producer PTCOMM; hospeda walker paralelo, sharder, drivers de changelog/stripe. 1
hpc-walker Walker POSIX paralelo (rayon work-stealing + canal crossbeam bounded). 2
hpc-shard Estratégias de sharding de namespace. 3
hpc-changelog Drivers Lustre/GPFS/CephFS/BeeGFS atrás de cargo features. 4-6
hpc-stripe Readers paralelos stripe-aware (Lustre llapi, GPFS NSD). 4-5
hpc-ptcomm Wire format entre cdylib e backend. 1
hpc-cli CLI operacional podheitor-hpc (gen-fileset, shard-probe, bench). 3

2.2 Threading model

  • bacula-fd — uma thread por job. O cdylib roda nessa thread e nunca pode bloqueá-la por longos períodos.
  • hpc-cdylib — single-threaded por contrato Bacula. Drena frames PTCOMM sincronamente e traduz cada frame em um ciclo save_pkt/pluginIO.
  • hpc-backend — spawna um pool rayon dimensionado em nproc. Todo trabalho de metadados é paralelo. Um único emitter PTCOMM single-threaded consome o canal MPSC bounded e escreve no stdout — esse é o único ponto de serialização do lado do producer.

O resultado prático: o gargalo deixa de ser o walker do FD e passa a ser o consumer (a própria thread do FD drenando PTCOMM). O backend produz no line rate do filesystem, e o FD passa a ser o limite — um limite muito mais alto.

3. Por que sharding é mandatório em escala HPC

Mesmo com paralelismo perfeito dentro do backend, há um teto: o bacula-fd mantém uma única conexão SD por job. Bacula 15.x não tem multiplexing de stream dentro do job. A única forma de multiplicar throughput de saída é rodar N jobs concorrentes, com Maximum Concurrent Jobs aplicado em Director, Job, Client e Storage.

O crate hpc-shard torna isso uma alavanca operacional:

podheitor-hpc gen-fileset --strategy inode-hash --shards 4 
    --path /lustre/scratch --output /opt/bacula/etc/conf.d/

Produz 4 FileSets e 4 snippets de Job, cada um com:

Plugin = "podheitor-hpc:path=/lustre/scratch;shard=K/4;mode=lustre"

Rode-os em paralelo para 4× streams SD de saída.

3.1 Estratégias de sharding

Estratégia Quando usar Notas
None Shard único, dataset pequeno, config manual Default Phase 0
InodeHash { shard, of } Default paralelo geral — distribuição uniforme xxh3-based; determinístico entre runs
Subtree { path } Subtrees diferentes merecem políticas diferentes Compõe bem com InodeHash per-subtree
MtimeBucket { shard, of } Incrementais espalhados em N shards Pareie com Level = Incremental
LustreMdt { shard, of } Lustre — pin um shard por MDT Lê info MDT de lfs df -i. Cai para InodeHash se MDT info ausente.

3.2 A pegadinha do volume serializado

Um detalhe que destrói o ganho de sharding ingênuo: o SD do Bacula serializa block writes por volume. Se todos os N shards aterrissarem no mesmo volume, você obtém N walkers FD paralelos alimentando 1 SD writer serial — você obtém corretude (paridade, união de cobertura) mas não wall-time speedup.

A medição de laboratório Phase 3 capturou exatamente esse comportamento: 4 shards no mesmo Volume → 13s vs baseline single-stream de 10s (i.e., mais lento). Para destravar o speedup que a arquitetura promete, escolha uma das três topologias:

Topologia Setup Tradeoff
Pool por shard Um Pool por shard, com sequência de volumes própria (LabelFormat = "Shard-{N}-"). gen-fileset emite um snippet de Pool junto com FileSet/Job. Mais limpo; volumes nunca colidem.
MaximumVolumeJobs = 1 Set no Pool default existente. Bacula aloca volume novo por job. Mais leve; produz muitos volumes pequenos, harder to manage long-term.
Múltiplos Storages → devices distintos Round-robin gen-fileset --storage File1,File2,File3,File4. Cada Storage aponta para SD Device diferente, com seu próprio pool de volumes. Melhor quando o SD tem múltiplos discos/spindles para fan out.

4. Drivers de changelog (incrementais nativos)

O salto de “scan completo” para “incremental real” depende de não fazer 10⁹ stat() calls. Cada filesystem paralelo expõe seu próprio mecanismo de changelog; o crate hpc-changelog abstrai isso atrás do trait ChangelogSource:

Filesystem Mecanismo O que captura
Lustre 2.14+ lfs changelog create/unlink/rename/rmdir/setattr per-MDT, com cookie de consumer registrado para garantir liberação correta dos slots
IBM Spectrum Scale (GPFS) 5.x mmapplypolicy com policy template Arquivos com MODIFICATION_TIME > X via metadata scan paralelo nativo do GPFS
CephFS rstats + rctime recursivos Subtrees inteiros podem ser podados quando rctime <= last_run — corte logarítmico na árvore
BeeGFS 7.x Metadata-shard scan Cada metadata target é varrido em paralelo; sem changelog formal, mas o shard scan já é altamente paralelizável
POSIX (fallback) PosixWalk com mtime >= last_run Quando nenhum dos acima está disponível

5. Reader paralelo stripe-aware

Em Lustre, um arquivo “grande” é striped em N OSTs. Lê-lo sequencialmente via syscall read() no client gera RPCs serializados por OST — desperdiçando paralelismo nativo. O crate hpc-stripe usa llapi_layout_get_by_path para descobrir o layout de stripe e emite N reads concorrentes (um por OST), reagrupando in-order via PTCOMM.

Em GPFS o equivalente é a API NSD; em CephFS/BeeGFS/WekaFS o reader cai para POSIX paralelo por chunk. Ganho típico em arquivos > 1 GiB com stripe count = 4: 3.2-3.8× sobre leitura sequencial (limite teórico 4×, perda fica no overhead de reassembly).

6. Benchmarks de campo (Phase 10)

Os benchmarks vivem no documento STRESS_RESULTS.md do projeto e são reproduzíveis via scripts/stress-1m-files.sh. Resultado representativo:

Data FS Mode Files Walker Backup Throughput
2026-05-03 Lustre (1-OST lab) lustre 100,000 9.97 s 135.0 s 740 files/s

Notas críticas para interpretação:

  • Walker isolado: ~10K files/s — o ratio Phase 2 (7× mais rápido que find_one_file) se mantém, porque a lógica do walker é idêntica em ext4 e Lustre.
  • Backup full (walker + read + PTCOMM): ~740 files/s nesta hardware — read-bound em OST único. Em produção com 4-8 OSTs e stripe-aware reader, throughput sobe linearmente até o teto de RPC do MDT.
  • Extrapolação: 10M arquivos ≈ 3h45m no mesmo hardware single-OST — bem dentro da janela noturna de 8h da AC original Phase 10. Em hardware HPC real (8 OSTs, 32 vCPU, 128 GB RAM no FD), o esperado é 30-90 minutos.
  • Zero erros, zero arquivos pulados em validação de paridade catalog vs find.

6.1 Memory budget

O canal MPSC é bounded (default channel_depth = 8192 entries). Cada entry carrega metadados + um pequeno prefixo do arquivo; arquivos grandes streamam em chunks, não buffered. Target RSS para o backend:

  • < 256 MiB em 1 M arquivos
  • < 1 GiB em 1 B arquivos

Se RSS cresce, o suspeito quase sempre é o produto channel_depth × avg entry size. Reduza channel_depth, não workers (reduzir workers diminui throughput sem economizar RAM).

7. Phase 7 — scan no nó de compute via Slurm

Default: o backend roda no mesmo processo que o cdylib spawnou (no host do FD). Adicionar scheduler=slurm ao plugin command desloca o walker paralelo / drain de changelog / stripe reader para uma alocação Slurm, enquanto o cdylib permanece no login node:

Plugin = "podheitor-hpc:path=/lustre/scratch;shard=0/4;mode=lustre;
          scheduler=slurm;partition=backup;cpus=8;mem=16G;time=1h"

O fluxo end-to-end:

  1. O cdylib parseia o command e execs podheitor-hpc-backend backup --scheduler=slurm --partition=backup --cpus=8 ....
  2. Essa instância de backend vira o submitter: bind em socket AF_UNIX no FS compartilhado (sob o root do FileSet, ou $PODHEITOR_HPC_SHARED_DIR), depois pede ao SlurmDriver para sbatch --wrap uma invocação worker no cluster.
  3. SchedulerDriver::submit retorna o JobId Slurm; o submitter bloqueia em accept(2).
  4. O worker alocado pelo Slurm conecta no socket, faz dup2 do socket fd sobre seu stdout, e roda o mesmo codepath de backup — frames PTCOMM fluem para o socket como se fosse stdout.
  5. O submitter relaya bytes verbatim sobre seu próprio stdout, que o FilePuller do cdylib drena. O relay é opaco à framing PTCOMM; cap é o buffer de socket + a “shovel” de 64 KiB do relay() (≥ 5 GiB/s em 10 GbE).

7.1 Throttle Slurm-aware

O daemon podheitor-hpc throttle-daemon polla squeue -t R -o "%Q" a cada 10s; em contenção, reescreve /var/lib/podheitor-hpc/throttle.toml. Cada worker em execução observa o mtime desse arquivo em poll de 5s e aplica o novo {rayon_threads, read_buffer_kib, pause_ms} mid-run. O TOML é escrito write-then-rename, então readers nunca veem conteúdo torn.

Por que arquivo (e não Unix socket ou signal):

  • Operadores podem cat e editar durante incidentes.
  • Sobrevive a restart do daemon.
  • One-writer/many-readers não precisa de protocolo.
  • Já pagamos 5s de jitter — adicionar inotify gateia em filesystems que não propagam cross-mount (algumas configs Lustre client).

8. Restripe-on-restore

Backup preserva bytes — mas em HPC, preservar striping é tão importante quanto. Um arquivo de 100 GB stripado em 8 OSTs e restaurado para 1 OST perde 8× a banda de leitura. O plugin captura o layout original via llapi_layout_get_by_path e o serializa como RestoreObject Bacula. Na restauração, antes do primeiro byte de data, o cdylib aplica llapi_layout_file_create para recriar o stripe layout exato — depois grava bytes normalmente.

9. Anti-patterns documentados

  • Não rode o plugin contra / de um filesystem paralelo. Sempre pin para subtree (Subtree { path }) ou shard com inode-hash. Backup de inodes raiz do MDT corre risco de contention com RPCs de metadados de produção.
  • Não desabilite panic = "abort". Um panic atravessando FFI via pluginIO é UB. O profile release aborta — comportamento intencional.
  • Não rode o FD em OSS Lustre / NSD GPFS server. O plugin é client HPC; deploy em nó de storage só é suportado via Slurm submit path (Phase 7), onde o scan roda em compute job.

10. License posture

O plugin distribui sob LicenseRef-PodHeitor-Proprietary. Não vincula estaticamente nenhum source AGPLv3 do Bacula. O binding é exclusivamente via extern "C" independente no crate bacula-fd-abi. O campo info-&gt;plugin_license = "Bacula AGPLv3" satisfaz apenas o gate de runtime do FD loader — não há transitive AGPL.

Pronto para avaliar?

Trial gratuito de 30 dias para workloads HPC qualificadas (Lustre, GPFS, BeeGFS, CephFS, WekaFS). Garantimos no mínimo 50% de desconto vs Bacula Enterprise, Veeam ou Commvault, com mais funcionalidades inclusas.

Heitor Faria — Fundador, PodHeitor International
[email protected]
☎ +1 (789) 726-1749 · +55 (61) 98268-4220 (WhatsApp)
🔗 Página do plugin PodHeitor HPC · Whitepaper PDF (executivo)

Disponível em: pt-brPortuguêsenEnglish (Inglês)esEspañol (Espanhol)

Deixe um comentário