Migraciones de base de datos sin downtime

Una migración que toma un bloqueo exclusivo en el momento del deploy es una caída programada con distintivo de deploy. Las migraciones Postgres sin downtime usan expand/contract: agregar esquema antes de eliminarlo, hacer backfill antes de aplicar restricciones y nunca bloquear lecturas en el hot path.

Ingeniería7 min de lectura
PostgresMigraciones de base de datosZero downtimeExpand contractDevOps
Compartir

Deploy el viernes. Corre la migración. ALTER TABLE orders ADD COLUMN status_v2 VARCHAR(20) NOT NULL DEFAULT 'pending' — salvo que la tabla tiene cuarenta millones de filas y Postgres reescribe el heap bajo un bloqueo ACCESS EXCLUSIVE. El checkout se detiene once minutos. El equipo lo llama deploy, no caída. Los clientes lo llaman de otra forma. La migración Postgres sin downtime no es una feature de herramienta: es una disciplina — expandir el esquema junto al viejo, migrar datos de forma asíncrona, contraer solo tras la prueba y tratar la duración del bloqueo como bloqueante de release.

La producción no se pausa por cambios de esquema. Las aplicaciones se despliegan de forma continua. Versiones viejas y nuevas del código corren en simultáneo durante los deploys. Las migraciones deben funcionar mientras ambas versiones están activas — compatibles hacia atrás en la subida, compatibles hacia adelante hasta que termine la fase de contract.

Los modos de bloqueo deciden si el deploy es migración o caída

El DDL de Postgres adquiere niveles de bloqueo que impiden acceso concurrente:

Lock levelBlocks reads?Blocks writes?Typical DDL
ACCESS SHARENoNoinicio de CREATE INDEX CONCURRENTLY
ROW EXCLUSIVENoNoINSERT, UPDATE, DELETE
SHARE UPDATE EXCLUSIVENoBloqueo breve de escrituraVACUUM, CREATE INDEX CONCURRENTLY
ACCESS EXCLUSIVEla mayoría de formas de ALTER TABLE, DROP

Cualquier migración que requiera ACCESS EXCLUSIVE en una tabla caliente en hora pico es un plan de caída. Medir la espera de bloqueo en staging con conteos de filas al estilo producción — no tablas vacías.

Una columna NOT NULL agregada en una transacción sobre una tabla grande es un bloqueo, no una estrategia de migración.

Expand/contract es el patrón de migración sin downtime

Tres fases, a menudo en varios deploys:

Expand. Agregar esquema nuevo sin romper el código viejo.

  • Agregar columna nullable — el código viejo la ignora.
  • Agregar tabla nueva junto a la vieja — dual-write o sync asíncrono.
  • Agregar índice nuevo CONCURRENTLY — sin bloqueo de tabla para lecturas/escrituras.
  • Agregar trigger o columna generada para el período de transición.

Migrate. Mover datos y tráfico.

  • Hacer backfill de la columna nueva por lotes — UPDATE ... WHERE id BETWEEN ? AND ? LIMIT 1000 con pausa entre lotes.
  • Dual-write: la aplicación escribe en columnas/tablas viejas y nuevas.
  • Cambiar lecturas al camino nuevo vía feature flag cuando el backfill termine.
  • Validar con consultas de reconciliación — conteos, checksums, diffs por muestra.

Contract. Quitar esquema viejo solo tras la prueba.

  • Eliminar columna vieja — solo cuando ningún código la lee o escribe.
  • Eliminar tabla vieja — solo tras tráfico en cero por un período sostenido.
  • Agregar restricción NOT NULL — solo tras confirmar con backfill que no hay nulls.
Deploy 1 (expand):  ADD COLUMN email_normalized TEXT NULL
Deploy 2 (migrate): backfill job + app dual-write
Deploy 3 (migrate): app reads email_normalized; flag on
Deploy 4 (contract): DROP COLUMN email_legacy — after N days zero traffic

El artículo sobre estrategia de rollback para deploys cubre qué pasa cuando fallan deploys en fase migrate — el rollback en fase expand suele ser revertir la app mientras el esquema queda; el rollback en fase contract puede ser imposible sin un forward-fix.

Operaciones de alto riesgo y sus alternativas seguras

Risky migrationWhy it locks or blocksSafe pattern
ADD COLUMN ... NOT NULL en tabla grandeReescribe la tabla, bloqueo exclusivoAgregar nullable → backfill → SET NOT NULL con check constraint por pasos
CREATE INDEX (no concurrent)Bloqueo SHARE impide escriturasCREATE INDEX CONCURRENTLY
Cambio de tipo de columnaReescritura, bloqueo exclusivoColumna nueva + backfill + swap de rename en contract
ADD FOREIGN KEYValida todas las filas, bloqueo fuerteRestricción NOT VALID primero → VALIDATE CONSTRAINT por separado
Reescritura de tabla (ALTER ... TYPE)Bloqueo totalTabla nueva + copia + swap de vistas o routing
Renombrar columnaRompe el código viejo al instanteAgregar nueva, dual-write, deprecar nombre viejo en código primero

CREATE INDEX CONCURRENTLY no puede ejecutarse dentro de un bloque de transacción. Las herramientas de migración que envuelven todo en transacciones necesitan manejo especial para creación concurrente de índices.

Backfill sin derretir la base de datos

Los backfills por lotes protegen la producción:

-- Repeat until zero rows affected
UPDATE orders
SET status_v2 = map_status(status_legacy)
WHERE id IN (
  SELECT id FROM orders
  WHERE status_v2 IS NULL
  LIMIT 5000
  FOR UPDATE SKIP LOCKED
);

Patrones:

  • Tamaño de lote ajustado al I/O — monitorear replication lag en réplicas durante el backfill.
  • SKIP LOCKED — evitar pelear con escrituras en vivo por las mismas filas.
  • Programación fuera de pico — el backfill es carga de producción.
  • Seguimiento de progreso — filas restantes, ETA, interruptor de pausa para incidentes.
  • Backfill idempotente — seguro de reiniciar; guarda WHERE new_col IS NULL.

Los jobs de backfill compiten con el tráfico en vivo por I/O y slots del connection pool. Coordinar con connection pooling en serverless — backfills de larga duración por el mismo pool que los request handlers ahogan las consultas de la aplicación.

Matriz de compatibilidad de aplicación por deploy

Cada deploy de migración necesita una matriz escrita:

Deploy phaseOld app (N)New app (N+1)Schema state
ExpandWorksWorksExisten columnas nuevas nullable
Dual-writeEscribe solo col viejaEscribe ambasAmbas columnas se pueblan con el tiempo
Read switchLee viejaLee nuevaBackfill completo
ContractDebe estar fueraWorksColumna vieja eliminada

Si N y N+1 deben correr en simultáneo — siempre cierto en rolling deploys — los cambios de esquema no deben romper N. La fase contract exige prueba de que N ya no está en ejecución.

¿Cómo deben ejecutar los equipos migraciones Postgres sin downtime?

Estas prácticas separan la disciplina expand/contract de los deploys de bloquear-y-rezar.

¿Pueden ser todas las migraciones sin downtime?

No. Algunos cambios — dividir una tabla muy contenciosa, reescrituras fundamentales del modelo — requieren ventanas de mantenimiento o modo solo lectura. El objetivo es sin downtime por defecto, ventana de mantenimiento por decisión explícita con aviso a stakeholders — no por accidente cuando un bloqueo golpea producción.

¿Cuántos deploys debe tomar expand/contract?

Los que hagan falta por seguridad. Secuencias de cuatro deploys son normales. Apurar contract antes de que termine el backfill crea corrupción de datos, no velocidad.

¿Cuándo es aceptable una ventana de mantenimiento?

Períodos de bajo tráfico para operaciones que genuinamente no pueden hacerse concurrentes — raro con el tooling de Postgres. Documentar RTO, avisar a clientes, ensayar. No usar ventanas de mantenimiento para evitar aprender CREATE INDEX CONCURRENTLY.

Una visión opuesta

Un argumento frecuente sostiene que las migraciones sin downtime añaden complejidad desproporcionada al riesgo de caída — que equipos pequeños deberían aceptar bloqueos breves y enviar más rápido.

Bloqueos breves en tablas pequeñas están bien. Bloqueos breves en tablas calientes de millones de filas no son breves. La complejidad de expand/contract se carga al inicio y queda documentada; la complejidad de caídas no planificadas de once minutos es reactiva y visible para el cliente. Los equipos que miden duración de bloqueo en staging rara vez eligen el bloqueo.

Key takeaways

  • Las migraciones sin downtime usan expand/contract en varios deploys — nunca DDL big-bang en tablas calientes.
  • Los bloqueos ACCESS EXCLUSIVE en tablas de producción son planes de caída — medir en staging con conteos reales de filas.
  • Agregar columnas nullable primero; backfill por lotes; forzar NOT NULL solo tras completar el backfill.
  • CREATE INDEX CONCURRENTLY y foreign keys NOT VALID evitan bloqueos de validación.
  • Escribir matriz de compatibilidad de app por deploy — código viejo y nuevo corren en simultáneo en rolling deploys.
  • El backfill es carga de producción — lotear, monitorear replication lag en réplicas, coordinar con connection pools.

Conclusion

Las migraciones de base de datos sin downtime son un contrato con la producción en ejecución: los cambios de esquema llegan compatibles con el código que sirve tráfico ahora, los datos se mueven en cronogramas que respetan límites de I/O y los pasos destructivos de contract esperan la prueba.

La próxima migración debe clasificarse antes de que alguien escriba SQL: ¿expand, migrate o contract? Si la respuesta es «los tres en una transacción», devolverla a rediseño. La producción no se pausa — las migraciones no deberían pedírselo.

Artículos relacionados

Paleta de comandos

Buscá un comando para ejecutar...