Whitepaper técnico — PodHeitor HPC Backup Plugin para Bacula

Whitepaper técnico — PodHeitor HPC Backup Plugin para Bacula. Arquitectura interna, modelo de paralelismo, sharding, integración con sistemas de archivos paralelos (Lustre, GPFS, BeeGFS, CephFS, WekaFS), benchmarks de campo y topologías de despliegue para clusters HPC con espacios de nombres de mil millones de archivos.

Documento técnico complementario a la página del plugin PodHeitor HPC. Para descargar el PDF ejecutivo: PodHeitor HPC Whitepaper (PDF).

1. El problema: Bacula no fue diseñado para HPC

El File Daemon stock de Bacula recorre el sistema de archivos single-threaded. En findlib/find_one.c, la recursión es breaddir/readdir seguida de save_file() síncrono. Ese diseño es razonable para un servidor de aplicaciones típico — pero en un Lustre con mil millones de archivos, la aritmética no cierra:

  • Costo medio de readdir+lstat en Lustre caliente: ~150 µs por archivo (limitado por RPC al MDT).
  • 1 × 10⁹ archivos × 150 µs = ~41 horas solo de scan de metadatos, antes de leer un solo byte de datos.
  • Bacula Enterprise 18.2 ofrece plugins para HDFS, Quobyte, NDMP, NetApp y Nutanix — ninguno para los sistemas de archivos paralelos que de hecho ejecutan HPC moderno.

El PodHeitor HPC Backup Plugin cierra ese gap con paralelismo agresivo dentro del job, sharding de namespace cross-job y drivers de changelog específicos por filesystem.

2. Modelo arquitectónico

El plugin sigue el patrón PodHeitor de cdylib + backend standalone, con PTCOMM (length-tagged framing en stdin/stdout) entre los dos procesos. La motivación es triple:

  1. Aislamiento de crashes. Un panic en el walker mata al backend, no al bacula-fd. El cdylib observa EOF en la pipe, reporta el job como fallido, y el FD continúa atendiendo otros jobs.
  2. Libertad de paralelismo. El backend puede spawnar threads rayon arbitrarias sin violar el contrato Bacula de «una thread por bpContext«.
  3. License firewall. El loader de Bacula gatea en info->plugin_license en runtime. El subproceso nunca toca el ABI de Bacula; solo el cdylib lo hace, y exclusivamente via crate bacula-fd-abi con declaraciones extern "C" independientes — sin source AGPLv3 de Bacula vinculado estáticamente.

2.1 Mapa de crates

Crate Rol Fase
hpc-cdylib .so cargado por bacula-fd. Adapter metaplugin-rs minimalista. 1
hpc-backend Binario Rust standalone. Producer PTCOMM; aloja walker paralelo, sharder, drivers de changelog/stripe. 1
hpc-walker Walker POSIX paralelo (rayon work-stealing + canal crossbeam bounded). 2
hpc-shard Estrategias de sharding de namespace. 3
hpc-changelog Drivers Lustre/GPFS/CephFS/BeeGFS detrás de cargo features. 4-6
hpc-stripe Readers paralelos stripe-aware (Lustre llapi, GPFS NSD). 4-5
hpc-ptcomm Wire format entre cdylib y backend. 1
hpc-cli CLI operacional podheitor-hpc (gen-fileset, shard-probe, bench). 3

2.2 Modelo de threading

  • bacula-fd — una thread por job. El cdylib corre en esa thread y nunca puede bloquearla por períodos largos.
  • hpc-cdylib — single-threaded por contrato Bacula. Drena frames PTCOMM síncronamente y traduce cada frame en un ciclo save_pkt/pluginIO.
  • hpc-backend — spawna un pool rayon dimensionado a nproc. Todo el trabajo de metadatos es paralelo. Un único emitter PTCOMM single-threaded consume el canal MPSC bounded y escribe a stdout — ese es el único punto de serialización del lado del producer.

El resultado práctico: el cuello de botella deja de ser el walker del FD y pasa a ser el consumer (la propia thread del FD drenando PTCOMM). El backend produce a line rate del filesystem, y el FD pasa a ser el límite — un límite mucho más alto.

3. Por qué sharding es obligatorio en escala HPC

Aún con paralelismo perfecto dentro del backend, hay un techo: bacula-fd mantiene una sola conexión SD por job. Bacula 15.x no tiene multiplexing de stream dentro del job. La única forma de multiplicar throughput de salida es ejecutar N jobs concurrentes, con Maximum Concurrent Jobs aplicado en Director, Job, Client y Storage.

El crate hpc-shard convierte esto en una palanca operativa:

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

Produce 4 FileSets y 4 snippets de Job, cada uno con:

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

Ejecútelos en paralelo para 4× streams SD de salida.

3.1 Estrategias de sharding

Estrategia Cuándo usar Notas
None Shard único, dataset pequeño, config manual Default Phase 0
InodeHash { shard, of } Default paralelo general — distribución uniforme xxh3-based; determinista entre runs
Subtree { path } Subtrees diferentes merecen políticas diferentes Compone bien con InodeHash per-subtree
MtimeBucket { shard, of } Incrementales en muchos shards Pareée con Level = Incremental
LustreMdt { shard, of } Lustre — pin un shard por MDT Lee info MDT de lfs df -i. Cae a InodeHash si MDT info ausente.

3.2 La trampa del volumen serializado

Un detalle que destruye la ganancia de sharding ingenua: el SD de Bacula serializa block writes por volumen. Si los N shards aterrizan en el mismo volumen, obtienes N walkers FD paralelos alimentando 1 SD writer serial — obtienes corrección (paridad, unión de cobertura) pero no wall-time speedup.

La medición de laboratorio Phase 3 capturó exactamente eso: 4 shards en el mismo Volume → 13s vs baseline single-stream de 10s (es decir, más lento). Para destrabar el speedup que la arquitectura promete, elija una de tres topologías:

Topología Setup Tradeoff
Pool por shard Un Pool por shard, con secuencia de volúmenes propia (LabelFormat = "Shard-{N}-"). gen-fileset emite snippet de Pool junto con FileSet/Job. Más limpio; volúmenes nunca colisionan.
MaximumVolumeJobs = 1 Set en el Pool default existente. Bacula asigna volumen nuevo por job. Más liviano; produce muchos volúmenes pequeños, harder to manage long-term.
Múltiples Storages → devices distintos Round-robin gen-fileset --storage File1,File2,File3,File4. Cada Storage apunta a SD Device diferente, con su propio pool de volúmenes. Mejor cuando el SD tiene múltiples discos/spindles para fan out.

4. Drivers de changelog (incrementales nativos)

El salto de «scan completo» a «incremental real» depende de no hacer 10⁹ stat() calls. Cada filesystem paralelo expone su propio mecanismo de changelog; el crate hpc-changelog abstrae eso detrás del trait ChangelogSource:

Filesystem Mecanismo Qué captura
Lustre 2.14+ lfs changelog create/unlink/rename/rmdir/setattr per-MDT, con cookie de consumer registrado para garantizar liberación correcta de slots
IBM Spectrum Scale (GPFS) 5.x mmapplypolicy con policy template Archivos con MODIFICATION_TIME > X via metadata scan paralelo nativo de GPFS
CephFS rstats + rctime recursivos Subtrees enteros pueden podarse cuando rctime <= last_run — corte logarítmico en el árbol
BeeGFS 7.x Metadata-shard scan Cada metadata target se recorre en paralelo; sin changelog formal, pero el shard scan ya es altamente paralelizable
POSIX (fallback) PosixWalk con mtime >= last_run Cuando ninguno de los anteriores está disponible

5. Reader paralelo stripe-aware

En Lustre, un archivo «grande» está striped en N OSTs. Leerlo secuencialmente via syscall read() en el client genera RPCs serializados por OST — desperdiciando paralelismo nativo. El crate hpc-stripe usa llapi_layout_get_by_path para descubrir el layout de stripe y emite N reads concurrentes (uno por OST), reagrupando in-order via PTCOMM.

En GPFS el equivalente es la API NSD; en CephFS/BeeGFS/WekaFS el reader cae a POSIX paralelo por chunk. Ganancia típica en archivos > 1 GiB con stripe count = 4: 3.2-3.8× sobre lectura secuencial (límite teórico 4×, la pérdida es overhead de reassembly).

6. Benchmarks de campo (Phase 10)

Los benchmarks viven en STRESS_RESULTS.md del proyecto y son reproducibles via scripts/stress-1m-files.sh. Resultado representativo:

Fecha FS Modo Archivos Walker Backup Throughput
2026-05-03 Lustre (1-OST lab) lustre 100,000 9.97 s 135.0 s 740 archivos/s

Notas críticas para interpretación:

  • Walker aislado: ~10K archivos/s — el ratio Phase 2 (7× más rápido que find_one_file) se mantiene, porque la lógica del walker es idéntica en ext4 y Lustre.
  • Backup full (walker + read + PTCOMM): ~740 archivos/s en este hardware — read-bound en OST único. En producción con 4-8 OSTs y stripe-aware reader, throughput escala linealmente hasta el techo de RPC del MDT.
  • Extrapolación: 10M archivos ≈ 3h45m en el mismo hardware single-OST — bien dentro de la ventana nocturna de 8h del AC original Phase 10. En hardware HPC real (8 OSTs, 32 vCPU, 128 GB RAM en el FD), esperar 30-90 minutos.
  • Cero errores, cero archivos saltados en validación de paridad catalog vs find.

6.1 Memory budget

El canal MPSC es bounded (default channel_depth = 8192 entries). Cada entry carga metadatos + un pequeño prefijo del archivo; archivos grandes streamean en chunks, no buffered. Target RSS para el backend:

  • < 256 MiB en 1 M archivos
  • < 1 GiB en 1 B archivos

Si RSS crece, el sospechoso casi siempre es el producto channel_depth × avg entry size. Reduzca channel_depth, no workers (reducir workers baja throughput sin ahorrar RAM).

7. Phase 7 — scan en nodo de compute via Slurm

Default: el backend corre en el mismo proceso que el cdylib spawnó (en el host del FD). Agregar scheduler=slurm al plugin command desplaza el walker paralelo / drain de changelog / stripe reader a una asignación Slurm, mientras el cdylib permanece en el login node:

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

Flujo end-to-end:

  1. El cdylib parsea el command y execs podheitor-hpc-backend backup --scheduler=slurm --partition=backup --cpus=8 ....
  2. Esa instancia de backend se vuelve el submitter: bind en socket AF_UNIX en el FS compartido (bajo el root del FileSet, o $PODHEITOR_HPC_SHARED_DIR), luego pide al SlurmDriver hacer sbatch --wrap de una invocación worker en el cluster.
  3. SchedulerDriver::submit retorna el JobId Slurm; el submitter bloquea en accept(2).
  4. El worker asignado por Slurm se conecta al socket, hace dup2 del socket fd sobre su stdout, y corre el mismo codepath de backup — frames PTCOMM fluyen al socket como si fuera stdout.
  5. El submitter relayea bytes verbatim sobre su propio stdout, que el FilePuller del cdylib drena. El relay es opaco al framing PTCOMM; cap es lo que el buffer de socket + la «shovel» de 64 KiB del relay() aguante (≥ 5 GiB/s en 10 GbE).

7.1 Throttle Slurm-aware

El daemon podheitor-hpc throttle-daemon hace poll de squeue -t R -o "%Q" cada 10s; en contención reescribe /var/lib/podheitor-hpc/throttle.toml. Cada worker en ejecución observa el mtime de ese archivo en poll de 5s y aplica nuevo {rayon_threads, read_buffer_kib, pause_ms} mid-run. El TOML es write-then-rename, así readers nunca ven contenido torn.

Por qué archivo (y no Unix socket o signal):

  • Operadores pueden cat y editar durante incidentes.
  • Sobrevive a restart del daemon.
  • One-writer/many-readers no necesita protocolo.
  • Ya pagamos 5s de jitter — agregar inotify gatearía en filesystems que no lo propagan cross-mount (algunas configs Lustre client).

8. Restripe-on-restore

Backup preserva bytes — pero en HPC, preservar striping es tan importante como. Un archivo de 100 GB stripado en 8 OSTs y restaurado a 1 OST pierde 8× el ancho de banda de lectura. El plugin captura el layout original via llapi_layout_get_by_path y lo serializa como RestoreObject Bacula. En la restauración, antes del primer byte de data, el cdylib aplica llapi_layout_file_create para recrear el stripe layout exacto — luego graba bytes normalmente.

9. Anti-patrones documentados

  • No corra el plugin contra / de un filesystem paralelo. Siempre pin a un subtree (Subtree { path }) o shard con inode-hash. Backup de inodes raíz del MDT corre riesgo de contención con RPCs de metadatos de producción.
  • No deshabilite panic = "abort". Un panic atravesando FFI via pluginIO es UB. El profile release aborta — comportamiento intencional.
  • No corra el FD en OSS Lustre / NSD GPFS server. El plugin es client HPC; deploy en nodo de storage solo es soportado via Slurm submit path (Phase 7), donde el scan corre en compute job.

10. Postura de licencia

El plugin distribuye bajo LicenseRef-PodHeitor-Proprietary. No vincula estáticamente ningún source AGPLv3 de Bacula. El binding es exclusivamente via extern "C" independiente en el crate bacula-fd-abi. El campo info-&gt;plugin_license = "Bacula AGPLv3" satisface solo el gate de runtime del FD loader — no hay AGPL transitivo.

¿Listo para evaluar?

Trial gratuito de 30 días para workloads HPC calificadas (Lustre, GPFS, BeeGFS, CephFS, WekaFS). Garantizamos al menos 50% de descuento vs Bacula Enterprise, Veeam o Commvault, con más funcionalidades incluidas.

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

Disponível em: esEspañol

Deja una respuesta