Módulo 6: Migrar datos sin downtime
El read-switch y apagar el dual-write
Descripción
Llegaste al corte final. El dual-write cubrió el frente, el backfill cubrió el histórico, el parallel-run comparó y la reconciliación llevó las discrepancias a cero —sostenido—. El almacén nuevo, por fin, dice la verdad en todas las filas y se mantiene cuadrado mientras el sistema opera. Solo ahora, con esa evidencia en la mano, se ejecuta el read-switch: mover las lecturas del viejo al nuevo. Y después, en un orden que importa, se apaga el dual-write y se retira el viejo.
El corte final no es un solo paso, son tres, y el orden entre ellos es lo que hace la lección:
- read-switch: las lecturas pasan a salir del nuevo. El viejo sigue caliente —el dual-write sigue encendido, escribiéndole—, porque es tu red de respaldo: si algo sale mal con el nuevo recién ascendido a fuente de verdad, puedes volver las lecturas al viejo en un instante.
- apagar el dual-write: cuando llevas suficiente tiempo leyendo del nuevo sin problemas, dejas de escribir al viejo. El viejo queda congelado.
- retirar el viejo: cuando nadie lo lee ni lo escribe, lo apagas y lo borras.
La regla dura de esta lección es el orden: read-switch primero (con el viejo aún vivo como red), apagar el dual-write después, retirar el viejo al final. Invertir los dos primeros pasos es un error concreto y peligroso —lo vas a ver ejecutado—: si apagaras el dual-write antes del read-switch, el viejo dejaría de recibir escrituras mientras las lecturas todavía salen de él, y estarías sirviendo datos rancios a los usuarios. El orden no es una preferencia estética; es lo que evita ese hueco.
Y hay un cierre que casi nadie recuerda, y que este módulo insiste en no olvidar: apagar el dual-write de verdad. Es fácil hacer el read-switch, ver que el nuevo sirve bien las lecturas, y dejar el dual-write encendido "por si acaso" —escribiendo para siempre a un almacén viejo que ya nadie lee—. Eso es la migración que nunca termina: dos sistemas mantenidos por la eternidad, el doble de costo y de complejidad, sin que nadie coseche el premio de haber migrado. El corte final termina cuando el viejo está retirado, no cuando las lecturas se movieron.
Conexión con el módulo. Esta lección consume el cero sostenido de la lección 6 y ejecuta el cierre. Es el "migrate + contract" de las lecturas que la lección 2 (expand-contract) anticipó: mover los lectores al nuevo (migrate) y retirar el viejo (contract), en ese orden. Fíjate en la frontera: el read-switch de este módulo mueve las lecturas de datos; en el módulo 3, el strangler movía las lecturas y escrituras de tráfico con un facade —la simetría es total, y el "retirar el legacy" de M3 es hermano del "retirar el viejo" de aquí—. Medir el progreso de todo esto (el burn-down de llamadas al viejo hasta cero, evitar la migración eterna) es el módulo 7, que toma este corte y lo convierte en una métrica.
Una analogía: cambiar del radar viejo al nuevo en la torre de control
En una torre de control aéreo, los controladores vigilan los aviones en una pantalla de radar. La torre va a reemplazar su radar viejo por uno nuevo. Los dos radares rastrean los mismos aviones —están instalados en paralelo, los dos encendidos y actualizándose en tiempo real—; la única pregunta es cuál pantalla miran los controladores para tomar decisiones. Hoy miran la del radar viejo.
El cambio se hace con un cuidado extremo, porque hay vidas de por medio, y el orden es sagrado:
-
Los controladores cambian a mirar la pantalla nueva (el read-switch). A partir de este momento, sus decisiones se basan en el radar nuevo. Pero —y esto es lo importante— el radar viejo sigue encendido y actualizándose. Nadie lo apaga. Es la red de respaldo: si la pantalla nueva parpadea, se congela o muestra algo raro, los controladores vuelven a mirar la vieja en un segundo, sin haber perdido el rastro de ningún avión.
-
Mantienen los dos radares vivos un buen rato (el dual-write sigue encendido). Durante días, la pantalla nueva es la que se mira, pero la vieja se mantiene alimentada, lista para retomar. Se observa: ¿la nueva rastrea todo bien?, ¿coincide con lo que mostraba la vieja?, ¿algún avión desaparece de una y no de la otra? Solo cuando hay plena confianza en la nueva se pasa al siguiente paso.
-
Dejan de mantener el radar viejo (apagar el dual-write). Ya nadie lo mira y la nueva demostró ser confiable; se deja de alimentarlo. El radar viejo queda congelado, con la última imagen que tenía.
-
Desmantelan el radar viejo (retirar). Se apaga del todo y se retira de la torre. Ya no ocupa espacio ni consume energía ni confunde a nadie.
Ahora, el error que el orden evita, y que en una torre de control sería catastrófico: ¿qué pasaría si dejaran de alimentar el radar viejo mientras los controladores todavía lo están mirando? La pantalla vieja se congelaría —seguiría mostrando la última posición de cada avión, pero ya no se actualizaría—. Los controladores estarían tomando decisiones sobre una imagen rancia: creerían que un avión sigue donde estaba hace cinco minutos, cuando en realidad ya se movió. Es exactamente el peligro del orden invertido en datos: apagar el dual-write (dejar de alimentar el viejo) antes del read-switch (antes de que los controladores miren la nueva) hace que las lecturas salgan de un almacén que ya no se actualiza —datos rancios servidos como si fueran frescos—.
Por eso el orden es: primero miras la pantalla nueva (read-switch), con la vieja aún viva como respaldo; solo después dejas de alimentar la vieja (apagar dual-write); y al final la desmantelas (retirar). Nunca dejas de alimentar la que todavía miras. Vamos a ejecutar exactamente esta secuencia.
Ejemplo trabajado: el corte final, paso a paso
Partimos con old_db y new_db ya sincronizados y el parallel-run verde. Hay dos perillas de escritura —write_to_old y write_to_new— y una de lectura —read_source—. El dual-write encendido significa que las dos perillas de escritura están en True. Vamos a ejecutar los seis pasos del corte y observar la columna read ssd (de dónde sale el precio que lee un usuario) y la columna dual_write en cada paso.
# --- Dos perillas de escritura + de donde se lee. dual_write = escribir a ambos.
write_to_old = True
write_to_new = True # dual_write ON: se escribe a los dos
read_source = "old" # las lecturas todavia salen del viejo
def to_new_model(old_row):
return {"id": old_row["sku"], "title": old_row["name"],
"price_usd": old_row["price_cents"] / 100, "in_stock": old_row["stock"] > 0}
old_db = {}
new_db = {}
def write_product(row):
landed = []
if write_to_old:
old_db[row["sku"]] = row; landed.append("old")
if write_to_new:
new_db[row["sku"]] = to_new_model(row); landed.append("new")
return "+".join(landed) if landed else "(nada)"
def read_price(sku): # lee del almacen ACTIVO
if read_source == "new":
return round(new_db[sku]["price_usd"] * 100)
return old_db[sku]["price_cents"]
def dual_state():
return "ON" if (write_to_old and write_to_new) else "OFF"
# --- Estado de partida: old y new ya sincronizados, parallel_run verde. ---
for sku, name, cents, stock in [("ssd-1tb", "SSD 1TB", 8499, 12),
("kbd-mech", "Mech Kbd", 7999, 5)]:
old_db[sku] = {"sku": sku, "name": name, "price_cents": cents, "stock": stock}
new_db[sku] = to_new_model(old_db[sku])
print("El corte final, paso a paso (old y new ya cuadran, parallel_run verde)\n")
print(f"{'paso':<24}{'dual_write':<11}{'reads':<7}{'read ssd':>9} observacion")
print("-" * 82)
# PASO 1: parallel_run verde -> se autoriza el switch (reads aun del old).
print(f"{'1 parallel_run verde':<24}{dual_state():<11}{read_source:<7}{read_price('ssd-1tb'):>9} discrepancias=0 -> autoriza")
# PASO 2: read_switch. Las lecturas ahora salen del new. dual_write SIGUE ON.
read_source = "new"
print(f"{'2 read_switch':<24}{dual_state():<11}{read_source:<7}{read_price('ssd-1tb'):>9} lecturas movidas al new")
# PASO 3: llega un write. Con dual_write ON cae en AMBOS (old sigue caliente).
landed = write_product({"sku": "ssd-1tb", "name": "SSD 1TB", "price_cents": 8299, "stock": 12})
print(f"{'3 write ssd 82.99':<24}{dual_state():<11}{read_source:<7}{read_price('ssd-1tb'):>9} cayo en {landed} (old de respaldo)")
# PASO 4: apagar dual_write. Las escrituras dejan de ir al old.
write_to_old = False
print(f"{'4 dual_write OFF':<24}{dual_state():<11}{read_source:<7}{read_price('ssd-1tb'):>9} ya no se escribe al old")
# PASO 5: llega otro write. Con dual_write OFF cae SOLO en el new; old congelado.
landed = write_product({"sku": "kbd-mech", "name": "Mech Kbd", "price_cents": 7499, "stock": 5})
print(f"{'5 write kbd 74.99':<24}{dual_state():<11}{read_source:<7}{read_price('ssd-1tb'):>9} cayo en {landed} (old NO)")
# PASO 6: retirar el old. Ya nadie lo lee ni lo escribe.
old_kbd_stale = old_db["kbd-mech"]["price_cents"] # quedo congelado en 79.99
new_kbd_fresh = round(new_db["kbd-mech"]["price_usd"] * 100)
old_db = None
print(f"{'6 retirar old':<24}{dual_state():<11}{read_source:<7}{read_price('ssd-1tb'):>9} old_db eliminado")
print("-" * 82)
print("\n Por que el ORDEN importa. En el paso 5 el old NO recibio el update de kbd:")
print(f" old['kbd-mech'] quedo en {old_kbd_stale} (79.99, STALE); new tiene {new_kbd_fresh} (74.99).")
print(" Como ya leemos del new (paso 2), ese old rancio es inofensivo: nadie lo lee.")
print(" Si hubieras apagado dual_write ANTES del read_switch, esa lectura del old")
print(" habria devuelto 79.99 rancio a un usuario. Por eso: read_switch PRIMERO,")
print(" apagar dual_write DESPUES, retirar el old AL FINAL.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
El corte final, paso a paso (old y new ya cuadran, parallel_run verde)
paso dual_write reads read ssd observacion
----------------------------------------------------------------------------------
1 parallel_run verde ON old 8499 discrepancias=0 -> autoriza
2 read_switch ON new 8499 lecturas movidas al new
3 write ssd 82.99 ON new 8299 cayo en old+new (old de respaldo)
4 dual_write OFF OFF new 8299 ya no se escribe al old
5 write kbd 74.99 OFF new 8299 cayo en new (old NO)
6 retirar old OFF new 8299 old_db eliminado
----------------------------------------------------------------------------------
Por que el ORDEN importa. En el paso 5 el old NO recibio el update de kbd:
old['kbd-mech'] quedo en 7999 (79.99, STALE); new tiene 7499 (74.99).
Como ya leemos del new (paso 2), ese old rancio es inofensivo: nadie lo lee.
Si hubieras apagado dual_write ANTES del read_switch, esa lectura del old
habria devuelto 79.99 rancio a un usuario. Por eso: read_switch PRIMERO,
apagar dual_write DESPUES, retirar el old AL FINAL.
Lee la tabla paso por paso, siguiendo las columnas reads (de dónde se lee) y dual_write (si aún se escribe al viejo).
Paso 1 — El parallel-run está verde (discrepancias = 0). Las lecturas todavía salen del viejo (reads = old), el dual-write está ON. El precio leído de ssd-1tb es 8499. Este es el estado que la lección 6 nos entregó: nuevo validado, listo, pero aún no en uso para lecturas. El cero verde autoriza el switch.
Paso 2 — El read_switch. reads pasa a new: las lecturas ahora salen del almacén nuevo. El precio leído sigue siendo 8499 —idéntico al del viejo, porque están sincronizados; por eso el switch es transparente para el usuario—. Fíjate: el dual_write sigue en ON. No apagamos nada del viejo; solo cambiamos de dónde leemos. El viejo queda caliente, como el radar viejo que sigue encendido: es la red de respaldo. Si algo saliera mal con el nuevo ahora, un solo cambio (read_source = "old") devuelve las lecturas al viejo, que está al día.
Paso 3 — Llega una escritura mientras el read-switch ya ocurrió pero el dual-write sigue ON: ssd-1tb baja a 82.99. Cayó en old+new (los dos): el usuario la lee del nuevo (82.99, actualizado), y el viejo también la recibió, manteniéndose caliente y al día como respaldo. Esta es la fase de coexistencia post-switch: lees del nuevo, pero el viejo sigue espejando por si acaso.
Paso 4 — Apagar el dual_write. write_to_old pasa a False; ahora dual_write marca OFF. A partir de aquí, las escrituras dejan de ir al viejo. Lo hacemos ahora, no antes, porque ya llevamos tiempo leyendo del nuevo sin problemas y confiamos en él. El viejo va a empezar a congelarse.
Paso 5 — Llega otra escritura con el dual-write ya OFF: kbd-mech baja a 74.99. Cayó solo en new —"(old NO)"—. El viejo no la recibió: old['kbd-mech'] se quedó en su valor anterior (7999). El viejo ya está rancio para kbd-mech. Pero fíjate: eso no le hace daño a nadie, porque las lecturas salen del nuevo (paso 2), que sí tiene el 74.99. El viejo rancio es inofensivo porque ya nadie lo lee.
Paso 6 — Retirar el viejo. old_db = None: lo eliminamos. Ya nadie lo lee (reads = new desde el paso 2) ni lo escribe (dual_write = OFF desde el paso 4), así que borrarlo no afecta nada. El precio de ssd-1tb se sigue leyendo bien (8299) del nuevo. La migración terminó: el viejo no existe.
Y el bloque final ejecuta la lección del orden con números. En el paso 5, el viejo se quedó rancio para kbd-mech (7999 en vez de 7499). Como leemos del nuevo, da igual —nadie ve ese 7999—. Pero imagina el orden invertido: si hubieras apagado el dual-write (paso 4) antes del read-switch (paso 2), las lecturas seguirían saliendo del viejo, y esa lectura de kbd-mech devolvería 7999 rancio a un usuario real, cuando el precio verdadero es 7499. El viejo dejó de actualizarse mientras todavía era la fuente de las lecturas: la pantalla del radar congelada. Ese es el hueco que el orden correcto —read-switch primero, apagar dual-write después— elimina.
Profundización: el orden, la red de respaldo, y el cierre que no se olvida
Por qué el viejo sigue caliente después del read-switch. Podría parecer que, una vez que las lecturas salen del nuevo, el viejo ya no sirve para nada y se puede apagar de inmediato. Pero el read-switch es el momento de máximo riesgo de toda la migración: es la primera vez que el nuevo es la fuente de verdad para lecturas reales, en producción, con tráfico real. Si hay un problema que el parallel-run no atrapó —una carga que el nuevo no aguanta, un caso raro—, quieres poder volver atrás en segundos. Mantener el dual-write encendido después del read-switch mantiene al viejo al día, así que el rollback es trivial: cambias read_source de vuelta a old y el viejo, que nunca dejó de actualizarse, retoma sin haber perdido nada. Apagar el dual-write demasiado pronto quema esa red: en cuanto el viejo se congela, ya no es un rollback válido (estaría rancio). Por eso la secuencia deja un margen —lees del nuevo con el viejo aún vivo— antes de cortar el dual-write.
El orden completo, y la simetría de las escrituras. Fíjate en un detalle sutil del ejemplo: durante la transición, la escritura al viejo iba primero (era la fuente de verdad, lección 3). Después del read-switch, el nuevo es la fuente de verdad, y el que no puede fallar es él. El "apagar el dual-write" formaliza esa transición: se deja de escribir al viejo porque ya no es la verdad de nadie. El corte de lecturas y el de escrituras están escalonados a propósito:
reads writes
───── ──────────────
antes del corte old old (verdad) + new (copia) <- dual_write ON
read_switch new old + new <- dual_write ON, old caliente
dual_write OFF new new <- old congelado, red retirada
retirar new new <- old borrado
Las lecturas cambian primero (read_switch), las escrituras al viejo se cortan después (dual_write OFF), y el viejo se borra al final. Cada fila de esa tabla es reversible hasta la penúltima: mientras el viejo esté caliente (dual_write ON), puedes volver las lecturas al viejo. En cuanto apagas el dual-write, el viejo empieza a envejecer y el rollback deja de ser gratis —por eso ese paso solo se da con confianza ganada—.
Retirar significa borrar, no "dejarlo por si acaso". El paso 6 hace old_db = None: elimina el viejo. En un sistema real, retirar significa borrar la tabla, apagar la base, quitar el código que la tocaba. No es "dejarla ahí congelada indefinidamente". Un almacén viejo que se queda "por si acaso" tiene costos reales: ocupa espacio, alguien tiene que respaldarlo y mantenerlo, confunde a quien lea el código ("¿esta tabla se usa o no?"), y es una tentación permanente de que algún componente vuelva a leerla por error. El premio de la migración —un sistema más simple, con un almacén en vez de dos— solo se cobra cuando el viejo desaparece de verdad. Igual que el strangler del módulo 3 no termina hasta retirar el legacy, la migración de datos no termina hasta borrar el viejo.
El error de olvidar apagar el dual-write. Es el fracaso silencioso más común del corte. El equipo hace el read-switch, ve que el nuevo sirve bien, y... deja el dual-write encendido para siempre. El sistema queda escribiendo cada dato a dos almacenes indefinidamente, cuando ya solo lee de uno. Los costos se acumulan: cada escritura paga el doble (dos almacenes), el viejo sigue creciendo sin que nadie lo lea, y la complejidad de "mantener los dos sincronizados" se vuelve permanente en vez de temporal. Peor: nadie decide dejarlo así; simplemente el corte "se dio por terminado" en el read-switch, y los pasos 4 a 6 nunca se ejecutaron. La regla es clara: el corte final tiene cuatro pasos (read-switch, apagar dual-write, esperar, retirar), y termina en el retiro. Un corte que se detiene en el read-switch es una migración a medio hacer disfrazada de completa. El módulo 7 mide justamente esto —que el viejo de verdad se apague— para que no se quede prendido por inercia.
Errores comunes
Apagar el dual-write antes del read-switch (el orden invertido). Qué pasa: el equipo, pensando "ya validamos el nuevo, dejemos de escribir al viejo", apaga el dual-write mientras las lecturas todavía salen del viejo —que empieza a congelarse y a servir datos rancios—. Por qué pasa: los dos pasos (apagar dual-write, mover lecturas) se ven como "terminar de usar el viejo", y el orden entre ellos parece intercambiable. Cómo detectarlo: aparecen lecturas rancias —usuarios que ven un precio o un stock viejo— justo después de apagar el dual-write, y el viejo (aún fuente de las lecturas) no refleja las escrituras recientes. Cómo corregirlo: read-switch primero, apagar dual-write después, sin excepción. Mientras las lecturas salgan del viejo, el viejo tiene que seguir recibiendo escrituras (dual-write ON), o servirá datos congelados. Solo cuando las lecturas ya salen del nuevo es seguro dejar de alimentar el viejo. Nunca dejes de alimentar el almacén que todavía lees —el radar que todavía miras—.
Apagar el dual-write demasiado pronto tras el read-switch (quemar la red de respaldo). Qué pasa: el equipo hace el read-switch y, minutos después, apaga el dual-write —de modo que cuando aparece un problema con el nuevo, ya no puede volver al viejo, que quedó rancio—. Por qué pasa: llegar al final se siente cerca, y mantener el dual-write "un rato más" parece desperdicio. Cómo detectarlo: entre el read-switch y el apagado del dual-write pasó muy poco tiempo (minutos, no días); cuando surge un incidente en el nuevo, el rollback al viejo ya no es viable porque el viejo se congeló. Cómo corregirlo: mantén el dual-write encendido un periodo prudente después del read-switch —el suficiente para confiar en que el nuevo aguanta el tráfico real—. Durante ese periodo el viejo se mantiene al día y el rollback es gratis: un cambio de read_source y estás de vuelta. Apagar el dual-write es irreversible en la práctica (el viejo envejece de inmediato), así que se hace solo con confianza ganada, no por prisa.
Terminar en el read-switch y no retirar el viejo. Qué pasa: las lecturas se movieron al nuevo, todo funciona, y el proyecto se declara terminado —el dual-write sigue encendido, el viejo sigue ahí, para siempre—. Por qué pasa: el read-switch es el momento visible y satisfactorio (¡el nuevo ya sirve!); apagar el dual-write y borrar el viejo son trabajo sin recompensa visible y con un poco de miedo. Cómo detectarlo: meses después del read-switch, el sistema todavía escribe a los dos almacenes y el viejo sigue desplegado, aunque nadie lo lea. Cómo corregirlo: el corte termina en el retiro, no en el read-switch. Define los pasos 4-6 (apagar dual-write, esperar, borrar el viejo) como parte del proyecto desde el inicio, con una fecha. El premio de la migración —un solo almacén, la mitad de la complejidad, el fin del doble costo de escritura— solo se cobra cuando el viejo desaparece. Dejarlo "por si acaso" es pagar el costo de la migración sin cobrar su beneficio: lo peor de los dos mundos. El módulo 7 mide este cierre para que no se quede a medias.
Ejercicios
Ejercicio 1 — Reconstruye el peligro del orden invertido. En el ejemplo, el paso 5 escribió kbd-mech a 74.99 solo en el nuevo, dejando el viejo en 79.99. (a) ¿Por qué ese viejo rancio fue inofensivo en la secuencia correcta? (b) Si se hubiera apagado el dual-write antes del read-switch, ¿qué habría leído un usuario que consultara kbd-mech? (c) ¿Qué principio general resume esto?
Ver solución
(a) Fue inofensivo porque, para el paso 5, las lecturas ya salían del nuevo (el read-switch ocurrió en el paso 2). El viejo tenía kbd-mech rancio en 79.99, pero nadie lee del viejo, así que ese valor congelado no llega a ningún usuario. Un almacén rancio solo hace daño si alguien lo lee; una vez movidas las lecturas al nuevo, el viejo puede envejecer tranquilo.
(b) Habría leído 79.99, el valor rancio. Con el orden invertido, las lecturas seguirían saliendo del viejo, pero el dual-write ya estaría apagado, así que la escritura de kbd-mech a 74.99 habría caído solo en el nuevo —que nadie lee— y el viejo se habría quedado en 79.99. El usuario vería un precio desactualizado como si fuera el actual: un dato rancio servido como fresco.
(c) El principio: nunca dejes de alimentar el almacén del que todavía lees. Mientras las lecturas salgan del viejo, el viejo tiene que seguir recibiendo escrituras (dual-write ON). Solo cuando las lecturas ya salen del nuevo es seguro dejar de alimentar el viejo. Por eso el orden es read-switch primero (mover las lecturas), apagar dual-write después (dejar de alimentar el que ya nadie lee).
Ejercicio 2 — La ventana de rollback. Después del read-switch, el equipo mantiene el dual-write encendido tres días antes de apagarlo. (a) ¿Para qué sirve esa ventana de tres días? (b) Si en el día 2 el nuevo empieza a dar problemas de rendimiento, ¿cómo se vuelve al viejo, y por qué es gratis? (c) ¿Por qué apagar el dual-write "convierte en irreversible" el corte, en la práctica?
Ver solución
(a) Sirve como ventana de rollback: es un periodo en que las lecturas ya salen del nuevo (para probarlo con tráfico real) pero el viejo se mantiene al día (dual-write ON), listo para retomar si el nuevo falla. Es la fase de máxima cautela: confías lo suficiente en el nuevo para leer de él, pero no tanto como para quemar la red de respaldo. Los tres días dan tiempo a que aparezcan problemas que el parallel-run no atrapó (carga, casos raros, picos de tráfico).
(b) Se vuelve al viejo con un solo cambio: read_source = "old". Como el dual-write estuvo encendido todo el tiempo, el viejo recibió todas las escrituras de esos dos días y está completamente al día —no perdió nada—. Por eso el rollback es gratis: no hay que recuperar datos ni resincronizar; el viejo ya tiene todo, solo hay que volver a apuntarle las lecturas. Es instantáneo y sin pérdida.
(c) Porque en el instante en que apagas el dual-write, el viejo deja de recibir escrituras y empieza a envejecer. Cada escritura nueva cae solo en el nuevo; el viejo se va quedando cada vez más atrás. A los pocos minutos, volver las lecturas al viejo ya significaría servir datos rancios (le faltarían todas las escrituras posteriores al apagado). En teoría podrías re-sincronizar el viejo, pero eso es tan caro como una nueva migración. En la práctica, apagar el dual-write quema la red de respaldo: por eso se hace solo con confianza ganada, al final de la ventana de rollback, no al principio.
Ejercicio 3 — La migración que no termina. Un equipo hizo el read-switch hace seis meses. El nuevo sirve todas las lecturas sin problemas. Pero el dual-write sigue encendido y el viejo sigue desplegado. (a) ¿Qué costos está pagando el equipo por no haber terminado el corte? (b) ¿Qué pasos les faltan? (c) ¿Por qué es "lo peor de los dos mundos"?
Ver solución
(a) Está pagando: el doble costo de escritura (cada escritura va a dos almacenes, aunque solo se lea de uno); el mantenimiento del viejo (respaldos, monitoreo, espacio, parches de un almacén que nadie lee); complejidad y confusión (el código mantiene los dos sincronizados sin razón, y quien lo lea no sabe si el viejo se usa); y el riesgo de que algún componente vuelva a leer el viejo por error, o de que la divergencia entre ambos cause bugs. Todo eso para sostener un almacén que ya no aporta nada.
(b) Les faltan los pasos 4-6 del corte: apagar el dual-write (dejar de escribir al viejo, una vez confirmado que el nuevo es confiable), esperar el periodo prudente, y retirar el viejo (borrar la tabla, apagar la base, quitar el código que la tocaba). El corte se detuvo en el read-switch (paso 2) y nunca llegó al retiro (paso 6).
(c) Porque el equipo pagó todo el costo de la migración pero no cobró su beneficio. Hicieron el trabajo duro (dual-write, backfill, parallel-run, reconciliación, read-switch) —con todo su costo y riesgo— pero al no retirar el viejo, no obtienen el premio que justificaba ese trabajo: un sistema más simple, con un solo almacén y sin el doble costo de escritura. Se quedaron con la complejidad de dos almacenes (como antes de migrar) más la maquinaria de sincronización (que no existía antes). Es lo peor de los dos mundos: la complejidad del estado viejo y la del intermedio, a la vez, para siempre. Terminar el corte —retirar el viejo— es lo único que convierte el costo pagado en beneficio cobrado.
Resumen y siguiente paso
En esta lección ejecutaste el corte final de la migración de datos: el read-switch y el apagado del dual-write. Viste, con la torre de control que cambia del radar viejo al nuevo, que el orden es sagrado —los controladores miran la pantalla nueva primero (read-switch), con el radar viejo aún encendido como respaldo; solo después dejan de alimentarlo (apagar dual-write); y al final lo desmantelan (retirar)—, y que dejar de alimentar el radar que todavía miras congela la imagen y sirve datos rancios. Lo ejecutaste paso a paso: read-switch con el viejo caliente, una escritura cayendo en ambos como respaldo, el apagado del dual-write, y el retiro —y viste con números por qué el orden invertido serviría un kbd-mech rancio a 79.99 en vez del 74.99 real—. Y guardaste el cierre que casi nadie recuerda: apagar el dual-write de verdad y borrar el viejo, porque una migración que termina en el read-switch pagó el costo sin cobrar el premio.
Antes de avanzar deberías poder: ordenar los pasos del corte (read-switch → apagar dual-write → retirar) y justificar cada orden; explicar por qué el viejo sigue caliente después del read-switch (la ventana de rollback) y por qué apagar el dual-write quema esa red; describir el peligro del orden invertido (lecturas rancias del viejo congelado); y reconocer la migración que no termina (dual-write eterno, viejo sin retirar) como el fracaso silencioso del corte.
La lección 8 integra todo el módulo en un solo capstone ejecutado: migrar los datos del catalog de Mercado de old_db a new_db de punta a punta, como un diario de migración día por día. Una clase DataMigration que enciende el dual-write, hace el backfill (con un bug real que salta los productos sin stock), corre el parallel-run que atrapa el hueco, reconcilia a cero, hace el read-switch, apaga el dual-write y retira el viejo —los siete días de una migración sin downtime, con el sistema vivo en cada paso—. Entrega: el plan, el código ejecutado, y la justificación de por qué la migración incremental y verificada venció al "copiar todo en una ventana de mantenimiento".
Recursos
- Martin Fowler, "ParallelChange" — martinfowler.com/bliki/ParallelChange.html. El read-switch y el retiro del viejo son el "migrate" y el "contract" de las lecturas: mover a los lectores al nuevo y quitar el viejo, en ese orden. La entrada que da el marco al corte final. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 4, "Decomposing the Database" — el corte de lecturas al almacén nuevo, mantener el viejo como respaldo durante la transición, y retirarlo cuando ya no se usa, dentro del proceso completo de separar la base del monolito. En inglés.
- Stripe Engineering, "Online migrations at scale" — stripe.com/blog/online-migrations. El relato de producción termina exactamente aquí: mover las lecturas a la tabla nueva, mantener la vieja un tiempo como red, y finalmente retirarla —con la advertencia de completar el retiro y no dejar la escritura doble encendida—. En inglés.
- Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. La simetría con el módulo 3: retirar el legacy cuando el tráfico llegó al 100% es hermano de retirar el viejo cuando las lecturas se movieron al nuevo. El mismo "no dejes lo viejo prendido por si acaso". En inglés.