Ir al contenido

La migración que tiró el sitio

Añadir una columna es gratis. Añadirla con un valor por defecto, o un índice, o cambiar un tipo, toma un bloqueo - y en una tabla grande el bloqueo dura más que el tiempo de espera de la petición.

5 min de lectura

El despliegue salió a las cuatro y diez. La tubería estaba en verde, la migración informó de éxito, y el sitio estuvo devolviendo errores durante seis minutos en medio de todo ello.

No falló nada. Una tabla quedó bloqueada mientras se reescribía, todas las peticiones que la tocaban se encolaron detrás del bloqueo, y la cola creció más rápido de lo que se vaciaba hasta agotar el pool de conexiones.

Qué operaciones son gratis y cuáles no

La distinción que importa es si la base de datos puede cambiar los metadatos de la tabla o tiene que reescribir sus filas.

Generalmente seguro, en los dos motores principales:

  • Añadir una columna anulable sin valor por defecto
  • Eliminar una columna
  • Renombrar una tabla
  • Añadir una restricción que no se valida de inmediato, donde esté soportado

Generalmente no seguro en una tabla grande:

  • Añadir una columna con valor por defecto, en motores antiguos
  • Cambiar el tipo de una columna
  • Añadir un índice sin la opción concurrente o en línea
  • Añadir una clave foránea, que valida cada fila existente
  • Cualquier cosa que cambie la clave primaria

Aquí las versiones importan, y los detalles también: MySQL y MariaDB recientes resuelven la adición instantánea de columnas en muchos casos, y Postgres añade de forma barata una columna anulable con valor por defecto desde la versión 11. La cuestión no es memorizar la matriz sino saber de qué lado cae su migración antes de que se ejecute contra producción.

La que sorprende a la gente

Schema::table('orders', function (Blueprint $table) {
    $table->index('customer_id');
});

Añadir un índice parece leer, no escribir. En MySQL sin un camino de DDL en línea, y en Postgres sin CONCURRENTLY, toma un bloqueo durante toda la construcción, que en una tabla grande son minutos.

Postgres ofrece la opción concurrente, y Laravel no la emite, así que hay que escribirla a mano:

public function up(): void
{
    DB::statement('CREATE INDEX CONCURRENTLY orders_customer_id_index ON orders (customer_id)');
}

De esa sentencia se siguen dos cosas. No puede ejecutarse dentro de una transacción, así que la migración tiene que renunciar a la envoltura. Y puede fallar a medias, dejando detrás un índice inválido que hay que eliminar antes de reintentar, lo cual conviene saber antes y no durante.

Cambiar una columna sin reescribir la tabla

La mayoría de los cambios de tipo - ensanchar un entero, cambiar la longitud de un varchar, pasar un estado de cadena a referencia - se pueden hacer sin una sola operación bloqueante, en más pasos de los que parece que necesitan:

  1. Añada la columna nueva, anulable, sin valor por defecto. Instantáneo.
  2. Escriba en las dos columnas desde la aplicación, despliegue eso, y déjelo correr.
  3. Rellene las filas antiguas por lotes en una cola, con una pausa entre lotes para que la replicación y el resto del tráfico respiren.
  4. Verifique que coinciden: un recuento de filas donde difieran debería ser cero.
  5. Cambie las lecturas a la columna nueva. Despliegue. Espere.
  6. Deje de escribir la antigua. Despliegue.
  7. Elimínela, en una entrega posterior.

Siete despliegues en lugar de una línea. Son también siete despliegues durante los cuales el sitio sigue en pie, cada uno de los cuales se puede parar o revertir, que es el intercambio que se hace lo nombre alguien o no.

El relleno es donde va el cuidado. Un único UPDATE sobre diez millones de filas es exactamente el bloqueo que intentaba evitar, con otro sombrero:

Order::whereNull('customer_uuid')
    ->select('id')
    ->chunkById(1000, function ($orders) {
        Order::whereIn('id', $orders->pluck('id'))
            ->update([/* ... */]);
 
        usleep(100_000);
    });

Reglas que conviene tener

Cada migración declara si reescribe la tabla. Un comentario arriba, contestado con honestidad, obliga a hacerse la pregunta en la revisión y no en producción.

Todo lo que reescribe se ejecuta aparte del despliegue. Vigilado, por alguien, con capacidad de pararlo.

Nada que elimine se despliega con el código que deja de usarlo. Una columna eliminada rompe todos los procesos que siguen corriendo la entrega anterior, incluidos los workers de cola, que mantienen su código hasta que se reinician.

Conozca su tiempo de espera de bloqueo. Una migración que se rinde a los treinta segundos es enormemente mejor que una que espera indefinidamente mientras las peticiones se amontonan detrás. Ponerlo convierte una caída en una migración fallida.

Lo último es el seguro más barato de todo este artículo, y es una línea de configuración que la mayoría de las aplicaciones no ha puesto nunca.

Qué operaciones bloquean, y cuánto con sus volúmenes de filas, lo mide un encargo de base de datos sobre sus datos. Las estimaciones sirven de poco aquí. Si todavía está eligiendo motor y esto es lo que lo decide, las diferencias están desglosadas por motor.

Preguntas relacionadas

¿Esto afecta a Postgres igual que a MySQL?
A los dos, de forma distinta. Postgres puede añadir una columna anulable al instante y puede construir un índice de forma concurrente, pero sigue necesitando un bloqueo exclusivo breve para empezar - y ese bloqueo se encola detrás de transacciones largas, que es como una migración "segura" atasca una tabla ocupada. MySQL tiene DDL en línea para muchas operaciones y no para todas.
¿Cómo de grande es grande para preocuparse?
No hay un número de filas que lo haga seguro, porque lo que importa es cuánto tarda la operación frente a cuánto puede esperar su tráfico. Un millón de filas en almacenamiento rápido pueden ser segundos; la misma tabla con carga de escritura alta y un informe largo abierto contra ella es otra respuesta.
¿No podemos ejecutar las migraciones en una ventana de mantenimiento?
Puede, y es una respuesta perfectamente buena para un negocio con horas tranquilas. La mayoría de las técnicas de aquí existen para sistemas que no las tienen - e incluso con ventana, saber qué operaciones la necesitan es la parte útil.
¿Deberían ejecutarse las migraciones automáticamente en el despliegue?
Normalmente sí para las rutinarias, porque un paso manual es un paso olvidado. La excepción son exactamente las operaciones que trata este artículo: todo lo que reescribe una tabla quiere ejecutarse a propósito, vigilado, y separado del despliegue que depende de ello.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto