Le malentendu le plus coûteux qu'on croise sur PostgreSQL tient en une phrase : « on a déjà de la réplication, on va s'en servir pour alimenter le datawarehouse ». Non. La réplication physique et la réplication logique sont deux mécaniques distinctes, qui résolvent deux problèmes distincts, et confondre les deux se paie en heures de production perdues.
La réplication physique, celle qu'on met en place derrière un cluster PostgreSQL avec Patroni, expédie le WAL binaire octet pour octet. Le secondaire est une copie conforme du primaire : même version majeure, même architecture, mêmes fichiers sur disque, mêmes tables, tout ou rien. C'est parfait pour la haute disponibilité, inutilisable pour tout le reste. Vous ne pouvez pas répliquer trois tables sur quinze. Vous ne pouvez pas écrire sur le secondaire. Vous ne pouvez pas envoyer ça vers ClickHouse ou vers un bus d'événements.
La réplication logique décode le WAL et reconstruit des changements au niveau de la ligne : celle-ci insérée, celle-là modifiée avec ces valeurs, cette autre supprimée. Un flux structuré, pas des blocs binaires. Il traverse les versions majeures et les architectures, et il peut atterrir ailleurs que dans un PostgreSQL. C'est le socle du Change Data Capture.
Ce que la mécanique fait réellement
Trois briques, et il faut les tenir séparées dans sa tête.
Le slot de réplication est une marque posée dans le WAL. Il dit au primaire : « je n'ai pas encore consommé au-delà de cette position, ne supprime pas les segments correspondants ». Tant que le slot existe, PostgreSQL garde le WAL. C'est une garantie de durabilité, et c'est aussi une arme pointée sur votre partition de données. On y revient.
Le plugin de décodage transforme le WAL en événements exploitables. Depuis PostgreSQL 10, pgoutput est intégré au moteur : rien à compiler, rien à installer, il est là. C'est ce qui l'a imposé face à decoderbufs ou wal2json, surtout dans les environnements où poser une bibliothèque native sur le serveur de base est refusé par principe.
La publication et la souscription définissent le périmètre côté PostgreSQL natif. CREATE PUBLICATION sur le primaire, CREATE SUBSCRIPTION sur la cible. Trois paramètres à poser dans postgresql.conf avant tout : wal_level à logical, max_wal_senders et max_replication_slots dimensionnés au nombre de consommateurs. Le passage de wal_level à logical impose un redémarrage, pas un reload : prévoyez la fenêtre.
Le CDC, ou comment sortir de l'ETL nocturne
Le cas d'usage qui justifie le chantier, on le voit toujours arriver sous la même forme. Une base transactionnelle de quelques centaines de gigaoctets, une équipe data qui veut des chiffres, et un job batch qui tourne à 2h du matin en rejouant un SELECT massif sur les tables de production. Ça marche jusqu'au jour où le batch déborde sur les heures ouvrées, ou jusqu'au jour où le métier demande de la fraîcheur à la minute.
Le Change Data Capture retourne le problème. Au lieu d'interroger périodiquement la base, on branche un consommateur sur le flux de décodage logique. Debezium est l'implémentation de référence : un connecteur Kafka Connect qui ouvre un slot, décode via pgoutput, et publie un événement par changement dans un topic Kafka. Chaque événement porte l'état avant et l'état après, plus des métadonnées de transaction.
En sortie de Kafka, vous branchez ce que vous voulez : un sink JDBC vers l'entrepôt analytique, un consommateur qui invalide un cache Redis en quasi temps réel, un service qui déclenche des notifications. La base transactionnelle ne voit qu'un seul lecteur supplémentaire : le walsender. Elle ne subit plus les scans de l'ETL.
Le gain n'est pas seulement la fraîcheur, c'est le découplage : ajouter un cinquième consommateur du flux ne coûte rien à PostgreSQL si vous passez par Kafka, alors que cinq ETL batch, c'est cinq fois la charge sur la source. Si vous partez sur cette architecture, la supervision du cluster Kafka en mode KRaft devient une dépendance de production au même titre que la base elle-même : un Kafka indisponible, et c'est le slot PostgreSQL qui prend l'eau.
Le piège numéro un : le slot qui grossit
Voilà le point sur lequel il faut être brutal, parce qu'il tue des bases de production tous les mois.
Un slot de réplication garantit la rétention du WAL. Si le consommateur décroche, s'arrête, ou se met à traiter plus lentement que la base ne produit, le slot n'avance plus. PostgreSQL, lui, continue d'accumuler des segments WAL qu'il ne peut pas recycler. La partition se remplit. Quand elle est pleine, le moteur s'arrête. Pas de dégradation progressive, pas de mode lecture seule salvateur : arrêt.
Le paramètre max_slot_wal_keep_size, apparu en PostgreSQL 13, existe précisément pour ça. Sa valeur par défaut est -1, c'est-à-dire rétention illimitée. Autrement dit : par défaut, PostgreSQL préfère mourir plutôt que de perdre un slot. Une valeur sans unité est interprétée en mégaoctets, et le paramètre ne se change que dans postgresql.conf ou en ligne de commande du serveur.
La règle qu'on applique : max_slot_wal_keep_size toujours positionné, jamais laissé à -1 sur une base qu'on opère. On dimensionne à ce que la partition peut absorber sans risque, typiquement une fraction du volume libre, et on accepte le compromis. Quand le seuil est franchi, PostgreSQL invalide le slot. Le consommateur devra refaire un instantané complet. C'est douloureux, mais c'est une opération planifiable en journée avec du café. Un moteur arrêté à 3h du matin parce qu'un connecteur Debezium a planté le vendredi soir, non.
Deuxième garde-fou, disponible depuis PostgreSQL 18 : idle_replication_slot_timeout, qui invalide automatiquement un slot resté inactif trop longtemps. Utile contre le slot oublié, celui qu'un développeur a créé pour un test six mois plus tôt.
Troisième garde-fou, et c'est du bon sens : une alerte sur pg_replication_slots. On surveille le retard en octets entre restart_lsn et la position courante, on alerte bien avant la saturation. Une supervision qui découvre le problème quand la partition est à 95 pour cent n'est pas une supervision, c'est un constat de décès.
Et sur une base sans écriture, un slot peut aussi rester bloqué parce que le WAL n'avance plus, ce qui fausse les métriques de retard. C'est pour ça que Debezium expose heartbeat.interval.ms : un battement périodique qui force la progression du slot même quand la table surveillée est calme.
Le piège numéro deux : l'identité de ligne
Pour émettre un événement de suppression ou de modification, le décodage logique a besoin d'identifier la ligne concernée. Par défaut, c'est la clé primaire. Pas de clé primaire, pas d'identité de réplication : les UPDATE et DELETE sur cette table font échouer la réplication.
Le contournement classique est REPLICA IDENTITY FULL, qui embarque l'intégralité de l'ancienne ligne dans le WAL. Ça résout le problème fonctionnel et ça en crée un autre : plus de volume WAL, plus de trafic réseau, et côté abonné une recherche de ligne qui peut dégénérer. Un benchmark publié par Xata en 2025 mesure, sur leur charge de test, un passage du pic CPU d'environ 30 à 35 pour cent avec REPLICA IDENTITY FULL activé, et une hausse comparable du pic de retard du slot. Le chiffre vaut pour cette charge précise, pas comme constante universelle, mais l'ordre de grandeur est le bon : l'impact est réel et modéré tant que les tables répliquées ont une clé primaire.
Notre position est plus simple : une table sans clé primaire dans une base de production est un défaut de conception avant d'être un problème de réplication. Corrigez la table, pas le paramètre.
Ce que la réplication logique ne fera jamais pour vous
La liste des restrictions officielles mérite d'être lue avant de promettre quoi que ce soit au métier.
Le DDL n'est pas répliqué. Un ALTER TABLE ADD COLUMN sur le primaire ne se propage pas. Le schéma initial se copie à la main avec pg_dump --schema-only, et chaque évolution doit être appliquée des deux côtés. La bonne pratique documentée : appliquer les changements additifs d'abord sur l'abonné, pour éviter les erreurs intermittentes pendant la fenêtre de désynchronisation.
Les séquences ne sont pas répliquées. Les valeurs des colonnes serial ou identity passent bien, mais le compteur de séquence côté abonné reste à sa valeur de départ. Sans importance pour un abonné en lecture seule. Dramatique le jour où vous basculez dessus en écriture : conflits de clé primaire immédiats. Un pg_dump des séquences ou un recalcul depuis les données, obligatoire dans la procédure de bascule.
Les grands objets ne sont pas répliqués. Seules les tables le sont, y compris les tables partitionnées. Ni vues, ni vues matérialisées, ni tables distantes.
Sur les tables partitionnées, la réplication part par défaut des partitions feuilles, qui doivent donc exister côté abonné. L'option publish_via_partition_root de CREATE PUBLICATION permet de publier sous l'identité de la table racine, ce qui simplifie la vie quand le partitionnement diffère des deux côtés.
Enfin, un slot logique ne vit que sur un primaire. Historiquement, une bascule Patroni faisait perdre le slot et imposait un instantané complet. PostgreSQL 18 corrige ça sérieusement : failover = true sur la souscription ou sur pg_create_logical_replication_slot, plus sync_replication_slots sur le secondaire, synchronisent le slot vers le standby. Le walsender logique garantit que le slot de bascule ne consomme pas de changements avant qu'ils soient reçus et écrits sur le standby physique. Si vous montez du CDC sur un cluster à haute disponibilité, c'est l'argument qui justifie de viser PostgreSQL 18.
La bidirectionnelle : la fausse bonne idée
La question arrive systématiquement : « et si on répliquait dans les deux sens, pour avoir deux sites actifs ? »
Techniquement, PostgreSQL sait le faire depuis la version 16 avec origin = none sur la souscription, qui évite le rebouclage infini des changements. Fonctionnellement, vous venez d'inventer un système distribué avec résolution de conflits, et PostgreSQL ne résout rien tout seul : deux sites qui modifient la même ligne au même moment produisent un conflit, et par défaut la réplication s'arrête sur erreur. PostgreSQL 18 ajoute des compteurs dans pg_stat_subscription_stats, ce qui aide à mesurer le problème sans le résoudre.
Le multi-maître se justifie quand la partition des écritures est stricte et démontrable : le site A n'écrit jamais sur les lignes du site B, garanti par la clé, pas par une convention d'équipe. Dans tous les autres cas, un primaire unique avec bascule automatique coûte infiniment moins cher en exploitation. Une réplication bidirectionnelle sans règle de partition écrite est une panne à retardement.
La migration sans coupure, le cas qui vaut le détour
Le meilleur argument commercial de la réplication logique n'est pas le CDC, c'est la montée de version majeure.
pg_upgrade demande une fenêtre de coupure qui grandit avec le volume. La réplication logique traverse les versions majeures : vous montez une cible en PostgreSQL 18, vous souscrivez à la publication du primaire en 15, vous laissez rattraper, vous vérifiez, et vous basculez le trafic applicatif quand tout est en place. La coupure se réduit au temps de reconfiguration des connexions, quelques secondes derrière un PgBouncer correctement configuré.
L'outil pg_createsubscriber industrialise ce scénario en convertissant un standby physique en abonné logique, ce qui évite de recopier les données. PostgreSQL 18 lui ajoute --all pour traiter toutes les bases d'un coup, --clean pour nettoyer les publications résiduelles, et --enable-two-phase pour les transactions préparées.
Le point d'attention reste le même : les séquences. Elles ne sont pas répliquées, elles doivent être resynchronisées dans la fenêtre de bascule. C'est la ligne de la procédure qu'on oublie, et c'est celle qui fait échouer la migration. Les autres pièges classiques d'un changement de moteur sont détaillés dans notre guide des stratégies de migration de bases de données.
Ce qu'on recommande concrètement
Avant d'ouvrir le premier slot, trois décisions à prendre et à écrire.
Dimensionner et plafonner. max_slot_wal_keep_size positionné explicitement, alerte de supervision sur le retard de chaque slot, et une procédure écrite de reprise après invalidation. Si personne dans l'équipe ne sait ce qui se passe quand un slot est invalidé, l'architecture n'est pas prête.
Isoler la charge de décodage. Le décodage logique consomme du CPU sur le primaire. Sur une base déjà tendue, le sujet se traite d'abord côté optimisation PostgreSQL avancée, pas en empilant des slots sur un serveur qui sature déjà.
Jouer la panne avant la mise en production. Coupez le consommateur pendant deux heures, regardez le WAL grimper, vérifiez que l'alerte part au bon seuil, provoquez l'invalidation du slot et mesurez le temps de reprise complète. Une chaîne CDC dont le scénario de décrochage n'a jamais été joué n'est pas une chaîne CDC, c'est une hypothèse.
Monter du CDC sur une base de production, c'est ajouter une dépendance permanente entre votre moteur transactionnel et un consommateur externe. Le paramétrage PostgreSQL est la partie facile : ce qui se paie sur la durée, c'est la supervision des slots, la procédure de reprise et la synchronisation de schéma entre les deux extrémités. C'est ce type de chaîne qu'on opère au quotidien sur les infrastructures de nos clients, avec la base et le bus d'événements dans le même périmètre de supervision. Si vous montez une réplication logique vers un système analytique et que la question « qui regarde le slot » n'a pas encore de réponse, on peut cadrer l'architecture et prendre l'exploitation en charge.
Sources
- PostgreSQL Documentation : Restrictions de la réplication logique : liste officielle de ce qui n'est pas répliqué, DDL, séquences, grands objets, contraintes sur les tables partitionnées.
- PostgreSQL Documentation : Logical Replication Failover : synchronisation des slots vers un standby avec
failoveretsync_replication_slots. - PostgreSQL Documentation : max_slot_wal_keep_size : comportement par défaut de la rétention WAL et invalidation des slots.
- PostgreSQL 18 Release Notes :
idle_replication_slot_timeout, compteurs de conflits, nouvelles options depg_createsubscriber. - Debezium : connecteur PostgreSQL : paramétrage requis, heartbeat, identité de réplication, limites du décodage logique.
- Debezium : installation des plugins de décodage logique : comparaison
pgoutput,decoderbufset contraintes d'installation. - Xata : On the performance impact of REPLICA IDENTITY FULL in Postgres : mesures CPU et retard de slot sur charge réelle.
- Gunnar Morling : Mastering Postgres Replication Slots : pièges de production sur la croissance des slots, par un des auteurs de Debezium.


