Aller au contenu
  • Accueil
  • Infrastructure
  • Datacenter
  • Services
    Nos services
    InfogéranceCloudHébergementIABare MetalHousingTransit IP
  • Blog
Contactez-nous
  1. Accueil
  2. /
  3. Blog
  4. /
  5. Migrer un serveur sans coupure quand la base écrit

Infrastructure
Web
Administration

Migrer un serveur sans coupure quand la base écrit

12 novembre 2026

11 min de lecture

Sommaire
J-7 : le TTL, le redémarrage et le budget
J-3 : la copie initiale et le chemin unique vers la base
J-1 : répéter, puis figer le reste
H-0 : la séquence de bascule
H+1 à J+7 : ce qui peut casser le retour
Le retour arrière, une fois que le nouveau serveur a écrit
Fermer la fenêtre de retour
Sources

Combien de secondes de gel des écritures acceptez-vous ? Cette réponse fixe l'architecture de la migration, et elle se donne avant de toucher au moindre TTL.

Migrer un serveur dont la base écrit, sans erreur côté client, demande une réplication vers le nouveau serveur, un pooler qui retient les connexions pendant le gel, et une réplication inverse créée avant la première écriture sur la cible. Le gel subsiste, réduit à un ralentissement, et le retour arrière reste possible.

Notre banc tourne sur PostgreSQL 18.6 et PgBouncer 1.26.1, en conteneurs sur un même hôte, avec une base pgbench de 755 Mo. Sans réplication, le gel dure toute la copie : 27,3 s avec pg_dump | psql, 37,3 s avec pg_dump -Fc puis pg_restore -j 4, mesurés à vide. Avec réplication logique et PgBouncer, à 455 transactions par seconde, il tombe à 0,57 s, 0,76 s et 1,27 s sur trois bascules, aucun échec sur 50 040 transactions.

Sans réseau réel, et avec un dump compressé de 14 Mo seulement, ces durées sont un plancher. Le premier passage a saturé le poste (pire transaction à 9,4 s). Un second, sur 1 million de comptes, donne la latence : bascule de 0,49 s à 100 transactions par seconde, pire transaction à 469 ms, aucun échec sur 1 959.

Pour la base seule, nos stratégies de migration de bases de données détaillent les approches de réplication. Le zéro coupure qu'elles visent exige réplication et bascule applicative.

J-7 : le TTL, le redémarrage et le budget

Google conseille de baisser le TTL au moins une semaine avant le déménagement. La mesure requête par requête est dans notre article sur la propagation DNS. Un TTL baissé sur le A et oublié sur le AAAA laisse les clients IPv6 sur l'ancienne adresse.

En PostgreSQL 18, wal_level vaut replica par défaut et ne change qu'au démarrage. Passer l'ancien serveur à logical impose un redémarrage : une vraie coupure sans pooler, à placer dans une fenêtre annoncée. Si PgBouncer est déjà en place, PAUSE, redémarrage, RESUME la masquent : environ 1 s et aucun échec sur 502 transactions, sur notre petite base. Le nouveau serveur démarre aussi en logical, sinon il ne pourra pas publier vers l'ancien. Listez aussi les tables sans clé primaire : publiées, elles refusent les UPDATE et les DELETE.

Chronométrez ensuite votre propre dump vers le nouveau serveur, sur le vrai lien. Sans réplication, le gel vaut dump, transfert, restauration et contrôles, additionnés. Si ce total passe dans une fenêtre annoncée, gardez cette méthode : elle est plus simple.

Écrivez enfin le critère de retour arrière : quel signal, quel seuil, qui décide, jusqu'à quelle heure.

J-3 : la copie initiale et le chemin unique vers la base

Le schéma ne se réplique pas : on le copie, puis on crée les publications des deux côtés, pour ne pas avoir à le faire pendant le gel.

pg_dump -h ancien -U postgres --schema-only --no-publications --no-subscriptions app \
  | psql -h nouveau -U postgres -q app
psql -h ancien  -U postgres app -c "CREATE PUBLICATION mig FOR ALL TABLES;"
psql -h nouveau -U postgres app -c "CREATE PUBLICATION mig FOR ALL TABLES;"
psql -h nouveau -U postgres app -c "CREATE SUBSCRIPTION mig_fwd
  CONNECTION 'host=ancien dbname=app user=postgres' PUBLICATION mig;"

Rejouée telle quelle, la souscription échoue en fe_sendauth: no password supplied : le serveur lui-même doit lire le mot de passe. Déposez un .pgpass dans le répertoire de l'utilisateur système postgres, des deux côtés. La copie de nos 755 Mo a pris 25 s, sans rien geler. Le banc utilise le superutilisateur ; en production, un rôle dédié. Les slots et les limites de cette mécanique sont dans notre guide de la réplication logique.

Toutes les écritures passent ensuite par un seul point : PgBouncer, en mode transaction. PAUSE y attend la fin de la transaction en cours, alors qu'en mode session il attend la déconnexion du client : votre gel n'aurait plus de borne. Basculez l'application dessus dès J-3, pour tout voir casser tôt.

Pour les fichiers, une première passe rsync -aHAX --numeric-ids --delete (elle efface sur la cible ce que la source n'a pas : vérifiez le sens avec -n), puis un test du nouveau frontal avec curl --resolve, sans toucher au DNS. Le certificat s'émet maintenant, par le défi DNS-01, car le défi HTTP interroge le serveur que le DNS désigne, donc l'ancien.

J-1 : répéter, puis figer le reste

Jouez l'aller et le retour sur une copie. Plus aucune migration de schéma ensuite, jusqu'à la fin de la fenêtre de retour : le DDL ne se réplique pas, et une colonne ajoutée sur l'éditeur seul arrête le flux (missing replicated column) jusqu'à son ajout chez l'abonné. Listez les tâches planifiées des deux serveurs, car un cron actif des deux côtés écrit en double.

H-0 : la séquence de bascule

  1. Arrêter les tâches planifiées de l'ancien serveur.
  2. PAUSE sur le pooler : les clients attendent, sans erreur.
  3. Geler le rôle applicatif sur l'ancien serveur et fermer ses sessions.
  4. Faire le dernier rsync : plus tôt, un fichier envoyé ensuite manquerait à la cible.
  5. Relever la position du WAL et attendre que le nouveau serveur l'ait rejouée.
  6. Recopier les séquences, non répliquées en PostgreSQL 18.
  7. Supprimer la souscription aller, créer l'inverse sans copie de données.
  8. Dégeler le rôle sur le nouveau serveur, repointer le pooler, RESUME.
  9. Faire de l'ancien frontal un proxy vers le nouveau, puis changer le DNS.

Le dernier rsync compte dans le gel, et nos chiffres ne l'incluent pas. Le script, rejoué tel quel à 50 transactions par seconde : 0,39 s à 0,58 s, aucun échec sur 1 534.

set -eo pipefail
bascule() { # SOURCE CIBLE SOUSCRIPTION_SUR_CIBLE SOUSCRIPTION_SUR_SOURCE
  FROM=$1; TO=$2; SUBTO=$3; SUBFROM=$4; LIMITE=$((SECONDS + 30))
  ADM="psql -h 127.0.0.1 -p 6432 -U postgres pgbouncer -At"
  $ADM -c "PAUSE app;"
  psql -h $FROM -U postgres app -Atq \
    -c "ALTER ROLE app SET default_transaction_read_only = on;" \
    -c "SELECT count(pg_terminate_backend(pid)) FROM pg_stat_activity WHERE usename='app';"
  LSN=$(psql -h $FROM -U postgres app -Atc "SELECT pg_current_wal_lsn();")
  until [ "$(psql -h $FROM -U postgres app -Atc "SELECT coalesce(bool_and(replay_lsn >= '$LSN'),false)
    FROM pg_stat_replication WHERE application_name='$SUBTO';")" = t ]; do
    [ $SECONDS -lt $LIMITE ]; sleep 0.05
  done
  psql -h $FROM -U postgres app -Atc "SELECT format('SELECT setval(%L, %s, true);',
    schemaname||'.'||sequencename, last_value) FROM pg_sequences WHERE last_value IS NOT NULL;" \
    | psql -h $TO -U postgres app -q -v ON_ERROR_STOP=1
  psql -h $TO -U postgres app -q -c "DROP SUBSCRIPTION $SUBTO;"
  psql -h $FROM -U postgres app -q -c "CREATE SUBSCRIPTION $SUBFROM
    CONNECTION 'host=$TO dbname=app user=postgres' PUBLICATION mig
    WITH (copy_data = false, origin = none);"
  psql -h $TO -U postgres app -q -c "ALTER ROLE app RESET default_transaction_read_only;"
  sed -i "s/^app = host=[^ ]*/app = host=$TO/" /etc/pgbouncer/pgbouncer.ini
  $ADM -c "RELOAD;"; $ADM -c "RESUME app;"
}
bascule ancien nouveau mig_fwd mig_rev

Une erreur, ou un rattrapage de plus de 30 s, arrête le script avec le pooler en pause (provoqué : paused reste à 1). Le gel dure alors jusqu'à votre décision, mais aucune écriture ne passe sans retour arrière.

Le poste le plus long est le rattrapage (0,78 s sur 1,27 s au premier banc) : basculez quand le retard est faible, pas sur un pic d'écriture.

Dans ce script, la souscription inverse existe avant le RESUME, et cet ordre ne se discute pas. Son slot marque le point à partir duquel le nouveau serveur conserve ses changements pour l'ancien. Une écriture passée avant serait perdue pour le retour arrière, sans bruit. Ce n'est pas la réplication bidirectionnelle que notre guide déconseille : un seul côté écrit à la fois.

Gelez le rôle plutôt que la base : avec ALTER DATABASE, toute nouvelle session passe en lecture seule, superutilisateur compris, même pour annuler le gel (sortie : SET default_transaction_read_only = off; dans la session). Le gel par rôle a sa limite : notre superutilisateur a écrit sans obstacle sur l'ancien serveur gelé, et un cron sous un autre compte ferait pareil.

L'ancien serveur relaie vers le nouveau, pour que les clients encore en cache DNS écrivent au bon endroit :

location / {
    proxy_pass https://203.0.113.10;
    proxy_ssl_server_name on;
    proxy_ssl_name www.example.fr;
    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

proxy_ssl_verify vaut off par défaut : sans lui, notre proxy a relayé vers un certificat au mauvais nom, avec lui il a rendu une 502.

H+1 à J+7 : ce qui peut casser le retour

Le slot vit sur le nouveau serveur. Si l'ancien décroche, le WAL s'accumule sur votre nouvelle production, qui finit en No space left on device. Plafonnez max_slot_wal_keep_size et alertez sur le retard. Un slot invalidé fait perdre la réplication inverse : on accepte ce prix pour garder la production debout.

Le retour arrière, une fois que le nouveau serveur a écrit

Remettre l'ancien DNS ne suffit pas. L'ancien serveur ne contient rien de ce que le nouveau a reçu depuis la bascule : les clients reviennent sur des données périmées, et les écritures de la cible sont perdues.

Si la réplication inverse tourne, le retour est la même fonction, rôles inversés :

bascule nouveau ancien mig_rev mig_fwd

Il a duré 0,57 s. Après l'aller, le retour et un nouvel aller, les comptes, les soldes et les commandes concordent des deux côtés :

old: 5000000|107040|50040|50040|107040
new: 5000000|107040|50040|50040|107040

Rapatriez ensuite les fichiers, en lisant d'abord ce que --delete retirerait : rsync -aHAXn -i --numeric-ids --delete nouveau:/var/www/ /var/www/.

Si une écriture parasite a atteint l'ancien serveur, deux cas. Un UPDATE, ou un INSERT sans collision de clé, ne déclenche rien : aucune erreur sur notre banc, et une ligne à 999999 d'un côté, 0 de l'autre. Comparez les tables avant tout retour. Avec une collision, le flux s'arrête. Provoqué par un INSERT direct :

ERROR:  conflict detected on relation "public.orders": conflict=insert_exists

Le worker réessaie en boucle : pg_stat_subscription_stats affichait 3 pour apply_error_count après 5 s. Mettez ce compteur en alerte. Un DELETE de la ligne parasite a suffi, et le flux est reparti seul. ALTER SUBSCRIPTION ... SKIP saute toute la transaction, changements sains compris.

Sans réplication inverse, on réconcilie depuis l'ancien serveur. Cela ne tient que s'il est resté gelé : le nouveau a alors raison sur tout. Si les deux ont écrit, aucune commande ne choisit à votre place, il faut une règle métier par table.

CREATE EXTENSION postgres_fdw;
CREATE SERVER nouveau FOREIGN DATA WRAPPER postgres_fdw
  OPTIONS (host 'nouveau', dbname 'app', fetch_size '10000');
CREATE USER MAPPING FOR postgres SERVER nouveau OPTIONS (user 'postgres', password 'secret');
CREATE SCHEMA nouveau;
IMPORT FOREIGN SCHEMA public LIMIT TO (orders) FROM SERVER nouveau INTO nouveau;

BEGIN;
INSERT INTO public.orders OVERRIDING SYSTEM VALUE SELECT * FROM nouveau.orders
  ON CONFLICT (id) DO UPDATE
  SET aid = EXCLUDED.aid, amount = EXCLUDED.amount, created_at = EXCLUDED.created_at;
DELETE FROM public.orders o
  WHERE NOT EXISTS (SELECT 1 FROM nouveau.orders n WHERE n.id = o.id);
COMMIT;

Ce DELETE retire de l'ancien toute ligne absente du nouveau, écriture légitime comprise : prenez un pg_dump de l'ancien avant. Joué après 1 014 transactions sur le nouveau serveur, avec un UPDATE ... FROM sur pgbench_accounts, le résultat donne des sommes identiques. Le UPDATE du million de comptes a pris 0,96 s, sans réseau réel. Chaque table se parcourt entièrement, gel en cours : mesurez à J-1. Notre test n'avait pas de clé étrangère : suivez l'ordre des dépendances, puis recopiez les séquences.

Sous MariaDB (12.3.3 testée, sans charge, log_slave_updates et gtid_strict_mode actifs), aller et retour ont tenu : read_only = ON, MASTER_GTID_WAIT (0 atteint, -1 on s'arrête), STOP SLAVE, RESET SLAVE ALL, puis CHANGE MASTER TO ... MASTER_USE_GTID = slave_pos, MASTER_DEMOTE_TO_SLAVE = 1 sur l'ancien. Même limite : root écrit malgré read_only = ON, et cette écriture a arrêté la réplication inverse (out-of-order sequence number). NO_LOCK_NO_ADMIN refuse aussi root depuis la 12.0 ; son effet sur la réplication n'est pas mesuré ici.

Fermer la fenêtre de retour

L'heure limite passée, supprimez mig_rev sur l'ancien serveur, ce qui retire le slot du nouveau, puis les publications. Éteignez l'ancienne machine seulement quand ses journaux ne reçoivent plus de trafic : c'est le critère de Google. Remontez ensuite le TTL.

Si cette bascule tombe sur une production sans équipe pour la répéter, elle entre dans le service d'infogérance de SHPV.

Sources

  • PostgreSQL 18 : CREATE SUBSCRIPTION : copy_data, origin.
  • PostgreSQL 18 : restrictions : DDL et séquences.
  • nginx : module proxy : proxy_ssl_verify, désactivé par défaut.
  • PostgreSQL 18 : conflits de réplication logique : types de conflit, SKIP.
  • PostgreSQL 18 : paramètres du WAL : wal_level.
  • PgBouncer : administration : PAUSE selon le mode de pool.
  • Google Search Central : déplacer un site sans changer d'URL : TTL, arrêt à trafic nul.
  • MariaDB : Global Transaction ID : MASTER_DEMOTE_TO_SLAVE.
  • MariaDB : variables système : read_only, dont NO_LOCK_NO_ADMIN.

Besoin d'aide sur ce sujet ?

Notre équipe d'experts est là pour vous accompagner dans vos projets d'infrastructure et d'infogérance.

Contactez-nous

Articles similaires

Erreur 524 Cloudflare : trouver sur le serveur la requête trop lente
Web
Administration
Performance
Web
Administration
Performance

Erreur 524 Cloudflare : trouver sur le serveur la requête trop lente

Cloudflare coupe à 125 secondes. La requête fautive se retrouve dans les logs de l'origine, en filtrant sur la durée et en lisant la cascade des délais.

10 nov. 2026
Lire plus
Redfish : piloter le BMC serveur en API moderne
Infrastructure
Administration
Infrastructure
Administration

Redfish : piloter le BMC serveur en API moderne

Redfish, l'API REST/JSON du DMTF, remplace IPMI pour piloter le BMC : énergie, BIOS, firmware, télémétrie. Exemples curl, Ansible, sushy.

24 juil. 2026
Lire plus
Step-CA : autorité de certification interne pour infra moderne
Sécurité
Infrastructure
Administration
Sécurité
Infrastructure
Administration

Step-CA : autorité de certification interne pour infra moderne

Déployer une PKI interne avec Smallstep step-ca. ACME, certificats short-lived, intégration K8s, mTLS, retours ops sur la gestion automatisée des certificats.

4 juin 2026
Lire plus

SHPV, votre partenaire de confiance en infrastructure et infogérance informatique en France.

SHPV
Contactez-nousNous contacterBlog
Expertise
InfrastructureDatacenterInfogéranceCloudHébergementTransit IPIABare MetalHousing
Légales
Conditions Générales de VenteCPS - Contrat de ServicesCPS - Hébergement CloudCPS - Microsoft 365Accord sous-traitance RGPDTarifs interventions

SHPV © 2026 - Tous droits réservés

Mentions légalesPolitiques de confidentialité
SHPV FRANCE - SAS au capital de 16 000 € - 52 Rue Romain Rolland, 71230 Saint-Vallier - SIRET n°80886287400035 - R.C.S. Chalon-sur-Saône. Par téléphone 09 72 310 818 - Email: support@shpv.fr