Hoy fue uno de esos días raros en los que se trabaja sin parar y, al final, lo que se siente no es cansancio — es propósito. Llevo más de veinte años dentro del área de Disaster Recovery. Backup, restore, replicación, integridad, soak testing, cadenas de chunks, tablas de hash, RocksDB, ESM, manifiestos. Es un área con el lujo cruel de solo importar cuando algo sale mal — y cuando algo sale mal, importa más que cualquier feature de marketing que se haya lanzado.
Hoy pasé el día entero metido en el plugin PodHeitor de Global Deduplication para Bacula, escrito desde cero en Rust. Y quiero contar por qué todavía no salió en GA — y por qué eso, en mi lectura, es exactamente lo opuesto a debilidad.
Lo que está funcionando, en números reales
El plugin opera en dos modos. Modo Storage: el Storage Daemon de Bacula intercepta los registros de datos, los chunkea con FastCDC (content-defined chunking) y guarda los chunks únicos en un content-addressable store local. En laboratorio, sobre /usr/bin (408,8 MiB de datos del cliente), el volumen final en disco quedó en 4,45 MiB — 1,09% de los bytes del cliente. 91× de reducción.
Modo Bothsides (deduplicación del lado del cliente, con intercambio de hashes sobre un canal TCP autenticado y cifrado): en caché caliente, sobre 420 MB de datos reales, transmitimos por la red 1,55 MB. Ahorro de banda medido: 99,63%. En soak multi-día (R5/R6, ciclos diarios backup→restore→md5 round-trip), el dedup_ratio da 1,0000 consistentemente sobre corpora estables — cada chunk presentado al daemon ya es conocido. Steady-state perfecto.
Los microbenchmarks del chunk-store, en una VM modesta (8 vCPU QEMU, 3 GB RAM, disco virtio rotacional), muestran ingesta a ~1,18 GB/s (chunks nuevos) y ~1,22 GB/s (chunks ya deduplicados), con latencia de recall en torno a 265 µs para un chunk de 4 MB — equivalente a ~15 GB/s efectivos cuando ayuda el cache de lectura. Estos números de laboratorio ya superan implementaciones de referencia escritas en C. Rust no es magia — es el resultado de cero overhead de runtime, layout de memoria predecible, y un compilador que evita cinco clases de bug al mismo tiempo.
Las técnicas detrás de la velocidad
El rendimiento en deduplicación no viene solo del lenguaje — viene de lo que no tenemos que hacer. Algunas decisiones de arquitectura que están corriendo hoy:
- Bloom filter multi-capa (hot + cold). Antes de cualquier lookup en RocksDB, el chunk pasa por un filtro de Bloom escalable memory-mapped. Capa caliente: chunks recientes en RAM. Capa fría: todo el histórico, mmap’d desde disco. False-positive rate configurable (por defecto 0,1%) — false-negative imposible por construcción. Resultado: la inmensa mayoría de los chunks «definitivamente nuevos» ni siquiera toca SSD. En workloads con 99% de dedup, esto elimina ~99% de las I/Os de índice que un esquema ingenuo emitiría.
- Segment Locality Tracking. Los chunks de una misma stream de backup (mismo job, mismo archivo) se agrupan en segmentos contiguos dentro del container físico. Cuando llega el restore, el primer recall calienta la caché; los siguientes vuelven al instante. Es el mismo patrón de «dedup container locality» que usan los sistemas comerciales consagrados, implementado nativamente, sin parches. El restore se acelera por un orden de magnitud sobre corpora reales.
- Adaptive Chunk Tuner. El tuner muestrea continuamente el
dedup_ratiosobre una ventana deslizante y ajusta el tamaño de chunk: dedup alto (>80%) → chunks más chicos (granularidad fina); dedup bajo (<20%) → chunks más grandes (menos overhead de índice). El sistema se ajusta solo al perfil de datos del cliente, sin tuning manual. - AEAD framing custom, sin TLS. Para Mode B descartamos TLS 1.3 con mTLS y hasta TLS 1.3 PSK. Mantuvimos las primitivas (HKDF-SHA256 + ChaCha20-Poly1305), descartamos la state-machine TLS. Resultado: handshake en 0,1–0,3 ms contra 3–5 ms del TLS basado en cert. Misma confidencialidad e integridad. 45 KB extra de binario por host en lugar de 200 KB. Un archivo de 32 bytes por site, sin CA, sin renovación anual.
- RocksDB con WAL durable + containers append-only con CRC-32. Cada chunk tiene su CRC verificado en la lectura. Si un disco corrompe silenciosamente un byte, el sistema lo detecta al instante — no «ocho meses después cuando falla el restore».
- Vacuum / GC con integrity scan. Operación periódica que recorre todo el índice, lee y re-verifica el CRC de cada chunk. Detecta bit-rot, marca
ChunkStatus::Corrupted, y el operador se entera antes del día en que vaya a necesitar el dato. - FastCDC variable-length chunking. Bordes determinados por contenido, no por offset fijo. ¿Insertar un byte en el medio de un archivo? Solo un chunk cambia — no toda la cadena. Eso es lo que hace que el dedup incremental de VMs aterrice en 99% de ratio en vez de 30%.
Por qué no lo lancé
Acá es donde el post se vuelve incómodo de escribir — porque la respuesta honesta es la opuesta a la que vende.
El componente es extremadamente delicado. La deduplicación global toca tres cosas que no toleran ningún bug:
- Reentrancia bajo estrés. El daemon de dedup corre en paralelo con el File Daemon y el Storage Daemon de Bacula. Encontramos y corregimos casos en los que una sesión stale del daemon, en vez de fallar cerrada, dejaba pasar 37.000 archivos como registros de 0 bytes — con estado final Backup OK. Ese es el peor tipo de bug en backup: silent data loss disfrazado de éxito. Corregido (commit
21bdfa5, 30 de abril), validado con md5 round-trip sobre 25.071/25.071 archivos, blindado con fail-closed en tres caminos de fallback. - Crash recovery. FD muerto en medio del backup. SD muerto en medio. Daemon de dedup reiniciado durante la ingesta. Cada uno de esos escenarios fue ejecutado en laboratorio, con restore posterior, con verificación byte a byte. Llevó meses cerrar B3a/B3b/B3c.
- Soak de 7 días. No es benchmark de una hora. Es correr ciclo backup-restore-md5 todos los días durante una semana, en producción sintética, y probar que
RSSno pierde memoria,/gddno crece sin razón, wire savings no regresan, md5 no se desvía. Estamos en R6 — soak de 7 días en curso. Día 1 cerrado. Faltan 6.
Lanzar antes de eso sería cobardía comercial disfrazada de agilidad.
La diferencia que nadie pone en una presentación
Trabajé toda mi carrera al lado, y a veces contra, proveedores europeos de backup que tratan el General Availability como un evento de marketing. Empresas que se apoyan en reseñas pagas para sostener la percepción pública, que entregan mala usabilidad con la excusa de «siempre fue así», que amenazan comercialmente a partners que se atreven a cuestionar una decisión técnica, y que tiran código a GA sabiendo que tiene una regresión conocida — porque el calendario de release importa más que el cliente que va a tener que restaurar mañana.
Ese no es el juego de PodHeitor. No tengo accionistas extranjeros pidiendo trimestre. Tengo un cliente final. Un cliente que, en un mal día, depende de que el backup de ayer efectivamente restaure. No voy a mirar a ese cliente dentro de un año sabiendo que firmé un GA prematuro porque una planilla de OKR me lo pidió.
Lo que viene
R6 termina el 7 de mayo. Con round-trip limpio en los 7 días, RSS estable y crecimiento acotado de /gdd, GDD entra en GA. No antes. Unos días más en un proyecto de 20 años no me cuestan nada — una falla silenciosa le costaría mucho a quien confía en nosotros.
Cuando salga, sale bien: byte-exact roundtrip validado, soak de 7 días documentado, fail-closed en todos los caminos de fallback, chunk store con su propio fsck, runbook de DR para el propio store de dedup (sí, hicimos un plan de recuperación de desastre del sistema de recuperación de desastre — de ese nivel de paranoia estamos hablando).
Ese es el trabajo. Es lento. Es obsesivo con el detalle. Es aburrido para cualquiera que quiera el release de mañana. Y, veinte años después, sigue siendo el único tipo de trabajo que me sigue dando propósito.
— Heitor Faria
podheitor.com | [email protected]
Disponível em:
Português (Portugués, Brasil)
English (Inglés)
Español