Respaldos de MySQL vs PostgreSQL en contenedores: cómo evitar respaldos falsos

¿Te fue útil?

Las bases de datos en contenedores son una maravilla para la productividad hasta el día en que necesitas restaurar. Entonces descubres que durante meses has estado subiendo a almacenamiento de objetos una decepción comprimida en gzip con cariño. Tiene nombre de archivo, suma de verificación, una política de retención y absolutamente ninguna capacidad para devolver tu servicio.

Los “respaldos falsos” son respaldos que se completan con éxito pero no pueden restaurar a un estado de base de datos consistente y funcional dentro del RPO/RTO que prometiste a la empresa (o a ti mismo a las 2 a. m.). En contenedores, los modos de fallo se multiplican: sistemas de archivos efímeros, mounts de volúmenes que olvidaste, WAL/binlogs ausentes y snapshots tomados en el peor momento posible.

Cómo es un respaldo falso (y por qué los contenedores lo empeoran)

Un respaldo falso es cualquier cosa que te da confianza sin dar recuperabilidad. Los sospechosos habituales:

  • Respaldos que “suceden” pero la restauración falla (permisos, roles ausentes, extensiones faltantes, archivo corrupto, versiones incompatibles).
  • Respaldos que se restauran pero los datos están mal (snapshot inconsistente, volcado parcial, binlogs/WAL faltantes, sorpresas de zona horaria/encoding).
  • Respaldos que se restauran pero tardan 12 horas y tu RTO era “menos de una hora”. El respaldo existe; tu puesto no.
  • Respaldos guardados dentro del sistema de archivos del contenedor, que desaparece durante reprogramaciones, drenados de nodo o actualizaciones de la imagen.
  • Respaldos que omiten lo único que necesitabas: permisos de objetos, rutinas, triggers, usuarios, grants, configuración o la secuencia WAL/binlog necesaria para la recuperación punto en el tiempo.

Los contenedores amplifican estos problemas porque hacen que “dónde vive el dato” sea engañosamente ambiguo. Puedes ejecutar mysqldump o pg_dump desde un cron en el contenedor, ver aparecer un archivo bajo /backups y sentirte bien. Pero si /backups no es un volumen persistente, acabas de crear un póster motivacional.

En segundo lugar, las plataformas de contenedores fomentan pensar sin estado. Excelente para servidores de aplicaciones. Peligroso para bases de datos. La plataforma reiniciará felizmente tu pod de base de datos mientras tomas un snapshot del sistema de archivos a menos que configures salvaguardas. También rotará secretos y romperá tu job de respaldo mientras sigue informando que el CronJob está “Completed”.

Tu regla: un respaldo es “real” solo si puedes restaurarlo, verificar su corrección y hacerlo dentro del tiempo que prometiste, usando automatización en la que confíes.

Broma #1: Un respaldo que nunca ha sido restaurado es como un paracaídas que compraste en oferta y nunca desplegaste. Te puedes imaginar cómo va la primera prueba.

MySQL vs PostgreSQL: qué significa realmente “respaldo consistente”

La consistencia es un contrato, no un archivo

La consistencia trata sobre la corrección transaccional. Tanto MySQL (InnoDB) como PostgreSQL pueden ofrecer respaldos consistentes, pero lo hacen de forma distinta y castigan errores diferentes.

MySQL: InnoDB, redo logs y binlogs

MySQL tiene dos grandes mundos: respaldos lógicos (mysqldump, mysqlpump) y respaldos físicos (Percona XtraBackup, MySQL Enterprise Backup). Los respaldos lógicos son portables y legibles; los físicos son más rápidos y mejores para bases de datos grandes, pero más sensibles a incompatibilidades de versión y configuración.

Para la recuperación punto en el tiempo (PITR), MySQL depende del registro binario (binlog). Un respaldo completo sin binlogs suele ser una restauración “de anoche”, no “de hace cinco minutos”. Si tu negocio espera “hace cinco minutos”, necesitas que los binlogs se envíen y retengan con un mapeo conocido al respaldo.

Patrón común de respaldos falsos en contenedores MySQL: ejecutas mysqldump pero olvidas --single-transaction para InnoDB, o vuelcas desde una réplica con lag de replicación, o no incluyes rutinas/eventos/triggers, o no capturas usuarios/grants. Se restaura, pero la aplicación se cae.

PostgreSQL: MVCC, base backups y WAL

El mundo de PostgreSQL es MVCC y logging de escritura adelantada (WAL). Un respaldo físico consistente es una base backup más los segmentos WAL necesarios para llevarlo a un punto consistente (y opcionalmente hacia adelante en el tiempo). Los respaldos lógicos (pg_dump) son consistentes a nivel de base de datos, pero son más lentos para conjuntos grandes y no capturan objetos a nivel de clúster a menos que añadas pg_dumpall para roles y globals.

Patrón común de respaldos falsos en contenedores PostgreSQL: tomas un snapshot del directorio de datos sin coordinar con PostgreSQL, no incluyes WAL y terminas con un respaldo que falla con “invalid checkpoint record” cuando intentas iniciarlo. Otro: usas pg_dump pero olvidas roles, extensiones o supuestos de search_path del esquema.

Conclusión operativa

  • Si necesitas restauraciones rápidas y bases de datos grandes: favorece respaldos físicos (XtraBackup / pg_basebackup) más logs para PITR.
  • Si necesitas portabilidad y conjuntos de datos pequeños: los respaldos lógicos están bien, pero debes incluir el conjunto completo de objetos necesarios y probar las restauraciones.
  • En contenedores: monta volúmenes persistentes correctamente, guarda logs (WAL/binlog) fuera de capas efímeras y trata los jobs de respaldo como sistemas de producción.

Una cita operativa que envejece bien: “La esperanza no es una estrategia.” — Gene Kranz

Hechos históricos e información interesante (porque el pasado explica tu pager)

  1. El linaje de PostgreSQL se remonta al proyecto POSTGRES en UC Berkeley en los años 80; WAL y MVCC maduraron a medida que creció hacia una base de datos con prioridad en la fiabilidad.
  2. La popularidad temprana de MySQL vino por la velocidad y simplicidad en pilas web, pero la fiabilidad transaccional para cargas serias llegó cuando InnoDB se convirtió en el motor por defecto años después.
  3. WAL (write-ahead logging) es más antiguo que MySQL y PostgreSQL; el principio proviene de investigación en bases de datos mucho antes de que los contenedores hicieran que todo pareciera desechable.
  4. La replicación de MySQL históricamente dependía de binlogs basados en sentencias; hoy el logging por filas es común porque evita problemas sutiles de reproducción no determinista.
  5. La PITR en PostgreSQL se hizo común cuando las herramientas de archivado WAL maduraron; sin retención de WAL, “respaldo” significa “cápsula del tiempo”, no “recuperación”.
  6. Los snapshots de sistema de archivos se volvieron un primitivo práctico de respaldo a medida que los sistemas copia-en-escritura y arrays de almacenamiento mejoraron; son potentes pero fáciles de usar mal sin coordinación con la base de datos.
  7. Percona XtraBackup ganó tracción porque permitió respaldos físicos en caliente para InnoDB sin apagar MySQL, cambiando la economía de respaldar conjuntos grandes de datos.
  8. La orquestación de contenedores normalizó imágenes inmutables y nodos efímeros; la desalineación con bases de datos stateful creó una década de lecciones duras sobre “stateful en Kubernetes”.
  9. Las sumas de verificación y “jobs exitosos” siempre han mentido por omisión: el éxito del respaldo es un evento de E/S, el éxito de la restauración es un evento de corrección. Operaciones aprendieron esto de forma ruidosa.

Tipos de respaldo que funcionan en contenedores (lógico, físico, snapshots y PITR)

Respaldos lógicos: portables, más lentos, fáciles de falsificar

MySQL: mysqldump está bien si lo usas correctamente y tu conjunto de datos no es enorme. Necesitas --single-transaction para la consistencia de InnoDB sin bloquear tablas, y probablemente quieras --routines --triggers --events. Pero los volcados lógicos aún pueden ser falsos: pueden estar incompletos, truncados silenciosamente o restaurarse en un esquema que no coincide con producción.

PostgreSQL: pg_dump produce volcados consistentes gracias a MVCC, pero es por base de datos. Los objetos de clúster (roles, tablespaces, algunas configuraciones) son separados. Si restauras en un clúster limpio sin recrear roles o extensiones, tu aplicación fallará de formas ingeniosas.

Respaldos físicos: rápidos, estrictos operativamente

MySQL: respaldos físicos con XtraBackup (o equivalente empresarial) son la opción adulta para conjuntos grandes. Pero debes manejar redo logs, preparar el respaldo y mantener compatibilidad de versiones en mente.

PostgreSQL: pg_basebackup más archivado WAL te da respaldos físicos consistentes y restaurables. Es directo, pero debes asegurar que WAL se archive y retenga, y necesitas un procedimiento de restauración limpio (incluida la configuración de recuperación o comandos modernos de restauración).

Snapshots: el respaldo “más rápido” que ama traicionarte

Los snapshots de volumen (LVM, ZFS, Ceph, EBS, snapshots CSI) son excelentes cuando puedes garantizar consistencia. La palabra clave es garantizar. Un snapshot crash-consistente puede funcionar, hasta que no funcione, y no sabrás cuál respaldo está mal hasta que lo necesites.

Para hacer snapshots bien, coordínate con la base de datos: flush/lock o usa modos de respaldo que aseguren recuperabilidad. PostgreSQL puede recuperarse de un crash, pero un snapshot tomado a mitad de escritura sin la cobertura WAL correcta aún puede fallar. InnoDB de MySQL puede recuperarse de crash, pero mezclar snapshots con logs faltantes o configuraciones fsync extrañas se pone feo rápido.

PITR: la diferencia entre “perdimos un día” y “perdimos cinco minutos”

PITR no es opcional si tu RPO es pequeño. Para MySQL: binlogs. Para PostgreSQL: archivado WAL. En contenedores, PITR falla cuando los directorios de logs no se persisten, el archivado está mal configurado o tu política de retención borra logs antes de que te des cuenta.

Broma #2: PITR es como una máquina del tiempo, excepto que solo va hacia atrás—usualmente hasta el momento exacto antes de ejecutar la migración destructiva. Conveniente.

Tareas prácticas: 12+ comandos para verificar que los respaldos son reales

Estas son tareas que puedes ejecutar hoy. Cada una incluye: comando, ejemplo de salida, qué significa y la decisión que tomas.

Tarea 1: Confirma que el directorio de datos de la base está realmente en un mount persistente (host del contenedor)

cr0x@server:~$ docker inspect mysql01 --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }} ({{ .Type }}){{ "\n" }}{{ end }}'
/var/lib/docker/volumes/mysql01-data/_data -> /var/lib/mysql (volume)
/srv/backups/mysql01 -> /backups (bind)

Significado: /var/lib/mysql es un volumen de Docker (persistente), y los respaldos se escriben en un bind mount del host en /srv/backups/mysql01.

Decisión: Si no ves un mount persistente para el directorio de datos y el directorio de respaldo, detente. Arregla el almacenamiento antes de discutir herramientas.

Tarea 2: Kubernetes: verifica que el pod usa un PVC (no emptyDir)

cr0x@server:~$ kubectl get pod pg-0 -o jsonpath='{range .spec.volumes[*]}{.name}{"\t"}{.persistentVolumeClaim.claimName}{"\t"}{.emptyDir}{"\n"}{end}'
data	pgdata-pg-0	
tmp		map[]

Significado: El volumen data está respaldado por un PVC. tmp es efímero (emptyDir).

Decisión: Si tus datos DB están en emptyDir, no tienes persistencia; tienes buenas intenciones.

Tarea 3: Volcado lógico de MySQL bien hecho (y detectar volcados truncados)

cr0x@server:~$ docker exec mysql01 sh -lc 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines --triggers --events --set-gtid-purged=OFF --databases appdb | gzip -1 > /backups/appdb.sql.gz && ls -lh /backups/appdb.sql.gz'
-rw-r--r-- 1 root root 412M Dec 31 02:10 /backups/appdb.sql.gz

Significado: Existe un volcado y tiene un tamaño plausible.

Decisión: El tamaño no es prueba. A continuación, valida la integridad gzip y la presencia de objetos requeridos.

Tarea 4: Validar integridad gzip (detecta subidas parciales y desastres por disco lleno)

cr0x@server:~$ gzip -t /srv/backups/mysql01/appdb.sql.gz && echo OK
OK

Significado: El archivo es un stream gzip válido (no truncado).

Decisión: Si esto falla, trata el respaldo como ausente. Investiga espacio en disco, logs del job y pasos de subida.

Tarea 5: Chequeo rápido dentro de un volcado MySQL (¿tenemos esquema y datos?)

cr0x@server:~$ zcat /srv/backups/mysql01/appdb.sql.gz | head -n 25
-- MySQL dump 10.13  Distrib 8.0.36, for Linux (x86_64)
--
-- Host: localhost    Database: appdb
-- ------------------------------------------------------
-- Server version	8.0.36
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
...

Significado: Parece un volcado real con metadatos. También confirma la versión de MySQL al momento del respaldo.

Decisión: Si falta el encabezado del volcado o el stream está vacío, tu job de respaldo escribió una página de error, no SQL.

Tarea 6: MySQL: confirma que el binlog está habilitado (PITR lo requiere)

cr0x@server:~$ docker exec mysql01 sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES LIKE \"log_bin\"; SHOW VARIABLES LIKE \"binlog_format\";"'
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON    |
+---------------+-------+
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+

Significado: Binlog está activo y el formato es ROW (generalmente más seguro para replicación y PITR).

Decisión: Si log_bin está OFF, acepta un RPO de “solo respaldos completos” o actívalo e implementa envío de binlogs.

Tarea 7: MySQL: verifica que realmente tienes binlogs recientes (no solo habilitados)

cr0x@server:~$ docker exec mysql01 sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW BINARY LOGS;" | tail -n 5'
| binlog.000231 |  10485760 |
| binlog.000232 |   8912451 |

Significado: Existen binlogs y están rotando.

Decisión: Si no hay binlogs o se detuvieron hace días, PITR no es real. Arregla la retención y el envío.

Tarea 8: PostgreSQL: realiza un respaldo lógico y captura globals (roles) por separado

cr0x@server:~$ kubectl exec -it pg-0 -- bash -lc 'pg_dump -U postgres -Fc -d appdb -f /backups/appdb.dump && pg_dumpall -U postgres --globals-only > /backups/globals.sql && ls -lh /backups/appdb.dump /backups/globals.sql'
-rw-r--r-- 1 root root 2.1G Dec 31 02:12 /backups/appdb.dump
-rw-r--r-- 1 root root  19K Dec 31 02:12 /backups/globals.sql

Significado: Tienes un volcado en formato personalizado más objetos globales.

Decisión: Si solo haces el volcado de la base e ignoras globals, espera fallos de permisos y roles faltantes al restaurar.

Tarea 9: PostgreSQL: verifica que el archivado WAL esté habilitado (PITR depende de ello)

cr0x@server:~$ kubectl exec -it pg-0 -- bash -lc 'psql -U postgres -d postgres -c "SHOW wal_level; SHOW archive_mode; SHOW archive_command;"'
 wal_level 
-----------
 replica
(1 row)

 archive_mode 
--------------
 on
(1 row)

                 archive_command                 
-------------------------------------------------
 test ! -f /wal-archive/%f && cp %p /wal-archive/%f
(1 row)

Significado: WAL se produce a nivel de réplica (bueno para PITR/replicación). El archivado está activo.

Decisión: Si archive_mode está off, no tienes PITR. O aceptas el riesgo o lo arreglas antes de prometer un RPO.

Tarea 10: PostgreSQL: confirma que WAL se está archivando realmente (no solo configurado)

cr0x@server:~$ kubectl exec -it pg-0 -- bash -lc 'psql -U postgres -d postgres -c "SELECT now(), last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time FROM pg_stat_archiver;"'
              now              | last_archived_wal |     last_archived_time     | failed_count | last_failed_wal | last_failed_time 
-------------------------------+-------------------+----------------------------+--------------+-----------------+------------------
 2025-12-31 02:14:01.12345+00  | 0000000100000000000000A7 | 2025-12-31 02:13:58+00 |            0 |                 | 
(1 row)

Significado: El archivado está teniendo éxito y es reciente.

Decisión: Si failed_count aumenta o last_archived_time está desactualizado, tu stream PITR está roto. Llama a alguien antes de necesitarlo.

Tarea 11: Prueba de restauración del volcado MySQL en un contenedor desechable (la única prueba que importa)

cr0x@server:~$ docker run --rm --name mysql-restore -e MYSQL_ROOT_PASSWORD=restorepass -d mysql:8.0
...output...
cr0x@server:~$ sleep 15 && zcat /srv/backups/mysql01/appdb.sql.gz | docker exec -i mysql-restore mysql -uroot -prestorepass
...output...
cr0x@server:~$ docker exec mysql-restore mysql -uroot -prestorepass -e "SHOW DATABASES; USE appdb; SHOW TABLES;" | tail -n 10
Tables_in_appdb
users
orders
order_items

Significado: El volcado importa y el esquema existe.

Decisión: Si la importación falla, el respaldo es falso. Si importa pero las consultas de la app fallan, te faltan objetos (rutinas, triggers) o dependes de settings específicos del entorno.

Tarea 12: Prueba de restauración del volcado PostgreSQL en una instancia desechable

cr0x@server:~$ docker run --rm --name pg-restore -e POSTGRES_PASSWORD=restorepass -d postgres:16
...output...
cr0x@server:~$ sleep 10 && cat /srv/backups/pg/globals.sql | docker exec -i pg-restore psql -U postgres
...output...
cr0x@server:~$ docker exec -i pg-restore createdb -U postgres appdb
...output...
cr0x@server:~$ docker exec -i pg-restore pg_restore -U postgres -d appdb /srv/backups/pg/appdb.dump
pg_restore: connecting to database for restore
pg_restore: creating TABLE "public.users"
pg_restore: creating TABLE "public.orders"
...output...

Significado: El volcado se restaura limpiamente en un clúster nuevo y ejecuta la creación de objetos.

Decisión: Si falla por roles/permisos, tu captura de globals está incompleta. Si falla por extensiones, necesitas preinstalarlas en el entorno de restauración.

Tarea 13: Verificación de seguridad de snapshots: ¿estás respaldando la ruta correcta?

cr0x@server:~$ docker exec mysql01 sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES LIKE \"datadir\";"'
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| datadir       | /var/lib/mysql/ |
+---------------+----------------+

Significado: Confirma el directorio de datos real.

Decisión: Si tu snapshot se toma desde una ruta del host distinta al datadir montado, estás snapshotteando la cosa equivocada. Sí, pasa.

Tarea 14: Chequeo básico de capacidad: ¿fallan los respaldos por presión de disco?

cr0x@server:~$ df -h /srv/backups
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  1.8T  1.7T   62G  97% /srv/backups

Significado: Estás al 97% de uso. Esto no es un estilo de vida.

Decisión: Si rutinariamente superas ~85–90%, espera archivos parciales, fsync fallidos y logs de “backup succeeded” que no coinciden con la realidad. Arregla retención, compresión y dimensionamiento de almacenamiento.

Tarea 15: Confirma que los jobs de respaldo no son “exitosos” mientras en realidad fallan (CronJob de Kubernetes)

cr0x@server:~$ kubectl get job -n db -l app=pg-backup -o wide
NAME               COMPLETIONS   DURATION   AGE   CONTAINERS   IMAGES        SELECTOR
pg-backup-28911    1/1           9s         2h    backup       alpine:3.20   controller-uid=...
cr0x@server:~$ kubectl logs -n db job/pg-backup-28911 | tail -n 20
pg_dump: error: connection to server at "pg" (10.0.2.44), port 5432 failed: FATAL:  password authentication failed for user "postgres"

Significado: El job se completó desde la perspectiva de Kubernetes, pero el respaldo falló.

Decisión: Trata los logs como la fuente de la verdad. Asegura que el job salga con código distinto de cero en fallo (no ocultes errores en scripts shell).

Guion de diagnóstico rápido: encuentra el cuello de botella rápido

Cuando los respaldos son lentos, faltan o no se pueden restaurar, no tienes tiempo para filosofía. Necesitas un ciclo cerrado. Aquí está el orden que captura la mayoría de problemas más rápido.

Primero: ¿es válido el artefacto de respaldo?

  • Verificar integridad: prueba gzip, listado tar o introspección del formato del volcado.
  • Verificar tendencia de tamaño: una caída o pico súbito suele significar fallo o datos fuera de control.
  • Verificar la última prueba de restauración exitosa: si no tienes una, no sabes nada.

Segundo: ¿incluye la canalización los logs necesarios para PITR?

  • MySQL: binlog habilitado, rotando, enviado y retenido.
  • PostgreSQL: archivado WAL activado, pg_stat_archiver saludable, retención cubre la ventana deseada.

Tercero: ¿el cuello de botella es CPU, disco o red?

  • Limitado por CPU: nivel de compresión muy alto, sobrecarga de cifrado, volcados mono-hilo.
  • Limitado por disco: PVC lento, disco del nodo saturado, demoras en copia de snapshot.
  • Limitado por red: throughput de almacenamiento de objetos, throttling o límites de egress entre zonas.

Cuarto: ¿estás tomando respaldos consistentes?

  • MySQL: --single-transaction para volcados; respaldo físico preparado; coordinación con snapshots.
  • PostgreSQL: base backup + WAL; o pg_dump más globals; coordinación con snapshots.

Quinto: ¿la restauración cumplirá el RTO?

  • Prueba el tiempo de restauración end-to-end: descargar + descifrar + descomprimir + importar + reconstrucción de índices + consultas de verificación.
  • Si el RTO se incumple, cambia el método de respaldo, paraleliza la restauración o cambia la promesa al negocio. Elige uno.

Tres microhistorias corporativas del reino de “pasó CI”

Incidente: la suposición errónea (¿un volcado lógico “incluye todo”, verdad?)

Una empresa SaaS de tamaño medio ejecutaba PostgreSQL en Kubernetes. Su job de respaldo hacía pg_dump -Fc cada noche, subía el archivo a almacenamiento de objetos y lo rotaba con una política de retención limpia. Dashboards verdes. Sin alertas. El tipo de configuración que hace asentir a los managers.

Entonces tuvieron un incidente: una migración de esquema salió mal y necesitaron restaurar a “ayer”. Levantaron un clúster nuevo y restauraron el volcado. Funcionó—técnicamente. La base de datos arrancó. Las tablas existían. La aplicación seguía fallando en la autenticación y empezaba a emitir errores de permisos como un metrónomo.

La pieza faltante fue aburrida: roles y grants no estaban incluidos. En producción, algunos roles se habían creado manualmente durante un firefight de on-call meses antes. El volcado capturó objetos, no el modelo de identidad y privilegios a nivel de clúster. Su “respaldo” no sabía quién podía hacer qué.

La solución fue inmediata: añadir pg_dumpall --globals-only al pipeline, forzar la creación de roles vía migraciones/Infra como Código, y añadir una prueba de restauración que ejecute un flujo mínimo de login de la aplicación contra la base restaurada. Dejaron de asumir que “respaldo de base de datos” equivale a “recuperación de la aplicación”.

Optimización que salió mal (compresión y astucia versus tiempo)

Una plataforma de e-commerce migró respaldos MySQL de físicos a lógicos para “simplificar”. La base era de varios cientos de GB. Los volcados se comprimieron agresivamente para reducir costos de almacenamiento de objetos. También ejecutaron respaldos durante horas de negocio porque “los contenedores escalan”. El job tenía un bonito valor de nice y todo parecía civilizado.

Lo clásico ocurrió: la CPU se disparó por la compresión alta, causando latencia en las consultas. Luego el respaldo duró más, solapándose con tráfico pico. Ese solapamiento extendido generó más contención, lo que hizo que el volcado durara aún más. La base no estuvo caída; fue peor—lenta, impredecible e irritada.

Cuando finalmente probaron el tiempo de restauración, fue brutal: descargar y descomprimir el volcado tomó una eternidad, luego la importación tomó otra eternidad, luego reconstruir índices tomó otra eternidad. El equipo había optimizado la métrica equivocada (costo de almacenamiento) e ignorado la que importa durante un incidente (tiempo para restaurar).

Cambiaron a respaldos físicos para MySQL, redujeron la compresión a un nivel sensato e implementaron envío de binlogs para PITR. El costo de almacenamiento subió, pero la ruta de restauración pasó de “te contactamos mañana” a algo que puedes decir en voz alta en una actualización de estado.

Práctica aburrida pero correcta que salvó el día (simulacros de restauración y fijar versiones)

Un equipo de servicios financieros ejecutaba PostgreSQL con archivado WAL y respaldos base semanales. Nada fancy: configuración consistente, retención estricta y un simulacro de restauración mensual en un entorno aislado. Fijaban versiones menores de Postgres para los entornos de restauración y mantenían una pequeña matriz de “respaldo tomado en versión X, restaurado en versión X/Y” que realmente probaban.

Un trimestre, un incidente de almacenamiento corrompió un volumen primario y dejó la base caída severamente. La replicación no los salvó; la réplica ya había reproducido la corrupción. El único camino fue restaurar.

El runbook de on-call era dolorosamente explícito. Recuperar base backup, recuperar WAL del archivo, restaurar en un PV nuevo, reproducir hasta un tiempo objetivo, ejecutar consultas de verificación, cambiar el tráfico. Lo siguieron paso a paso con la calma de gente que ha ensayado lo aburrido.

Restauraron dentro de su ventana prometida. Sin heroísmos. Sin arqueología en Slack. La revisión post-incidente fue corta porque el sistema hizo lo que fue diseñado para hacer. Este es el estándar a copiar.

Errores comunes: síntoma → causa raíz → solución

1) “El job de respaldo dice éxito, pero el archivo es pequeño”

Síntoma: los respaldos caen súbitamente de cientos de MB/GB a unos pocos KB/MB.

Causa raíz: el job escribió un mensaje de error en el stream de salida, la autenticación falló o el disco se llenó a mitad de escritura; la canalización aún devolvió código 0 por scripting shell descuidado.

Solución: habilita set -euo pipefail en scripts; valida la integridad del artefacto (prueba gzip); alerta en anomalías de tamaño; falla el job por patrones en stderr o códigos de salida no nulos.

2) “Restauración de Postgres falla: invalid checkpoint / WAL faltante”

Síntoma: el directorio de datos restaurado no arranca, se queja de registros de checkpoint o WAL faltantes.

Causa raíz: snapshot crash-inconsistente, o base backup sin los segmentos WAL correspondientes.

Solución: usa pg_basebackup con archivado WAL; asegura retención de WAL; si usas snapshots, coordina con PostgreSQL e incluye WAL necesarios para alcanzar consistencia.

3) “La restauración de MySQL funciona, pero faltan rutinas/eventos”

Síntoma: errores de aplicación tras restaurar; procedimientos almacenados o eventos programados ausentes.

Causa raíz: mysqldump sin --routines --events --triggers.

Solución: añade esas flags; añade consultas de verificación en la restauración que comprueben estos objetos.

4) “Existen respaldos, pero no puedes hacer PITR”

Síntoma: solo puedes restaurar hasta el tiempo del respaldo, no a un momento específico.

Causa raíz: binlogs/WAL no habilitados, no archivados o no retenidos el tiempo suficiente; la ruta de logs es efímera en el contenedor.

Solución: habilita y envía binlogs/WAL a almacenamiento duradero; monitorea salud del archivado; alinea la retención con el RPO del negocio.

5) “Kubernetes reprograma el pod y pierdes respaldos”

Síntoma: los respaldos desaparecen después de mantenimiento de nodo o reinicio del pod.

Causa raíz: los respaldos se escriben dentro del sistema de archivos del contenedor o en emptyDir.

Solución: escribe respaldos en un PVC o transmite directamente a almacenamiento de objetos; verifica mounts vía manifests e inspección en tiempo de ejecución.

6) “La restauración es demasiado lenta para cumplir RTO”

Síntoma: la restauración toma horas/días; la respuesta al incidente queda rehén de la velocidad de importación.

Causa raíz: volcados lógicos para conjuntos muy grandes, compresión excesiva, ruta de restauración mono-hilo, clase de almacenamiento lenta.

Solución: cambia a respaldos físicos; ajusta la compresión; prueba el tiempo de restauración; considera réplicas de lectura para un corte más rápido; usa PVC/tiers de almacenamiento más rápidos para entornos de recuperación.

7) “Respaldos basados en réplica restauran datos obsoletos”

Síntoma: la restauración es consistente pero faltan escrituras recientes.

Causa raíz: respaldaste desde una réplica con lag sin chequear el retraso, o la replicación estaba rota.

Solución: impón chequeos de lag antes del respaldo; alerta sobre salud de replicación; para PITR, confía en logs con orden conocido y retención adecuada.

8) “Todo se restaura, pero la app sigue fallando”

Síntoma: la BD está arriba; errores de app incluyen fallos de auth, extensiones faltantes, problemas de encoding, rarezas de zona horaria.

Causa raíz: respaldaste datos pero no el contrato del entorno: roles, extensiones, collations, supuestos de configuración.

Solución: codifica el bootstrap de la BD (roles/extensiones) como código; añade pruebas de humo que ejecuten consultas reales de la app; guarda la imagen/version del entorno de restauración junto a los respaldos.

Listas de verificación / plan paso a paso (aburrido a propósito)

Paso 1: Decide tus objetivos de recuperación (RPO/RTO) y hazlos ejecutables

  • Elige un RPO (pérdida máxima de datos aceptable) y un RTO (tiempo máximo de inactividad aceptable).
  • Transfórmalos en implementación: frecuencia de respaldos completos + retención de logs PITR + presupuesto de tiempo para el workflow de restauración.
  • Si no puedes permitir la complejidad de PITR, admite que el RPO es “último respaldo”. No improvises después.

Paso 2: Elige el método de respaldo correcto según la base de datos y el tamaño

  • MySQL pequeño/mediano: mysqldump con flags correctos + envío de binlogs si necesitas PITR.
  • MySQL grande: respaldos físicos (XtraBackup/enterprise) + envío de binlogs.
  • PostgreSQL pequeño/mediano: pg_dump -Fc + pg_dumpall --globals-only.
  • PostgreSQL grande: pg_basebackup + archivado WAL, además de pruebas periódicas de restauración.

Paso 3: Haz explícita la colocación del almacenamiento en los manifests de contenedores

  • Directorio de datos en PVC/volumen con clase de rendimiento conocida.
  • Directorio de staging de respaldos en PVC (o transmitir a almacenamiento de objetos).
  • WAL/binlog en almacenamiento duradero si se usa archivado local.

Paso 4: Integra verificación en la canalización

  • Cheques de integridad de artefactos: validación gzip/tar, introspección de formato de volcado.
  • Captura de metadatos: versión DB, hash de esquema, timestamp del respaldo, versión de la herramienta.
  • Automatización de pruebas de restauración: restaurar en entorno desechable y ejecutar consultas de verificación.

Paso 5: Monitorea el sistema de respaldos como producción

  • Alerta por respaldos faltantes, anomalías de tamaño y archivado WAL/binlog obsoleto.
  • Rastrea tendencias de tiempo de restauración (descarga + descifrado + descompresión + aplicar + verificar).
  • Haz que las fallas de respaldo sean noticia si reclamas un RPO/RTO. Las fallas silenciosas son cómo nacen los respaldos falsos.

Paso 6: Practica restauraciones (y controla la deriva de versiones)

  • Simulacros de restauración mensuales, como mínimo. Semanales si la BD es crítica para el negocio.
  • Fija versiones de herramientas de restauración (mysqldump/mysql client, pg_restore, etc.).
  • Mantén una imagen de entorno de restauración conocida y buena para cada versión mayor de BD que ejecutes.

Preguntas frecuentes

1) ¿Qué exactamente convierte un respaldo en “falso”?

Cualquier cosa que no pueda restaurar a un estado consistente y operativo dentro de tu ventana de tiempo requerida. “El archivo existe” y “el job tuvo éxito” no cuentan.

2) ¿Los respaldos lógicos (mysqldump/pg_dump) son inherentemente inseguros?

No. Son fáciles de malconfigurar y lentos a escala. Los respaldos lógicos están bien si los pruebas de restauración y tu RTO tolera el tiempo de importación.

3) En MySQL, ¿es --single-transaction siempre requerido?

Para volcados consistentes de InnoDB sin bloquear tablas, sí. Si tienes tablas no transaccionales (p. ej., MyISAM), la consistencia se complica y deberías reconsiderar el motor de almacenamiento o el método de respaldo.

4) En PostgreSQL, ¿por qué necesito pg_dumpall --globals-only?

pg_dump es por base de datos. Roles y otros objetos globales viven a nivel de clúster. Sin ellos, las restauraciones fallan frecuentemente con problemas de permisos.

5) ¿Puedo confiar en snapshots de volumen como mi único respaldo?

Solo si puedes garantizar consistencia y has probado restauraciones desde snapshots. Los snapshots son excelentes como componente, no como religión. La mayoría combina snapshots (rápidos) con logs PITR (recuperación granular).

6) ¿Cuál es el patrón más simple y seguro para respaldos en contenedores?

Transmite el respaldo directamente a almacenamiento durable (almacenamiento de objetos o un servidor de backups) y mantén el staging local mínimo. No escribas artefactos críticos en capas efímeras del contenedor.

7) ¿Cómo sé si PITR realmente funciona?

MySQL: confirma que el binlog está habilitado, que hay binlogs presentes y recientes, y que puedes reproducir hasta un timestamp objetivo en una prueba de restauración. PostgreSQL: pg_stat_archiver muestra archivado exitoso reciente y puedes restaurar a un tiempo objetivo usando WAL archivado.

8) ¿Debo respaldar desde el primario o desde una réplica?

Respaldar desde réplicas reduce la carga en el primario, pero debes imponer chequeos de salud de replicación y umbrales de lag. De lo contrario obtendrás respaldos limpios pero con tiempo equivocado.

9) ¿Por qué las restauraciones fallan solo en entornos parecidos a producción?

Porque producción tiene las partes desordenadas: roles, grants, extensiones y comportamientos de la app que ponen a prueba tus supuestos. Tus pruebas de restauración deben incluir una prueba de humo de la aplicación, no solo “la base arranca”.

10) ¿Cuál es la prueba de restauración mínima que debería automatizar?

Levantar una instancia DB desechable, restaurar el respaldo más reciente, ejecutar un puñado de consultas representativas (incluyendo rutas críticas de autenticación) y registrar el tiempo total. Si falla, alerta con fuerza.

Conclusión: siguientes pasos que realmente reducen el riesgo

Si no haces otra cosa, haz estas tres cosas esta semana:

  1. Prueba la restauración del respaldo más reciente de MySQL y/o PostgreSQL en un entorno desechable. Cronométralo. Regístralo.
  2. Verifica los prerrequisitos de PITR: binlogs de MySQL o archivado WAL de PostgreSQL, con retención que coincida con tu RPO.
  3. Haz explícito el almacenamiento: confirma que datos, respaldos y logs viven en volúmenes persistentes o almacenamiento remoto durable, no en efímeras del contenedor.

Luego haz el movimiento adulto: conecta esas comprobaciones a automatización y monitoreo, para que dejes de depender de la memoria, la esperanza y esa única persona que “conoce los respaldos”. El objetivo no es un archivo de respaldo. El objetivo es una restauración ensayada.

← Anterior
ZFS para archivos multimedia: registros grandes, gran compresión, grandes beneficios
Siguiente →
ZFS Send incremental: copias de seguridad que no vuelven a copiarlo todo

Deja un comentario