Whitepaper técnico — PodHeitor PostgreSQL para Bacula

Whitepaper técnico — PodHeitor PostgreSQL para Bacula

Cinco modos de backup (dump / parallel_dump / pitr / pitr_block / cdp), captura de replication slots y subscriptions, restore automatizado en 6 fases, y PG17 block-level vía pg_basebackup --incremental.

Documento técnico complementario a la página del plugin PodHeitor PostgreSQL.

1. Problema: Bacula stock + PostgreSQL es frágil

El backup de PostgreSQL con Bacula stock se reduce típicamente a una de tres opciones:

  • Backup filesystem-level del $PGDATA sin coordinación — captura archivos torn-write, el restore es lotería de WAL-replay.
  • pg_dump en RunBeforeJob — staging duplicado en disco; sin PITR; sin incremental real.
  • Plugin Bacula Enterprise PostgreSQL — wrappers Perl alrededor de pg_dump/pg_basebackup, sin PITR_BLOCK, sin CDP, sin captura de replication state.

El PodHeitor PostgreSQL Plugin entrega cinco modos de backup nativos, captura de replication slots/subscriptions, y restore automatizado en 6 fases — todos seleccionables desde el Bacula Director sin scripts externos.

2. Modelo arquitectural

Metaplugin Bacula que habla PTCOMM sobre stdin/stdout entre el cdylib FD (Rust .so) y el backend Rust. El backend orquesta cada paso PostgreSQL-aware: pg_dump, pg_backup_start/pg_backup_stop, gestión de WAL archive, enumeración per-file de $PGDATA, walks de tablespace, captura de logical replication slots, escritura automatizada de recovery configuration y verificación post-restore.

v2.0.0 (abril 2026) — el cdylib se construye desde el crate plugin-postgresql en el workspace PodHeitor Rust cdylib. Ningún source AGPLv3 de Bacula se vincula estáticamente. El shim C++ legacy que linkeaba pluginlib/metaplugin.o fue removido.

3. Cinco modos de backup

Mode Función Namespace de salida
dump (default) pg_dump lógico per-database @postgresql/dump/<db>.dump
parallel_dump Dump multi-DB con worker pool @postgresql/dump/<db>.dump
pitr Físico per-file: pg_backup_start/stop + walk de $PGDATA + WAL window @postgresql/pitr/{pgdata/, tblspc/, wal/, backup_label, tablespace_map, _manifest.json}
pitr_block PG17+ block-level vía pg_basebackup --incremental + pg_combinebackup @postgresql/pitr_block/
cdp Streaming continuo de WAL (continuous data protection) @postgresql/cdp/wal/

3.1 VLDB split — chunking COPY-range

Las tablas con PK numérica por encima de large_table_threshold (default 10G) se auto-splittean en rangos de PK y son procesadas por workers paralelos en modo parallel_dump. En DBs con tablas-monstruo single-threaded en pg_dump, esto convierte un job de 8 horas en ~1 hora con parallel_tables=8.

4. Captura de replication state

Con track_replication_state=true, el plugin captura en el _manifest.json per-job:

  • Physical + logical replication slots de pg_replication_slots (name, type, plugin, active, restart_lsn, confirmed_flush_lsn).
  • Subscriptions de pg_subscription con DDL CREATE SUBSCRIPTION ... WITH (slot_name=..., create_slot=false, enabled=...) sintetizada. LEFT JOIN pg_database garantiza que cada subscription aparezca una vez con su DB target real (fix v1.3.0 dedup).
  • Source roleprimary o standby, derivado vía pg_is_in_recovery(). Accesores primary-only vs standby-only protegidos con CASE para que la misma query funcione en ambos.

En el restore, con restore_replication_state=true (default), el plugin parsea el manifest y recrea slots ausentes vía pg_create_{physical,logical}_replication_slot() contra el cluster target. Las subscriptions que estaban disabled se recrean con enabled=false por default — el restore nunca activa accidentalmente replicación que estaba off en backup time.

5. Restore automatizado en 6 fases

El flujo de restore corre seis fases idempotentes que honran dry_run_restore=true:

  1. Pre-flight — autodetecta unidad systemd, para PG, opcional mv PGDATA → PGDATA.old.<UTC-ts> (rollback de 5 segundos), crea $PGDATA + wal_restore_dir con ownership correcto.
  2. Per-file receipt — dispatcha FNAME entrante por prefijo de vpath: PGDATA files, tablespace entries, WAL segments, backup_label, tablespace_map, _manifest.json.
  3. Recovery config — anexa un bloque PodHeitor-managed claramente delimitado en $PGDATA/postgresql.auto.conf con restore_command + recovery_target_{time,lsn,xid,name} suministrados por el operador + recovery_target_action (promote/pause/shutdown). Toca recovery.signal.
  4. Start + monitor — opcional systemctl start; pollea pg_is_in_recovery() + pg_last_wal_replay_lsn() hasta que la target action converja o restore_timeout dispare.
  5. Verify — sanity checks SQL post-recovery; opcional pg_checksums --check; opcional script SQL suministrado por el operador.
  6. Cleanup — purge de PGDATA.old.<ts> más antiguos que pgdata_backup_retain_days; opcionalmente remueve wal_restore_dir.

6. Backup-from-standby + multi-cluster

Parámetro Default Función
backup_from_standby false Conecta a un standby read-only vía PGTARGETSESSIONATTRS=read-only; offload completo del primary
cluster_id (vacío) Prefijo de namespace multi-cluster: @postgresql/cluster_<id>/...
track_replication_state false Captura slots + subscriptions en manifest
compress zstd zstd, lz4, gzip, none
archive_dir auto Auto-derivado de SHOW archive_command (PITR only)

7. Compatibilidad

Componente Versiones soportadas
PostgreSQL 12, 13, 14, 15, 16, 17 (incremental & VLDB requieren 17)
Bacula Community 15.0.3+ (probado en Oracle Linux 9.6)
Bacula Enterprise 18.0+ (con patches open_bpipe + FD_PLUGIN_INTERFACE_VERSION)
OS (host del plugin) glibc 2.17+ o musl-static: CentOS 7, Rocky/Alma/OL 8/9, RHEL 8/9, Debian 11+, Ubuntu 20.04+
Arch x86_64

El binario backend es musl static-pie ~500 KB — portable entre distros sin dependencia de glibc específica.

8. Comparación Bacula Enterprise vs PodHeitor

Feature Bacula Enterprise PostgreSQL PodHeitor v2.0.0
DUMP mode (pg_dump)
Parallel DUMP multi-DB
VLDB split COPY-range
PITR Full + Diff + Inc (WAL-only)
PITR_BLOCK (PG17+)
CDP (continuous WAL)
Backup from standby
Replication slot capture
Subscription DDL capture
Restore automatizado 6 fases ⚠️ (manual) ✅ (con dry_run_restore)
Recovery-target params (time/lsn/xid/name)
Tablespace remap ⚠️ ✅ (OID:/path)
Rollback safety (mv antes de wipe)
Prometheus metrics
Multi-cluster namespace

9. Anti-patterns documentados

  • No corras Differential o Incremental sin Accurate = yes en el Job. Bacula necesita el walker Accurate para filtrar archivos no modificados; sin esto, «incremental» envía todo.
  • No uses start_postgresql_after_restore=true por accidente en prod. Default es false precisamente para evitar auto-start de cluster restaurado en el ambiente equivocado.
  • No confundas archive_dir con wal_restore_dir. El primero es donde el source escribió WALs; el segundo es donde el restore los pone para replay.
  • No deshabilites el safety rename de wipe_pgdata. Default false es la protección; voltearlo a true sin entender el rollback de 5s mv PGDATA → PGDATA.old.<ts> puede llevar a pérdida de datos.

10. Postura de licencia

Single-license proprietary, AGPL-clean desde v2.0.0. Ambos binarios (backend + FD cdylib) son proprietary. Ningún source AGPLv3 de Bacula se vincula estáticamente. Los releases ≤ v1.3.0 distribuyeron un shim C++ que linkeaba objetos del Bacula Community — ese shim fue removido en v2.0.0.

¿Listo para evaluar?

Trial gratuito de 30 días para flotas PostgreSQL en producción (incluyendo PG17 con PITR_BLOCK y VLDB split). 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 PostgreSQL

Disponível em: pt-brPortuguês (Portugués, Brasil)enEnglish (Inglés)esEspañol

Deja una respuesta