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+lstatem 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:
- 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. - Liberdade de paralelismo. O backend pode spawnar arbitrárias threads rayon sem violar o contrato Bacula de “uma thread por
bpContext“. - License firewall. O loader do Bacula gateia em
info->plugin_licenseem runtime. O subprocesso nunca toca o ABI do Bacula; só o cdylib o faz, e exclusivamente via cratebacula-fd-abicom declaraçõesextern "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 ciclosave_pkt/pluginIO.hpc-backend— spawna um pool rayon dimensionado emnproc. 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:
- O cdylib parseia o command e
execspodheitor-hpc-backend backup --scheduler=slurm --partition=backup --cpus=8 .... - 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 aoSlurmDriverparasbatch --wrapuma invocação worker no cluster. SchedulerDriver::submitretorna o JobId Slurm; o submitter bloqueia emaccept(2).- O worker alocado pelo Slurm conecta no socket, faz
dup2do socket fd sobre seu stdout, e roda o mesmo codepath debackup— frames PTCOMM fluem para o socket como se fosse stdout. - O submitter relaya bytes verbatim sobre seu próprio stdout, que o
FilePullerdo cdylib drena. O relay é opaco à framing PTCOMM; cap é o buffer de socket + a “shovel” de 64 KiB dorelay()(≥ 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
cate editar durante incidentes. - Sobrevive a restart do daemon.
- One-writer/many-readers não precisa de protocolo.
- Já pagamos 5s de jitter — adicionar
inotifygateia 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 cominode-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 viapluginIOé 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->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:
Português
English (Inglês)
Español (Espanhol)