El día en que descubres si tus copias de seguridad funcionan nunca es un día tranquilo. Es un día con un hilo de Slack en pánico, un CFO que de repente se interesa por las políticas de retención y un ingeniero que jura “siempre lo hemos hecho así”.
La recuperación punto en el tiempo (PITR) debería ser tu vía de escape: restaurar la base de datos a justo antes del despliegue problemático, la tabla eliminada, la corrección accidental de datos que “solo tocó 12 filas”. Pero la PITR no es una característica que posees hasta que la has restaurado, cronometrado y explicado el resultado a alguien que no quiere excusas.
PITR es un simulacro, no una casilla
La PITR es simple en concepto: tomar una copia base y luego reproducir el registro de transacciones hasta un tiempo objetivo, un ID de transacción o una posición de log. La base de datos reaparece, un poco molesta, en el momento anterior al incidente.
En la práctica, la PITR es una cadena de suposiciones frágiles: sincronización de tiempo, retención de logs, configuración correcta, claves de cifrado disponibles, permisos y suficiente ancho de banda de E/S para hacer la reproducción antes de que tu negocio deje de pagar nóminas. Los simulacros de PITR son cómo encuentras el eslabón débil cuando los riesgos son bajos.
Aquí está la verdad operacional: una copia de seguridad que no has restaurado es solo un montón comprimido de esperanza.
Una cita para tener en un post-it (idea parafraseada): “La esperanza no es una estrategia” — atribuida frecuentemente en círculos de operaciones al General Gordon R. Sullivan.
Aparte de la paráfrasis, el sentimiento es dolorosamente exacto.
Broma #1: Lo único más optimista que una restauración no probada es la creencia de que el incidente esperará hasta el horario de oficina.
Hechos e historia pequeña que importan en producción
Los detalles detrás de los binlogs de MySQL y el WAL de PostgreSQL no son trivia. Explican por qué ciertos hábitos de PITR funcionan y por qué otros se pudren en silencio hasta que hay un incidente.
- PostgreSQL introdujo WAL en 7.1 (2001), habilitando la recuperación de fallos y más tarde la replicación por streaming. La PITR va sobre el mismo mecanismo.
- El binlog de MySQL se convirtió en núcleo de la replicación desde temprano, y la PITR en MySQL es esencialmente “restaurar la copia base + aplicar binlogs.” Operacionalmente, la replicación y la PITR comparten modos de fallo.
- El concepto de “timeline” en PostgreSQL existe porque la recuperación puede bifurcar la historia. Si restauras y promueves, creas una nueva timeline; ignorarlo es cómo reproduces un futuro incorrecto.
- MySQL soporta formatos de binlog statement-based, row-based y mixed. La fiabilidad y determinismo de la PITR cambian drásticamente según cuál uses.
- El archivado WAL basado en archive_command de PostgreSQL antecede al uso generalizado de almacenamiento en objetos en la nube, por eso muchas empresas aún envuelven scripts de shell alrededor; y por eso esos scripts fallan de maneras creativas.
- Los GTID de MySQL (identificadores globales de transacción) se introdujeron para hacer la replicación y el failover más robustos; también mejoran el razonamiento sobre PITR cuando se usan de forma consistente.
- PostgreSQL añadió backup_label y la mecánica recovery.signal (reemplazando recovery.conf antiguo) para hacer el estado de recuperación más explícito. Los runbooks antiguos que mencionan recovery.conf aún existen y siguen provocando confusión a medianoche.
- Los valores por defecto y cambios de binlog_row_image entre variantes de MySQL influyen en cuánto dato queda en los binlogs. Eso afecta el tiempo de reproducción, el coste de almacenamiento y si puedes reconstruir ciertas filas con precisión.
PITR en MySQL vs PostgreSQL: qué difiere realmente
Qué significa “reproducir logs” en cada sistema
La PITR en MySQL reproduce binlogs: cambios lógicos a nivel de sentencias SQL (statement-based), cambios a nivel de fila (row-based) o una mezcla. La herramienta de reproducción suele ser mysqlbinlog.
La PITR en PostgreSQL reproduce WAL (Write-Ahead Log): información de redo más física, aplicada por el servidor durante la recuperación. No “aplicas WAL” con una herramienta cliente; configuras la recuperación, le proporcionas los segmentos WAL (desde archivo o streaming) y PostgreSQL hace la reproducción.
Expectativas sobre la copia base
Las copias base de MySQL varían mucho: instantáneas de sistema de archivos (LVM/ZFS), xtrabackup o volcado lógico. La PITR con binlogs depende en gran medida de tener una copia base consistente que se alinee con las coordenadas del binlog.
PostgreSQL tiene una historia más estandarizada: un pg_basebackup (o una instantánea de sistema de archivos tomada correctamente) más WAL archivado es un camino común y bien probado.
El tiempo es mentiroso (a menos que lo hagas comportarse)
La PITR en MySQL a menudo usa posiciones de binlog, nombres de archivos, timestamps embebidos en eventos o GTIDs. Si los relojes del sistema desincronizan entre máquinas, “restaurar a 14:03:00” se convierte en un baile interpretativo.
PostgreSQL tiene recovery_target_time, pero también depende del manejo de zonas horarias del clúster y de la timeline WAL en la que estés. Si restauras a un punto que no existe en la timeline elegida, fallarás o aterrizarás en un lugar sorprendente.
Observabilidad durante la recuperación
PostgreSQL expone el estado de recuperación vía vistas como pg_stat_wal_receiver, y los logs te dicen qué WAL se está aplicando. Es razonablemente introspectable.
La reproducción en MySQL vía mysqlbinlog | mysql es transparente en el sentido de “puedes ver la tubería”, pero terrible en el sentido de “¿cuál es mi progreso exacto?” a menos que la envuelvas con tu propia instrumentación.
Una preferencia operacional (opinión)
Si gestionas MySQL y te importa la PITR, favorece binlogging a nivel de fila salvo que tengas una razón específica para no hacerlo. Los binlogs por sentencia y las funciones no deterministas son el plan de retiro de un abogado de divorcios.
Cálculo RPO/RTO que no se puede pasar por alto
Los simulacros de PITR no son solo “puede restaurar?” Son “¿puede restaurar lo suficientemente rápido y con suficiente precisión?”
- RPO (Recovery Point Objective): cuánto dato puedes perder. La PITR suele apuntar a casi cero, pero solo si los logs están completos y disponibles.
- RTO (Recovery Time Objective): cuánto tiempo puedes estar caído. Esto está dominado por el tiempo de restaurar la base + tiempo de reproducir logs + tiempo de validación.
El asesino del RTO es casi siempre la E/S: descomprimir backups, escribir terabytes en disco y reproducir logs que nunca fueron diseñados para ser leídos “rápido”. Si haces simulacros de PITR en una VM pequeña con almacenamiento lento, o entrarás en pánico sin motivo o, peor, creerás en un RTO que no se sostendrá en producción.
Mide la velocidad de reproducción. Mide el rendimiento de restauración. Mide cuánto tarda ejecutar chequeos de integridad y un puñado de consultas de aplicación. Luego decide si tu ventana de retención actual y la clase de almacenamiento son compatibles con tus promesas.
Diseñar un simulacro de PITR que encuentre errores reales
Elige un incidente objetivo que puedas simular
No hagas simulacros abstractos. Simula una falla realista:
- Un
DELETEaccidental sinWHERE(clásico, perenne). - Migración incorrecta que elimina un índice y provoca timeouts por todos lados.
- Error de aplicación que escribe el ID de tenant equivocado durante 15 minutos.
- Error de operador: ejecutar un script “seguro” en el clúster equivocado.
Define criterios de éxito por adelantado
El éxito no es “la base de datos arrancó.” El éxito es:
- Restaurar al punto correcto (verificado por consultas marcador conocidas).
- Tablas críticas para la aplicación presentes y consistentes.
- Números de RTO y RPO medidos y registrados.
- Runbook actualizado según lo que falló.
Incluye siempre un paso “¿quién tiene las llaves?”
Si las copias están cifradas (deberían estarlo), tu simulacro debe incluir recuperar la clave de descifrado desde donde la organización la oculta detrás de una cola de tickets. Si la restauración requiere un rol IAM específico o un permiso de KMS, prueba eso también.
Mantén una cosa aburrida: nombres y metadatos
Cada artefacto de backup debe incluir: ID de clúster, hora de inicio/fin, etiqueta de copia base, posición de inicio de binlog/WAL y la versión del software. Metadatos que hoy parecen “obvios” se convierten en arqueología en seis meses.
Tareas prácticas: comandos, salidas, decisiones
Los comandos abajo no son decorativos. Son el tipo exacto de memoria muscular que quieres antes de un incidente. Cada tarea incluye: un comando, qué significa la salida y la decisión que tomas.
Task 1 — Verificar que el binlog de MySQL está habilitado y es razonable
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'binlog_row_image';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | ON |
+---------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| binlog_row_image | FULL |
+------------------+-------+
Significado: La PITR depende de los binlogs; si log_bin=OFF, no puedes reproducir nada. binlog_format=ROW es el valor predeterminado más seguro para PITR.
Decisión: Si los binlogs están desactivados o basados en sentencias, trata la PITR como “mejor esfuerzo” y corrige la configuración antes de afirmar un RPO bajo.
Task 2 — Comprobar la retención de binlogs de MySQL (y si está mintiendo)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'binlog_expire_logs_seconds'; SHOW VARIABLES LIKE 'expire_logs_days';"
+--------------------------+--------+
| Variable_name | Value |
+--------------------------+--------+
| binlog_expire_logs_seconds | 259200 |
+--------------------------+--------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| expire_logs_days| 0 |
+-----------------+-------+
Significado: Los binlogs expiran después de 3 días aquí. Si cumplimiento exige 14 días de PITR, no estás conforme; solo estás callado.
Decisión: Ajusta la retención según los requisitos del negocio y la realidad del almacenamiento. Si envías binlogs a otro sitio, la retención en el primario puede ser más corta, pero solo si el envío está verificado.
Task 3 — Listar archivos de binlog de MySQL y elegir una ventana de reproducción
cr0x@server:~$ mysql -e "SHOW BINARY LOGS;"
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000421 | 52428800 |
| mysql-bin.000422 | 52428800 |
| mysql-bin.000423 | 8388608 |
+------------------+-----------+
Significado: Tienes segmentos de binlog presentes en el servidor. Aún no sabes si cubren el tiempo del incidente.
Decisión: Identifica qué binlog contiene la ventana del incidente usando los timestamps de mysqlbinlog (tarea siguiente) o GTIDs.
Task 4 — Inspeccionar timestamps de binlog de MySQL alrededor de un incidente
cr0x@server:~$ mysqlbinlog --base64-output=DECODE-ROWS --verbose --start-datetime="2025-12-30 13:55:00" --stop-datetime="2025-12-30 14:10:00" /var/lib/mysql/mysql-bin.000423 | head -n 25
# at 4
#251230 13:55:02 server id 1 end_log_pos 123 CRC32 0x4f2b1a77 Start: binlog v 4, server v 8.0.36 created 251230 13:55:02
# at 123
#251230 14:02:11 server id 1 end_log_pos 456 CRC32 0x9c4d6e10 Query thread_id=812 exec_time=0 error_code=0
SET TIMESTAMP=1735567331/*!*/;
BEGIN
# at 456
#251230 14:02:11 server id 1 end_log_pos 892 CRC32 0x19a0f2cb Table_map: `app`.`users` mapped to number 108
Significado: Puedes localizar la ventana temporal y ver qué tablas se tocaron. Esto te ayuda a fijar un punto de parada preciso.
Decisión: Elige --stop-datetime justo antes de la sentencia dañina, o encuentra la posición/GTID exacta para control preciso.
Task 5 — Extraer eventos de binlog de MySQL a un archivo (no canalices a ciegas)
cr0x@server:~$ mysqlbinlog --start-datetime="2025-12-30 13:55:00" --stop-datetime="2025-12-30 14:02:10" /var/lib/mysql/mysql-bin.000423 > /tmp/pitr_replay.sql
cr0x@server:~$ tail -n 5 /tmp/pitr_replay.sql
# at 3321
#251230 14:02:10 server id 1 end_log_pos 3456 CRC32 0x2f1b8c3a Xid = 991228
COMMIT/*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /*!*/;
DELIMITER ;
# End of log file
Significado: Tienes un script de reproducción determinista para esa ventana. Esto es auditable y te permite buscar “DROP” antes de pulsar Enter.
Decisión: Revisa por sentencias peligrosas y confirma el punto de parada. En restauraciones de alto riesgo, “confiar en la tubería” es cómo te justificarás después.
Task 6 — Restaurar la copia base de MySQL en un datadir de staging
cr0x@server:~$ sudo systemctl stop mysql
cr0x@server:~$ sudo rsync -aH --delete /backups/mysql/base/2025-12-30_1200/ /var/lib/mysql/
cr0x@server:~$ sudo chown -R mysql:mysql /var/lib/mysql
cr0x@server:~$ sudo systemctl start mysql
cr0x@server:~$ mysqladmin ping
mysqld is alive
Significado: La restauración base tuvo éxito y MySQL arranca. Esto no dice nada sobre si los datos coinciden con el punto de restauración previsto.
Decisión: Captura las coordenadas de binlog o el estado GTID desde los metadatos del backup, luego aplica binlogs desde allí hasta el tiempo objetivo.
Task 7 — Aplicar el script de reproducción de MySQL y vigilar errores
cr0x@server:~$ mysql --show-warnings < /tmp/pitr_replay.sql
Warning (Code 1287): 'SET @@SESSION.GTID_NEXT' is deprecated and will be removed in a future release.
Significado: Las advertencias no son fatales, pero los errores sí. Un error fatal común es faltar tablas porque la copia base no coincide con el punto de inicio de la reproducción.
Decisión: Si ves errores como “Table doesn’t exist,” para. Tu copia base + rango de binlog es inconsistente; corrige las coordenadas de inicio.
Task 8 — Verificar el estado GTID de MySQL después de la PITR (si usas GTIDs)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'gtid_mode'; SELECT @@global.gtid_executed\G"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode | ON |
+---------------+-------+
*************************** 1. row ***************************
@@global.gtid_executed: 8b3f9b75-9c67-11ee-9b7d-0242ac120002:1-889122
Significado: Los GTID muestran qué transacciones existen. Después de la PITR, esto importa si pretendes replicar desde/hacia esta instancia.
Decisión: Si vas a reincorporarte a la replicación, asegúrate de que los conjuntos GTID se alineen con tu plan de topología. Si no, crearás confusión en la replicación que parecerá corrupción de datos.
Task 9 — Verificar que el archivado de PostgreSQL está configurado y realmente funcionando
cr0x@server:~$ psql -d postgres -c "SHOW wal_level; SHOW archive_mode; SHOW archive_command; SHOW archive_timeout;"
wal_level
-----------
replica
(1 row)
archive_mode
--------------
on
(1 row)
archive_command
---------------------------------------------------------
test ! -f /wal_archive/%f && cp %p /wal_archive/%f
(1 row)
archive_timeout
-----------------
60s
(1 row)
Significado: El archivado está activado y el comando copia WAL a un directorio de archivo local. Esto es un comienzo, no una prueba.
Decisión: Confirma que los archivos WAL aparecen realmente y que las fallas se alertan. “Configurado” no es “funcionando”.
Task 10 — Comprobar el backlog del archivo de PostgreSQL y la actividad WAL reciente
cr0x@server:~$ ls -lh /wal_archive | tail -n 5
-rw------- 1 postgres postgres 16M Dec 30 14:00 000000010000002A0000009F
-rw------- 1 postgres postgres 16M Dec 30 14:01 000000010000002A000000A0
-rw------- 1 postgres postgres 16M Dec 30 14:02 000000010000002A000000A1
-rw------- 1 postgres postgres 16M Dec 30 14:03 000000010000002A000000A2
-rw------- 1 postgres postgres 16M Dec 30 14:04 000000010000002A000000A3
Significado: Los segmentos WAL se están archivando, los nombres son secuenciales y los timestamps parecen saludables.
Decisión: Si el archivo está desactualizado, deja de fingir que la PITR existe. Arregla el archivado primero; las copias base solas no son PITR.
Task 11 — Crear una copia base de PostgreSQL que realmente puedas restaurar
cr0x@server:~$ sudo -u postgres pg_basebackup -D /backups/pg/base/2025-12-30_1200 -Fp -Xs -P -R
waiting for checkpoint
22994/22994 kB (100%), 1/1 tablespace
22994/22994 kB (100%), 1/1 tablespace
Significado: Tomaste una copia física base e incluiste streaming de WAL (-Xs), además escribiste configuración de replicación (-R). Para PITR, aún necesitas WAL archivado o disponibilidad de WAL stream.
Decisión: Registra la etiqueta del backup y el timestamp. Asegura que el rango correspondiente de WAL exista y se retenga.
Task 12 — Restaurar la copia base de PostgreSQL en un nuevo datadir
cr0x@server:~$ sudo systemctl stop postgresql
cr0x@server:~$ sudo rm -rf /var/lib/postgresql/16/main
cr0x@server:~$ sudo rsync -aH /backups/pg/base/2025-12-30_1200/ /var/lib/postgresql/16/main/
cr0x@server:~$ sudo chown -R postgres:postgres /var/lib/postgresql/16/main
Significado: Los archivos del clúster están en su lugar. Ahora decides si estás haciendo PITR (recuperación) o simplemente arrancando un clon.
Decisión: Para PITR, configura objetivos de recuperación y restore_command; no lo inicies como primario normal a menos que pretendas bifurcar una timeline.
Task 13 — Configurar PITR en PostgreSQL: restore_command y tiempo objetivo
cr0x@server:~$ sudo -u postgres bash -c 'cat >> /var/lib/postgresql/16/main/postgresql.conf <<EOF
restore_command = '\''cp /wal_archive/%f %p'\''
recovery_target_time = '\''2025-12-30 14:02:10+00'\''
recovery_target_action = '\''pause'\''
EOF'
cr0x@server:~$ sudo -u postgres touch /var/lib/postgresql/16/main/recovery.signal
cr0x@server:~$ sudo systemctl start postgresql
Significado: PostgreSQL arrancará en modo recuperación, obtendrá WAL desde el archivo, reproducirá hasta el tiempo objetivo y luego pausará.
Decisión: Pausar es deliberado: inspeccionas datos antes de promover. Si auto-promueves, puedes consagrar accidentalmente el punto incorrecto en el tiempo como “verdad”.
Task 14 — Observar el progreso de recuperación de PostgreSQL y confirmar que se alcanzó el objetivo
cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
pg_is_in_recovery
-------------------
t
(1 row)
cr0x@server:~$ sudo -u postgres psql -c "SELECT now(), pg_last_wal_replay_lsn();"
now | pg_last_wal_replay_lsn
------------------------------+-------------------------
2025-12-30 14:02:12.123+00 | 2A/A100F2B0
(1 row)
Significado: Estás en recuperación y tienes un LSN de reproducción. Los logs normalmente dirán que alcanzaste recovery_target_time y que pausaste.
Decisión: Ejecuta consultas de validación ahora. Si es correcto, promueve; si no, ajusta el objetivo y reinicia la recuperación.
Task 15 — Promover PostgreSQL después de la validación
cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_promote(wait_seconds => 60);"
pg_promote
------------
t
(1 row)
cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
pg_is_in_recovery
-------------------
f
(1 row)
Significado: El servidor ahora es escribible y existe una nueva timeline.
Decisión: Decide cómo esta instancia vuelve a la topología. Una restauración PITR promovida no es automáticamente segura para reconectar como si nada hubiera pasado.
Task 16 — Validar datos en el límite de la aplicación (no solo “conteos de filas”)
cr0x@server:~$ psql -d appdb -c "SELECT COUNT(*) FROM users; SELECT MAX(updated_at) FROM users;"
count
---------
1284932
(1 row)
max
-----------------------
2025-12-30 14:02:05+00
(1 row)
Significado: Conteos y timestamps te dan una cordura rápida. El timestamp máximo anterior al incidente es una buena señal.
Decisión: Si los datos parecen incorrectos, no promuevas (PostgreSQL) ni hagas el corte (MySQL). Ajusta el punto de restauración y vuelve a ejecutar.
Task 17 — Medir cuellos de botella de E/S en la restauración (básicos de Linux que deciden tu RTO)
cr0x@server:~$ iostat -xm 2 3
Linux 6.5.0 (server) 12/31/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.10 0.00 6.20 38.70 0.00 42.00
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
nvme0n1 1200.0 98000.0 0.0 0.0 1.2 81.7 2200.0 180000.0 0.0 0.0 6.8 81.8 16.2 99.0
Significado: %util cerca de 99% y alto iowait significa que el almacenamiento es el cuello de botella. Tu restauración no mejorará añadiendo CPU.
Decisión: Mueve restauraciones a volúmenes más rápidos, usa NVMe local para scratch, paraleliza la descompresión o reduce la reproducción tomando copias base más frecuentes.
Task 18 — Confirmar sincronización de tiempo antes de PITR basado en tiempo
cr0x@server:~$ timedatectl
Local time: Wed 2025-12-31 10:12:03 UTC
Universal time: Wed 2025-12-31 10:12:03 UTC
RTC time: Wed 2025-12-31 10:12:03
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Significado: Tu reloj del sistema está sincronizado. Sin esto, “restaurar a 14:02” se vuelve conjetura.
Decisión: Si los relojes no están sincronizados, prefiere recuperación basada en posición (MySQL) o razonamiento basado en LSN (PostgreSQL) y arregla NTP antes del próximo simulacro.
Tres microhistorias desde la vida corporativa
Microhistoria 1: El incidente causado por una suposición equivocada
Una compañía SaaS mediana ejecutaba MySQL en VMs gestionadas. “Tenían PITR” porque existían copias base nocturnas y binlog estaba habilitado. La suposición: un agente copiaba los binlogs al almacenamiento de objetos y la retención en el bucket era “más o menos para siempre”.
Un desarrollador lanzó una migración que reconstruyó mal una tabla grande. No rompió la aplicación inmediatamente; simplemente empezó a escribir datos sutilmente incorrectos. Cuatro horas después, un cliente lo notó. El comandante de incidentes pidió PITR “a 30 minutos antes del despliegue”.
La restauración empezó sin problemas: la copia base se restauró, MySQL arrancó y el equipo empezó a aplicar binlogs. Luego la reproducción chocó con un hueco. Faltaba el archivo de binlog para una ventana de 40 minutos. No corrupto. Perdido.
La causa raíz no fue exótica. El agente de copia fallaba silenciosamente por errores de permiso después de un cambio en la política del bucket. Los binlogs en el primario ya habían expirado por una retención demasiado corta. Su “PITR” tenía un hueco de cuatro horas en mitad del día, como una mala memoria.
La solución no fue un nuevo proveedor. Fue aburrida: alertar sobre fallos de archivado, extender la retención hasta que el envío sea verificablemente fiable y añadir un paso al simulacro: “listar binlogs en el archivo para la ventana del incidente”.
Microhistoria 2: La optimización que salió mal
Otra organización ejecutaba PostgreSQL con archivado WAL. Los simulacros de restauración eran “muy lentos”, así que alguien optimizó comprimiendo WAL agresivamente y empujándolo a través de una canalización single-thread de cifrado+upload en el nodo de base de datos. La CPU subió, pero el tamaño del archivo cayó. Todos celebraron.
La celebración duró hasta el primer intento real de PITR. El clúster de restauración podía obtener WAL, pero la descompresión se convirtió en el cuello de botella y la canalización single-thread hacía que el archivo quedara retrasado respecto a los picos de escritura. Durante el incidente, los segmentos WAL más recientes no estaban aún archivados. La recuperación se quedó corta del tiempo objetivo.
El equipo intentó “esperar un poco a que el archivo se ponga al día.” Eso no es un RTO; es una plegaria con cita en el calendario.
Posterior al incidente, la solución fue contraintuitiva: reducir el nivel de compresión, mover cifrado/subida fuera del primario, paralelizar la canalización y establecer SLO explícitos de “WAL archivado dentro de X segundos”. Aceptaron más coste de almacenamiento y recuperaron predictibilidad.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día
Una compañía vinculada a finanzas hacía un simulacro semanal de PITR. No era glamoroso. Era una checklist, un ticket y un breve informe. También molestaba lo suficiente a todos como para ser efectivo.
Durante un simulacro de rutina, el ingeniero notó que la restauración funcionaba pero siempre aterrizaba cinco a siete minutos después del timestamp solicitado. Los datos “se veían bien”, pero la descoincidencia les molestó. Indagaron y encontraron confusión de zona horaria: el runbook usaba hora local, pero recovery_target_time esperaba UTC. Arreglaron el runbook, estandarizaron en UTC y añadieron una tabla marcador con una inserción timestamp durante los despliegues.
Dos meses después, un incidente de producción impactó: un job batch actualizó la partición equivocada durante unos ocho minutos. El equipo restauró justo antes del marcador y validó rápido. Cumplieron su RTO porque ya habían discutido el detalle de zona horaria en un simulacro de bajo riesgo.
Broma #2: Las zonas horarias son como la latencia de replicación: todos aceptan que existen y luego arruinan tu tarde de todos modos.
Guía de diagnóstico rápido
Las restauraciones fallan o se ralentizan por unas razones previsibles. El truco es identificar cuál en minutos, no en horas. Este es el orden que uso cuando alguien dice “PITR es lento/está roto”.
Primero: ¿faltan logs requeridos?
- MySQL: confirma la cobertura de binlog para el tiempo objetivo. ¿Tienes los archivos de binlog localmente o en el archivo para toda esa ventana?
- PostgreSQL: confirma la cobertura del archivo WAL y que
restore_commandpueda obtener el siguiente segmento necesario.
Si faltan logs, deja de optimizar. No tienes un problema de rendimiento; tienes un límite de pérdida de datos.
Segundo: ¿la copia base es consistente con el inicio de la reproducción?
- MySQL: la copia base debe coincidir con las coordenadas de binlog/conjunto GTID desde donde empiezas. La descoincidencia produce tablas faltantes o errores de clave duplicada durante la reproducción.
- PostgreSQL: la copia base debe estar completa e incluir los archivos necesarios; el WAL requerido desde el inicio de la copia debe existir.
Tercero: ¿es E/S, CPU o red?
- Limitado por E/S: alto iowait, discos con alta utilización, velocidad de restauración que se estanca independientemente de CPU.
- Limitado por CPU: compresión/descompresión/cifrado pegando los cores, discos no plenamente utilizados.
- Limitado por red: fetch lento desde la ubicación del archivo, alta latencia, límites de throughput.
Cuarto: ¿elegiste el objetivo y la timeline correctos?
- PostgreSQL: las descoincidencias de timeline tras una promoción son clásicas. Restaurar desde la timeline equivocada da “requested timeline does not contain minimum recovery point” o aterrizas en un punto inesperado.
- MySQL: la reproducción basada en tiempo es vulnerable al skew del reloj y la confusión de zona horaria; posición/GTID es más seguro cuando sea posible.
Quinto: ¿estás validando lo correcto?
“La base de datos arrancó” no es validación. Valida un pequeño conjunto de consultas de negocio, constraints y flujos de aplicación. Si no puedes definirlos, tu métrica de éxito de restauración es principalmente sensaciones.
Errores comunes: síntomas → causa raíz → solución
1) “La PITR se detiene temprano y no alcanza el tiempo objetivo”
Síntomas: La recuperación de PostgreSQL pausa antes del objetivo; los logs mencionan WAL faltante. La reproducción de MySQL termina sin alcanzar el tiempo del incidente.
Causa raíz: hueco en el archivo WAL/binlog, retención demasiado corta o fallos en el envío.
Solución: Alertar sobre fallos de archive_command, verificar la completitud del archivo diariamente, extender la retención y almacenar logs independientemente del primario.
2) “La reproducción de binlog en MySQL lanza errores de tabla faltante”
Síntomas: ERROR 1146 (42S02): Table '...' doesn't exist durante la reproducción.
Causa raíz: La copia base se restauró desde el tiempo T, pero la reproducción empezó desde binlogs generados antes de que la tabla existiera (o después de que fue eliminada/ recreada).
Solución: Empieza la reproducción desde la posición/GTID exacta registrada con la copia base; no adivines usando timestamps solamente.
3) “La recuperación de PostgreSQL hace bucle en el mismo segmento WAL”
Síntomas: Los logs muestran repetidos intentos de restaurar el mismo %f.
Causa raíz: restore_command devuelve éxito pero no coloca realmente el archivo (o coloca un archivo truncado). Clásico cuando scripts tragan errores.
Solución: Haz que restore_command falle ruidosamente. Verifica tamaño y checksum del archivo. Evita wrappers que hagan “exit 0” siempre.
4) “La restauración es dolorosamente lenta, pero la CPU está baja”
Síntomas: Tiempos largos de restauración; iostat muestra discos saturados; CPU mayormente ociosa.
Causa raíz: techo de throughput/IOPS del almacenamiento; demasiadas escrituras aleatorias pequeñas durante la reproducción; WAL/binlog en medios lentos.
Solución: Usa volúmenes scratch más rápidos para restaurar, separa datos de logs, considera copias base más frecuentes y asegúrate de que la ruta de fetch del archivo no esté limitada.
5) “La restauración es rápida y luego falla al final con quejas de corrupción”
Síntomas: PostgreSQL se queja de registro de checkpoint inválido; MySQL no arranca por problemas de log de InnoDB.
Causa raíz: Copia base incompleta/corrupta, o instantánea de sistema de archivos tomada sin quiescing apropiado.
Solución: Usa herramientas diseñadas para backups consistentes (pg_basebackup, procedimientos de snapshot probados, xtrabackup) y verifica la integridad del backup en el simulacro.
6) “La PITR aterriza en el tiempo equivocado”
Síntomas: El estado restaurado incluye cambios que deberían haber sido excluidos, o falta cambios que deberían incluirse.
Causa raíz: Mismatch de zona horaria, skew de reloj o malentendido de los límites inclusivos/exclusivos en la reproducción.
Solución: Estandariza en UTC para timestamps de incidentes, inserta una transacción marcador y prefiere objetivos por posición/LSN/GTID cuando sea posible.
7) “Todo restaura, pero la replicación después es un caos”
Síntomas: Réplicas PostgreSQL no pueden seguir; errores de replicación MySQL sobre GTIDs ejecutados o entradas duplicadas.
Causa raíz: Promoviste una instancia restaurada sin planear la nueva topología, timeline o alineación de conjuntos GTID.
Solución: Trata la restauración PITR como una bifurcación: decide el nodo autoritativo, reconstruye réplicas desde él y documenta el procedimiento de reingreso.
Listas de verificación / plan paso a paso
Simulacro semanal de PITR (90 minutos, aburrido a propósito)
- Elige un escenario: selecciona una ventana de “cambio malo” conocida de la última semana (o crea una transacción marcador inocua).
- Elige un punto objetivo: define el punto de restauración en UTC y regístralo.
- Confirma cobertura de logs: verifica que binlogs/WAL existan para toda la ventana.
- Restaura la copia base: en infraestructura aislada con clase de almacenamiento similar a producción.
- Reproduce logs hasta el objetivo: MySQL vía
mysqlbinlog; PostgreSQL vía configuración de recuperación. - Valida: 5–10 consultas críticas, una prueba smoke de aplicación y una verificación de integridad.
- Registra tiempos: tiempo de restauración base, tiempo de reproducción, tiempo de validación.
- Escribe un informe: qué falló, qué fue lento, qué cambió en el runbook.
Antes de afirmar “tenemos PITR” (barrera mínima)
- Las copias base son consistentes y restaurables sin heroísmos.
- El archivado de logs está completo, monitorizado y retenido según requisito.
- El runbook de restauración incluye pasos de IAM/KMS/acceso a claves.
- Los números de RTO/RPO están medidos, no deseados.
- Existen consultas de validación y alguien las posee.
Pasos del simulacro específicos para MySQL
- Registra el archivo/posición de binlog de la copia base o el conjunto GTID.
- Confirma
binlog_formaty la retención. - Extrae el SQL de reproducción a un archivo y revisa por sentencias peligrosas.
- Aplica la reproducción a la instancia restaurada; detente en el primer error y corrige la descoincidencia de coordenadas.
- Decide el plan de topología: ¿nuevo primario, clon temporal para analítica o fuente para reconstruir réplicas?
Pasos del simulacro específicos para PostgreSQL
- Confirma archivado WAL y frescura del archivo.
- Restaura la copia base; asegura
recovery.signaly elrestore_commandcorrecto. - Usa
recovery_target_action='pause'para simulacros y validar antes de promover. - Después de promover, anota la nueva timeline y planea la reconstrucción de réplicas acorde.
- Verifica que tu archivo contenga WAL a través de cambios de timeline (una trampa común a largo plazo).
Preguntas frecuentes
1) ¿PITR es lo mismo que alta disponibilidad?
No. HA te mantiene en funcionamiento ante fallos de nodos. PITR recupera desastres lógicos: despliegues malos, borrados, corrupción causada por humanos. Necesitas ambos.
2) ¿Cuál es “más fácil” para PITR: MySQL o PostgreSQL?
El flujo de PITR de PostgreSQL está más estandarizado: copia base + archivo WAL + objetivos de recuperación. PITR en MySQL puede ser limpia, pero varía más según el método de backup y el formato de binlog.
3) ¿Debería usar objetivos basados en timestamp o en posición?
Basados en posición (posición de binlog/GTID en MySQL, LSN en PostgreSQL) suelen ser más deterministas. Los objetivos por tiempo son amigables para humanos pero frágiles a menos que los relojes y zonas horarias estén disciplinados.
4) ¿Con qué frecuencia debo tomar copias base si tengo logs?
Tan seguido como tolere tu tiempo de reproducción. Brechas más largas significan más reproducción de logs. Si tu reproducción de WAL/binlog para un día toma seis horas, tu RTO ya está decidido por ti.
5) ¿Puedo hacer PITR desde backups lógicos (mysqldump/pg_dump)?
No de manera fiable en el sentido estricto de PITR. Los volcados lógicos capturan un punto, pero reproducir hasta un tiempo exacto es engorroso y lento. Úsalos para portabilidad; usa backups físicos más logs para PITR.
6) ¿Cuál es la forma más limpia de validar una restauración PITR?
Un pequeño conjunto de invariantes de negocio: conteos clave, marcadores de última actualización, chequeos de integridad referencial y una prueba smoke ligera de la aplicación. Si tu validación es “SELECT 1”, estás practicando la negación.
7) ¿Y el cifrado y las claves?
Si los backups o archivos están cifrados, tu simulacro debe incluir recuperar claves y permisos. La falla más común de “no podemos restaurar” es acceso, no datos.
8) ¿Puede PITR restaurar a través de upgrades de versión mayor?
Típicamente, no en el sitio para backups físicos. Los cambios de versión mayor a menudo requieren migración lógica o herramientas de upgrade. Tu plan de PITR debe asumir “restaurar en la misma versión mayor” y luego actualizar si es necesario.
9) ¿Cómo evito que la instancia restaurada se reconecte accidentalmente a producción?
Coloca las restauraciones en redes aisladas, cambia credenciales y deshabilita explícitamente conectividad saliente a servicios de producción. También: renombra el clúster y añade banners ruidosos en la monitorización.
10) ¿Cuál es la métrica que desearía que todos los equipos midieran?
“Tiempo desde el inicio del incidente hasta datos restaurados validados.” No “backup succeeded.” No “WAL archived.” El resultado de extremo a extremo es lo que experimenta el negocio.
Próximos pasos que realmente reducen el riesgo
Si no haces otra cosa este trimestre, haz esto: programa un simulacro de PITR, ejecútalo en infraestructura que se parezca a producción y apunta exactamente dónde te confundiste. Esa confusión es tu futuro outage.
- Escoge una base de datos (la que más duele) y ejecuta una restauración a un tiempo marcador conocido.
- Instrumenta la canalización: alerta sobre fallos de envío de binlog/WAL y frescura del archivo.
- Estandariza objetivos: timestamps en UTC más una posición/LSN/GTID cuando sea posible.
- Mide el RTO y decide si gastar dinero en almacenamiento más rápido, copias base más frecuentes o ventanas de reproducción más pequeñas.
- Actualiza el runbook inmediatamente después del simulacro, mientras el dolor está fresco y las lecciones son honestas.
La PITR no es una característica que activas. Es una habilidad que practicas. A la restauración no le importa lo confiado que suenes en las reuniones.