Livrez chaque changement de schéma en deux déploiements
L'affirmation Une migration qui modifie la forme d'une table et le code qui la lit dans le même déploiement ne peut pas être annulée. Pendant les secondes ou les minutes où l'ancie...
L'affirmation
Une migration qui modifie la forme d'une table et le code qui la lit dans le même déploiement ne peut pas être annulée. Pendant les secondes ou les minutes où l'ancien et le nouveau code tournent en parallèle — c'est-à-dire à chaque déploiement, sauf à mettre le site hors ligne — l'un des deux dialogue avec un schéma qu'il ne comprend pas. Répartissez le changement sur deux déploiements et les deux problèmes disparaissent.
Le motif : étendre, migrer, contracter
Prenons une demande courante : renommer users.phone en users.phone_e164 et reformater les valeurs. La version en une étape tient dans une migration et un changement de code. Elle casse dès qu'une requête atteint un ancien conteneur.
La version en trois étapes :
Déploiement 1 — étendre. Ajouter la nouvelle colonne. Écrire dans les deux, lire l'ancienne.
ALTER TABLE users ADD COLUMN phone_e164 text;
Remplissage — en dehors de tout déploiement. Par lots, reprenable, bridé :
UPDATE users SET phone_e164 = normalise_phone(phone)
WHERE phone_e164 IS NULL AND id IN (
SELECT id FROM users WHERE phone_e164 IS NULL ORDER BY id LIMIT 5000
);
Bouclez avec une courte pause entre les lots. Un seul UPDATE sur deux millions de lignes conserve les verrous de ligne pendant toute l'instruction et gonflera la table ; cinq mille à la fois avec une pause de 200 ms se termine en moins d'une demi-heure sans que personne ne s'en aperçoive.
Déploiement 2 — basculer les lectures. Lire la nouvelle colonne, écrire encore les deux.
Déploiement 3 — contracter. Cesser d'écrire l'ancienne colonne, puis la supprimer, idéalement une semaine plus tard une fois rassuré.
ALTER TABLE users DROP COLUMN phone;
Chaque déploiement est réversible indépendamment, car à aucun moment le code en exécution ne dépend d'un changement de schéma livré dans la même version.
Les verrous qui font mal
Postgres prend un verrou ACCESS EXCLUSIVE pour la plupart des formes d'ALTER TABLE. Le verrou n'est habituellement tenu que quelques microsecondes — ajouter une colonne nullable sans valeur par défaut ne touche que le catalogue — mais son obtention suppose d'attendre la fin de toutes les transactions en cours sur la table et, pendant cette attente, chaque nouvelle requête s'empile derrière. Un seul rapport de longue durée transforme une migration instantanée en panne de deux minutes.
Protégez-vous :
SET lock_timeout = '3s';
SET statement_timeout = '30s';
ALTER TABLE users ADD COLUMN phone_e164 text;
Si le verrou n'est pas obtenu en trois secondes, la migration échoue proprement et vous réessayez au lieu de figer l'application. Ce seul réglage évite plus d'incidents que n'importe quelle planification soignée.
Trois opérations à traiter autrement
- Les index : toujours
CREATE INDEX CONCURRENTLY. C'est plus long et cela ne peut pas s'exécuter dans une transaction, donc pas dans un fichier de migration ordinaire chez la plupart des cadriciels : faites-en une étape distincte. Une construction concurrente échouée laisse un index invalide ; vérifiez avecSELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;et supprimez ce que vous trouvez. - NOT NULL : ajoutez une contrainte
CHECK (col IS NOT NULL) NOT VALID, exécutezVALIDATE CONSTRAINTséparément, puis passez la colonne en NOT NULL. Les versions récentes de Postgres s'appuient sur la contrainte validée pour éviter le balayage complet. - Les clés étrangères : même motif —
NOT VALIDd'abord, validation ensuite. La validation prend un verrou moins fort et peut tourner en heures ouvrables.
Rendre le motif vérifiable
Le motif ne tient que si quelqu'un vérifie que l'ancien code fonctionne toujours contre le nouveau schéma. Ajoutez une étape à votre chaîne : récupérez l'étiquette de la version précédente, exécutez sa batterie de tests contre une base ayant reçu la nouvelle migration, et faites échouer la construction au moindre bris. Cette seule tâche attrape le cas où un développeur a ajouté une colonne NOT NULL sans valeur par défaut, qui reste la façon la plus fréquente dont une étape d'extension bien intentionnée met quand même le site hors ligne.
Ce que cela vous apporte
L'avantage évident est d'éviter l'indisponibilité. Celui qui compte davantage, c'est que le retour arrière redevient une véritable option. Quand un déploiement dérape à 16 h 40, l'écart entre revenir à la version précédente en quatre-vingt-dix secondes et se demander si une migration peut être annulée sans dégât, c'est l'écart entre un désagrément et une soirée perdue.
Deux règles à faire respecter en revue
- Aucune demande de fusion ne contient à la fois un changement de schéma et le code qui en dépend. Si un réviseur voit les deux, on scinde.
- Chaque migration fixe
lock_timeout. Mettez-le dans le gabarit de migration pour que personne n'ait à y penser.
Le coût, c'est qu'un renommage prend désormais trois déploiements étalés sur une semaine plutôt qu'un seul le mardi après-midi. En échange, aucun déploiement de cette séquence ne peut mettre le site hors ligne, et chacun s'annule le temps de redéployer l'image de conteneur précédente.