Image de couverture : Migrations de base de données sans interruption : expand-contract et lock timeouts pour Laravel et Node.js
Bases de données

Migrations de base de données sans interruption : expand-contract et lock timeouts pour Laravel et Node.js

2 novembre 2026
6 min de lecture
24 vues
Sébastien Muler

Une migration anodine peut mettre toute votre application à genoux

La plupart des pannes qui surviennent juste après un déploiement ne viennent pas du code applicatif. Elles viennent d'une migration qui paraissait inoffensive en revue de code : l'ajout d'une colonne NOT NULL avec valeur par défaut, un renommage, ou la création d'un index sur une table de plusieurs dizaines de millions de lignes. Le mécanisme est toujours le même : la migration attend un verrou, toutes les requêtes qui arrivent derrière se mettent en file, le pool de connexions se remplit, et les health checks échouent sur l'ensemble de la flotte.

Cet article reprend les grandes lignes d'un retour d'expérience publié sur DEV Community sur les migrations sans interruption pour des backends Laravel et Node.js sur PostgreSQL et MySQL, et le complète avec nos propres recommandations pour des équipes PHP/Symfony.

Pourquoi un simple ALTER TABLE bloque tout

Un ALTER TABLE demande un verrou fort sur la table. Si une transaction longue est déjà en cours sur cette table, la migration attend. Pendant cette attente, elle se place en tête de la file des verrous : toutes les requêtes suivantes, même de simples SELECT, doivent patienter derrière elle.

Le danger ne vient donc pas de la durée de la migration elle-même, qui peut ne prendre que quelques millisecondes, mais du temps passé à attendre le verrou. Une table très sollicitée transforme cette attente en embouteillage en quelques secondes.

La première défense consiste à rendre l'attente impossible à prolonger :

  • Définir un lock_timeout court sur chaque instruction DDL, pour que la migration abandonne plutôt que de bloquer le trafic.
  • Définir un statement_timeout raisonnable, pour qu'une opération inattendue ne s'éternise pas.
  • Prévoir un mécanisme de nouvelle tentative : une migration qui échoue sur un timeout est relancée un peu plus tard, quand la table est moins chargée.

Sur PostgreSQL, cela se règle au niveau de la session avant la migration :

SET lock_timeout = '3s';
SET statement_timeout = '60s';
ALTER TABLE orders ADD COLUMN status_v2 varchar(32);

Sur MySQL, on retrouve une logique voisine avec lock_wait_timeout, et il faut en plus vérifier si l'opération peut s'exécuter en mode INPLACE ou INSTANT, selon la version utilisée.

Le pattern expand-contract en trois temps

L'idée centrale est de ne jamais modifier le schéma de façon incompatible avec le code en cours d'exécution. Pendant un déploiement, l'ancienne et la nouvelle version de l'application coexistent : le schéma doit donc convenir aux deux.

1. Expand : ajouter sans casser

On ajoute la nouvelle colonne ou la nouvelle table, de façon nullable et sans contrainte lourde. L'ancien code l'ignore, le nouveau code peut commencer à l'écrire.

2. Migrate : remplir et basculer

On recopie les données existantes vers la nouvelle structure, par lots, en dehors des heures de pointe si possible. Le code écrit dans les deux emplacements (ancien et nouveau) pendant la transition, puis lit progressivement depuis le nouveau.

Un backfill par lots évite à la fois les transactions géantes et les verrous prolongés :

UPDATE orders
SET status_v2 = status
WHERE id BETWEEN 1 AND 10000
  AND status_v2 IS NULL;

On répète sur chaque tranche d'identifiants, avec une courte pause entre les lots pour laisser respirer la base et la réplication.

3. Contract : nettoyer

Une fois que plus aucun code ne lit l'ancienne colonne, et que cela a été vérifié en production, on la supprime. Les contraintes (NOT NULL, clés étrangères) s'ajoutent à ce stade, de préférence en deux étapes : d'abord en mode non validé, puis validation séparée, qui demande un verrou plus léger.

Un renommage de colonne suit exactement ce schéma : créer la nouvelle colonne, double écriture, bascule des lectures, suppression de l'ancienne. C'est plus long qu'un RENAME COLUMN, mais chaque étape est réversible.

L'ordre de déploiement compte autant que la migration

Un schéma compatible ne suffit pas si l'ordre des opérations est mauvais. La séquence qui fonctionne généralement :

  • Déployer la migration additive (expand) seule, avant le nouveau code.
  • Déployer le code qui écrit dans les deux structures.
  • Lancer le backfill, puis surveiller qu'il est complet.
  • Déployer le code qui lit la nouvelle structure.
  • Retirer l'écriture vers l'ancienne structure, puis exécuter la migration de nettoyage dans une version ultérieure.

Côté outillage, les migrations Laravel et Doctrine s'exécutent souvent automatiquement au déploiement. Pour les tables sensibles, il vaut mieux séparer les migrations lourdes du déploiement applicatif et les piloter explicitement, avec les timeouts configurés en tête de fichier. Pour les index, privilégier la création concurrente quand le moteur la supporte (CREATE INDEX CONCURRENTLY sur PostgreSQL), en sachant qu'elle ne peut pas s'exécuter dans une transaction.

Checklist avant de valider une migration

  • La migration est-elle compatible avec l'ancienne version du code ?
  • Un lock_timeout est-il défini, avec une stratégie de nouvelle tentative ?
  • Le backfill est-il découpé en lots, et reprenable en cas d'échec ?
  • Les contraintes lourdes sont-elles ajoutées après coup, en deux temps ?
  • Un retour arrière est-il possible à chaque étape ?

Conclusion

Une migration sans interruption n'est pas une question de chance : c'est une discipline. Des timeouts stricts sur chaque instruction DDL, le découpage en expand, migrate et contract, des backfills par lots et un ordre de déploiement maîtrisé suffisent à éviter l'essentiel des pannes liées au schéma. Cette méthode s'applique aussi bien à Laravel qu'à Symfony avec Doctrine : seul l'outil de migration change, pas le raisonnement.

Ce que ça change pour vous

Une mise à jour de la structure de vos données peut, mal préparée, bloquer votre application pendant plusieurs minutes, sans qu'aucune faute de programmation n'en soit la cause. Avec une méthode en plusieurs petites étapes, vos clients continuent de commander, de se connecter et de payer pendant que la mise à jour s'effectue. Cela réduit le risque d'une interruption en pleine journée et vous évite de bloquer des créneaux de maintenance la nuit ou le week-end. C'est un point à demander à votre prestataire avant toute évolution importante de votre base de données.

Partager cet article

Et votre site, il vaut quoi ?

Vitesse, accessibilité, référencement technique. Le rapport est écrit pour un dirigeant, pas pour un développeur. Gratuit, sans inscription.

Tester mon site