devto 2026-07-26 원문 보기 ↗
Migraste una base de datos MySQL de Amazon RDS a Amazon Aurora. La read replica de Aurora se puso al día, la promoviste, cambiaste el endpoint de la app, y ahora los writes aterrizan en Aurora.
Diez minutos después, o quizá tres horas después, algo anda mal y quieres regresar a la vieja instancia de RDS.
Aquí está la trampa. La vieja instancia de RDS está congelada en el momento en que hiciste el corte. Cada pedido, registro y edición que pegó en Aurora desde entonces no existe en RDS. "Revertir" de forma ingenua significa "tirar a la basura todo lo que pasó en la base de datos nueva". Y el arreglo de siempre, tomar una ventana de mantenimiento y recargar RDS desde Aurora, no escala: a 100 TB una recarga se mide en días, no en minutos.
Esta es la historia de construir una reversión que conserva los datos
nuevos, probarla en AWS real hasta que se rompió de maneras interesantes, y reducirla a un manual de operaciones que una persona cansada pueda seguir a las 3 de la mañana. Todo lo de abajo lo corrí en instancias desechables en una región aislada y las borré después.
Seconds_Behind_Source = 0. Llegar ahí significa que RDS tiene el 100% de los writes de la era Aurora. Hacer el corte antes pierde datos.DROP TABLE y unos cuantos ALTER). Las dos las validé en vivo.La migración hacia adelante de RDS a Aurora es una cosa resuelta de un solo botón: creas una read replica de Aurora desde la instancia de RDS, esperas, promueves. AWS hace el snapshot y convierte por ti. Esa parte la tomo como dada.
La dirección interesante es hacia atrás. Una vez que Aurora es primaria y toma writes, la vieja instancia de RDS se va quedando más desactualizada cada segundo. Una reversión que importa tiene que responder una pregunta: ¿a dónde van los writes de la era Aurora? Si la respuesta es "a ningún lado, los perdemos", no es una reversión, es un desastre con pasos extra.
La respuesta que escala es invertir la replicación. Cuando RDS era la fuente y Aurora la réplica, los datos fluían de RDS a Aurora. Después de la promoción lo configuras al revés: Aurora es la fuente, RDS es la réplica.
Ahora RDS jala de forma continua todo lo que Aurora escribe. La vieja
instancia no está congelada, es un espejo caliente que siempre va un segundo o dos atrás de Aurora. Revertir se vuelve: detén Aurora, deja que RDS termine el último segundo, cambia el endpoint.
La parte elegante es el costo de la sincronización inicial. En el instante de la promoción, RDS ya es byte por byte igual a Aurora, porque era la fuente. Así que el enlace inverso no re-copia la base de datos. La siembras en la coordenada de la promoción y de ahí en adelante solo carga el delta.
Por eso el tamaño de la base de datos deja de importar.
Levanté el mundo post corte en instancias desechables: un clúster de Aurora MySQL como "prod nuevo", una instancia de RDS for MySQL como el objetivo de reversión, en una región aislada. Luego cableé la replicación inversa y la empujé por el camino feliz y por los caminos de falla.
Primer intento de arrancar la replicación, la réplica nada más se quedó ahí sentada:
Replica_IO_Running: Connecting
Source_Server_Id: 0
Last_IO_Errno: 0
Last_IO_Error:
Connecting, para siempre. Sin número de error, sin texto de error. El hilo de IO estaba tratando de alcanzar Aurora y no recibía nada, pero nada se reportaba. La causa: las dos bases de datos eran públicamente accesibles (para que mi cliente de SQL las alcanzara), lo que significaba que la réplica resolvía el endpoint de Aurora a su IP pública. La conexión de replicación entonces llegaba a Aurora desde la IP pública de la réplica, que el security group no permitía. Un drop silencioso, presentado como un esperanzado Connecting.
La lección no es "abre la IP pública". Es lo contrario. En producción, mantén las dos bases de datos privadas. Entonces el tráfico de réplica a fuente fluye sobre IPs privadas dentro de la VPC, y una sola regla de autorreferencia del security group lo cubre. La accesibilidad pública fue lo que creó el problema.
Con la red arreglada, la mecánica funcionó exactamente como se anunciaba.
Escribe dos filas a Aurora, y en unos segundos aparecen en RDS. Detén el enlace, drena a cero lag, corre los dos procedimientos de reversión, y RDS se vuelve una primaria independiente que acepta writes mientras Aurora queda desacoplada. Cero datos perdidos. La secuencia completa está en el manual de operaciones al final.
Todo el diseño descansa en una sola disciplina, así que me obligué a verla fallar. Detuve la réplica, escribí dos filas más a Aurora, y luego "reverti" sin esperar a que la réplica se pusiera al día.
Las dos filas escritas durante la ventana se fueron. Esta es toda la razón por la que Seconds_Behind_Source = 0 es una compuerta dura y no una sugerencia. La reversión es sin pérdidas solo si congelas los writes en la fuente y dejas que la réplica drene a cero antes de hacer el corte.
Esperaba que los writes divergentes en el objetivo rompieran la replicación ruidosamente. No lo hicieron. Las read replicas de RDS traen por default slave_exec_mode = IDEMPOTENT, que en silencio hace que la fuente gane un conflicto de primary key en lugar de dar error. Así que un write perdido en el objetivo de reversión se sobreescribe calladito, no se marca.
Lo que sí la rompe es el schema drift. Tiré una columna en el objetivo, luego escribí una fila en la fuente que usaba esa columna. El aplicador de SQL se detuvo en seco:
Last_SQL_Errno: 13146
Replica_SQL_Running: No
La réplica se congeló y se quedó atrás en silencio, todavía reportando un hilo de IO sano. La conclusión operativa: alarma sobre que Replica_SQL_Running voltee a No, no solo sobre el lag. Y durante la ventana de standby caliente, trata el objetivo como estrictamente solo lectura y congela el DDL en la fuente.
La variante GTID es la que quieres en producción, porque sobrevive a un failover de Aurora. Al configurarla, el stored procedure rechazó mi llamada:
ERROR 1318 (42000): Incorrect number of arguments for PROCEDURE
mysql.rds_set_external_master_with_auto_position; expected 6, got 5
El procedimiento toma seis argumentos: host, port, user, password,
ssl_encryption, y delay. Leyendo ejemplos que omiten el delay final, es fácil pasar cinco. Trampa relacionada: el tunable se llama gtid-mode con guion en el parameter group, mientras que gtid_mode con guion bajo es una vista de runtime de solo-lectura que rechaza modificaciones.
Esta costó el más tiempo y produjo el síntoma más confuso. Después de
habilitar GTID, la replicación no arrancaba:
Got fatal error 1236 from source when reading data from binary log:
'Binary log is not open'
En Aurora, @@gtid_mode era ON, pero @@log_bin era 0. El binary logging estaba apagado, así que no había nada que replicar. La causa: había puesto binlog_format=ROW y dos parámetros de GTID en una sola edición del parameter group, y uno de los nombres de parámetro estaba mal. Un modify de parameter group es todo o nada. Toda la edición se rechazó, lo que en silencio dejó binlog_format en su default OFF. Los parámetros de GTID, puestos en una llamada posterior exitosa, estaban bien, que es por lo que el
estado se veía a medio arreglar.
El arreglo: pon binlog_format=ROW, reinicia el writer de Aurora, y
verifica @@log_bin = 1 y un SHOW MASTER STATUS no vacío antes de cablear la réplica. Aurora necesita el reinicio para de verdad abrir el binary log.
Una vez que eso quedó bien, el auto-posicionamiento de GTID funcionó de principio a fin:
Auto_Position: 1
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
Retrieved_Gtid_Set: <aurora-uuid>:1-3
El binlog nativo es la ruta recomendada, pero AWS DMS vale la pena conocerlo porque es casi por completo manejado por consola. Corrí una tarea de full load más CDC de Aurora a una segunda instancia de RDS. Funcionó, y es rápido:
| Métrica | Resultado |
|---|---|
| Full load, 2 tablas, 274 MB | 20.2 s (como 48 GB/h en la instancia más chica) |
| CDC, un solo insert | propagado en menos de 12 s |
Pero DMS tiene un hueco que el binlog no. Creé una tabla en Aurora (se replicó bien), luego la tiré:
El CDC de DMS no replica DROP TABLE ni RENAME TABLE, e ignora unos cuantos ALTER. Sobre una ventana de reversión larga eso es schema drift silencioso. Usa DMS si quieres el flujo de consola y puedes vivir con la restricción; usa binlog si quieres que el DDL venga incluido.
El punto de todo el ejercicio es que el tiempo de reversión se desacopla del tamaño de la base de datos. Dos tablas cuentan la historia. La primera es la ejecución de la reversión cuando la réplica caliente se mantiene corriendo:
| Tamaño de la BD | Tiempo de reversión (réplica caliente) |
|---|---|
| 1 GB | ~1 a 2 min |
| 100 GB | ~1 a 3 min |
| 1 TB | ~1 a 5 min |
| 100 TB | ~1 a 5 min |
Constante. La única variable es cuánto delta se apiló durante el incidente, y una instancia de clase producción drena eso a como 1 a 2 GB por minuto, así que hasta un backlog gordo de 10 GB se limpia en minutos de un solo dígito.
La segunda tabla es lo que pasa si no mantuviste la réplica caliente y tienes que reconstruir el fallback desde cero. Este es el camino a evitar:
| Tamaño de la BD | Reconstruir vía DMS paralelo (~0.9 TB/h) | Reconstruir vía dump + restore (~60 GB/h) |
|---|---|---|
| 100 GB | ~7 min | ~1.7 h |
| 1 TB | ~1.1 h | ~17 h |
| 100 TB | ~4 a 5 días | inviable |
A 100 TB la diferencia es minutos contra días. Ese es todo el argumento para mantener la vieja instancia caliente.
Un ancla medida para los números de reconstrucción: en una instancia de clase producción (32 vCPU, 256 GB) un insert lógico de un solo hilo corrió a como 1.2 GB por minuto, o 72 GB/h. El apply de binlog basado en filas y el restore lógico son del mismo orden de magnitud, que es sobre lo que están construidos los estimados de arriba. Las mediciones de instancia chica (un par de MB/s de apply de binlog, como 48 GB/h de full load de DMS) son pisos conservadores que escalan hacia arriba con el tamaño de la instancia.
Traté de sacar números exactos de clase producción corriendo toda la matriz en instancias de 32 vCPU con 100 GB de datos. Pasaron dos cosas. Primero, la migración hacia adelante se negó a arrancar:
Cannot upgrade from mysql 8.0.46 to aurora-mysql 8.0.mysql_aurora.3.12.0
Una read replica de RDS a Aurora requiere que la versión de MySQL del motor de Aurora sea al menos la de la fuente. La fuente iba un minor adelante del Aurora más nuevo, así que se rechazó. Reconstruye la fuente en una versión que empate y funciona, pero eso significaba regenerar los datos. Segundo, y más al punto, las instancias grandes cuestan dinero real por minuto. Maté la corrida y me eché para atrás a calcular desde las anclas de instancia chica.
La lección, pagada en dólares: no necesitas hardware de clase producción para medir una tasa de throughput. Una tasa se extrapola. Mide barato, extrapola con honestidad, etiqueta los pisos.
Un punto medio tentador es mantener una instancia de RDS aprovisionada y corriendo pero vacía, lista para llenarse bajo demanda. Suena ahorrador. Es lo peor de los dos mundos.
Una instancia vacía e inactiva cuesta lo mismo por hora que una réplica caliente, porque pagas por el cómputo, no por los datos. Pero una instancia vacía no tiene línea base, así que revertir significa cargar la base de datos entera en ella primero. Ese es el camino de reconstrucción lineal con el tamaño: minutos a 100 GB, días a 100 TB. Pagas precio completo de standby por la velocidad de reversión de no tener nada. Peor, estarías recargando desde Aurora, la base de datos de la que estás tratando de escapar, así que si el gatillo fue corrupción la copias fielmente.
Si estás pagando por mantener una caja corriendo como objetivo de reversión, mantenla caliente. Si de plano no puedes, la opción honesta es ningún standby y una reversión aceptada de varios días con pérdida alta, no una caja vacía que cuesta lo mismo que la buena opción.
Todo el asunto en una oración: la vieja instancia de RDS es una copia viva que ha estado tragándose cada write de Aurora desde el corte, así que revertir es detener Aurora, dejar que RDS termine los últimos writes, cambiar el endpoint.
Configúralo una vez, cuando promuevas Aurora:
SHOW MASTER STATUS).mysql.rds_set_external_master o la variante de auto-posición de GTID) y arranca la replicación.Replica_IO_Running: Yes y Replica_SQL_Running: Yes. Déjala corriendo. Alarma sobre que el hilo de SQL se detenga. No le escribas a RDS y no cambies el schema en Aurora mientras corre.Revierte, cuando lo decidas:
SET GLOBAL read_only = ON en Aurora, para que nada se cuele.Seconds_Behind_Source = 0 con los dos hilos en Yes. Esta es la compuerta. Significa que RDS ahora tiene cada write de la era Aurora. Si un hilo dice No, párate y escala, no hagas el corte.mysql.rds_stop_replication y luego mysql.rds_reset_external_master en RDS.SET GLOBAL read_only = OFF en RDS.No hay deshacer después del paso 4. Una vez que el enlace se corta y RDS es escribible, las dos bases de datos son independientes. Que es exactamente por lo que el paso 3 no es opcional: es la garantía de que te estás llevando todos los datos de vuelta contigo.