Saltar al contenido
Volver al archivo
Ingeniería5 min de lecturaPaso 8 de 9

Migración de esquema sin downtime

Por qué renombrar una columna rompe producción, y el patrón de cuatro pasos que convierte una migración en una tarea de martes por la tarde.

· Gabriel Dias
migracionexpand-contractpostgresdeploy

Existe un principio que explica prácticamente todo incidente de migración de base de datos, y cabe en una frase:

Durante el deploy, las dos versiones del código corren al mismo tiempo contra la misma base.

Siempre. Rolling update, blue-green, canary: en algún momento existe código viejo y código nuevo vivos simultáneamente, y los dos hablan con el mismo esquema.

Por lo tanto, toda alteración de esquema tiene que ser compatible con la versión anterior del código. Sin excepción.

Qué prohíbe eso

Renombrar una columna. Rompe la versión antigua en el instante en que corre.

Eliminar una columna que todavía se usa. El mismo problema.

Añadir una columna NOT NULL sin valor por defecto. La versión antigua no sabe rellenarla, y su INSERT falla.

Cambiar el tipo de una columna de forma incompatible. La versión antigua escribe un valor que ya no cabe.

Añadir una restricción que el dato existente viola. Falla en el momento o impide escritura legítima.

El patrón expand-contract

También llamado parallel change. Cuatro pasos, y cada uno es un deploy independiente y reversible.

  1. 1ExpandirColumna nueva, aceptando nulo. Ningún código la usa todavía.
  2. 2Escribir en las dosEl código nuevo graba en la vieja y en la nueva.
  3. 3Migrar el históricoLotes pequeños, con pausa, rellenan el pasado.
  4. 4ContraerLee solo de la nueva. La vieja se va en un deploy siguiente.
Cada caja es un deploy. La columna vieja solo desaparece en el último.

Paso 1: Expandir

Añade la estructura nueva sin tocar la vieja. Columna nueva, aceptando nulo, sin restricción. Nada se rompe, porque ningún código la usa todavía.

Paso 2: Escribir en las dos

La versión nueva del código pasa a grabar en la columna vieja y en la nueva. Deploy. Ahora todo dato nuevo existe en los dos sitios, y la versión antigua sigue funcionando leyendo la vieja.

Paso 3: Migrar el histórico

Un proceso por lotes rellena la columna nueva para los registros antiguos. En lotes pequeños, con pausa entre ellos, para no bloquear la tabla ni inundar el log de replicación.

Paso 4: Contraer

Cuando todo esté relleno y ninguna versión antigua siga en el aire, el código pasa a leer solo de la nueva. En un deploy siguiente, no en el mismo, eliminas la columna vieja.

Cuatro deploys en vez de uno. Parece burocrático. Es lo que convierte una migración en una tarea de martes por la tarde, en vez de una ventana de madrugada con el equipo entero despierto.

Y cada paso es reversible por sí solo, que es la propiedad que más importa cuando algo sale mal a las once de la noche.

Qué sale mal con volumen

Una migración que corre en milisegundos en una tabla vacía puede bloquear la tabla veinte minutos en producción, con diez millones de filas.

Los casos clásicos, en Postgres:

CREATE INDEX bloquea la escritura. Usa CREATE INDEX CONCURRENTLY. Es más lento, no bloquea, y puede fallar dejando un índice inválido que tienes que eliminar manualmente. Conoce ese comportamiento antes de la noche del deploy.

ALTER TABLE que reescribe la tabla. Cambiar el tipo de una columna normalmente lo reescribe todo, con lock exclusivo. En versiones modernas, añadir una columna con valor por defecto ya no reescribe, pero comprueba tu versión.

Añadir una clave foránea valida la tabla entera bajo lock. La salida es añadirla como NOT VALID y después ejecutar VALIDATE CONSTRAINT, que usa un lock más débil.

Un UPDATE masivo genera un volumen enorme de WAL, retrasa réplicas y puede llenar el disco. Siempre por lotes, con pausa, monitorizando el lag.

El lock que espera. En Postgres, un ALTER que necesita lock exclusivo entra en la cola, y mientras espera, bloquea todas las queries que llegan después. Una transacción larga reteniendo un lock ligero puede, indirectamente, bloquear la tabla entera. Usa un lock_timeout corto e inténtalo de nuevo, en vez de dejar el comando colgado.

Prueba la migración con volumen

Esto es lo que casi nadie hace y lo que más evita sorpresas.

Ejecuta la migración en una copia con volumen parecido al de producción, aunque sea una vez, antes del deploy grande. Cronométrala. Si tarda veinte minutos, acabas de descubrir que necesitas otro enfoque, y lo descubriste un martes, no de madrugada.

Y prueba también la migración hacia atrás. Muchas herramientas generan el script de reversión automáticamente y nadie lo ejecuta nunca. El día en que lo necesites, no funciona.

Migración de datos contra migración de esquema

Vale la pena separar las dos.

El cambio de esquema es rápido o lento dependiendo del comando, y es reversible en la mayoría de los casos.

El cambio de datos (recalcular un campo, normalizar valores, reprocesar histórico) siempre es lento y frecuentemente irreversible. Trátalo como un proceso por lotes con estado: registra lo que ya se procesó, permite retomar, y hazlo en lotes idempotentes.

El caso del renombrado de tabla

El mismo principio, un nivel más arriba. Quieres renombrar usuarios como cuentas.

Expandir: crea la tabla nueva y un trigger que replica las escrituras de una a la otra. O una vista con el nombre antiguo apuntando a la nueva, si tu base permite escritura en vistas.

Migrar: copia el histórico por lotes.

Contraer: mueve las lecturas, después las escrituras, después elimina la antigua.

La misma coreografía, más piezas. Y la pregunta que vale la pena hacerse antes: ¿ese renombrado entrega valor suficiente para justificar cuatro deploys? A veces la respuesta honesta es no.

Resumiendo

Tres reglas que evitan la mayoría de los incidentes:

  1. Nunca renombres. Añade, migra, elimina.
  2. Toda migración es compatible con la versión anterior del código.
  3. Prueba con volumen antes.

Y una cuarta, que es más cultural: la migración no es un apéndice de la tarea. Es parte del diseño, y merece el mismo cuidado que el código.

Lee esto después

Háblame

¿Dudas sobre el artículo? Escríbeme por WhatsApp

Sin formulario y sin lista de correo. Si no estás de acuerdo con algo que escribí, o quieres contarme cómo lo resolviste, la conversación es directa conmigo.

Abrir conversación