PostgreSQL 16: replicación lógica que ya es práctica

Hileras de servidores con luces LED azul y verde representando replicación de bases de datos

PostgreSQL 16, publicado en septiembre de 2023, no trajo una característica que acaparase titulares, sino algo más valioso para quien opera bases de datos en producción: cerró, una a una, las cuatro o cinco grietas que convertían la replicación lógica en un ejercicio de paciencia. Durante años la opción por defecto para migrar o distribuir cambios entre servidores PostgreSQL había sido la replicación física (streaming), sencilla y robusta pero inflexible: copia binaria bloque a bloque, todo o nada, misma versión mayor en origen y destino. La lógica, introducida formalmente en PostgreSQL 10, prometía granularidad por tabla, independencia de versión y el tipo de manipulación que hace posible el CDC (change data capture) hacia sistemas externos. Lo que prometía, sin embargo, chocaba en producción con cuellos de botella difíciles de justificar ante un equipo. PostgreSQL 16 corrige los más dolorosos.

Lógica frente a física: el malentendido habitual

La confusión más frecuente es tratar ambos modos como intercambiables. No lo son. La replicación física envía el WAL en crudo a una réplica idéntica byte a byte; es la base de los hot standbys, del failover y de casi toda alta disponibilidad seria en PostgreSQL. La lógica, en cambio, decodifica ese WAL y lo reexpresa como operaciones de alto nivel —INSERT, UPDATE, DELETE, TRUNCATE— asociadas a una publicación que filtra qué tablas salen y qué columnas se incluyen. Esa decodificación tiene un coste de CPU en el origen que la física no paga, pero a cambio permite replicar entre versiones distintas, hacia destinos con esquema diferente, o hacia consumidores que no son siquiera PostgreSQL (Kafka vía Debezium, ClickHouse, Snowflake). Son herramientas complementarias: nadie debería tirar el standby físico para montarlo todo con lógica, ni al revés.

Las grietas que se cerraron

Hasta PostgreSQL 15 el apply en el subscriber era estrictamente secuencial. Una transacción grande en el origen —una carga nocturna, un UPDATE masivo— bloqueaba la cola entera durante minutos y, bajo carga sostenida, el lag crecía hasta disparar alertas o forzar el descarte del slot. La 16 introduce parallel apply: cuando una transacción supera el umbral de logical_decoding_work_mem el subscriber la reparte entre varios workers declarados con max_parallel_apply_workers_per_subscription. La mejora típica para cargas write-heavy ronda el 2-5x de throughput, suficiente para que una réplica lógica deje de quedarse atrás sistemáticamente.

La segunda grieta afectaba a la topología. Antes de la 16, sólo el primario podía exponer slots lógicos, lo que convertía al primario en cuello de botella cuando había muchos subscribers. A partir de la 16 un standby físico puede exponer sus propios slots lógicos y alimentar a consumidores downstream, descargando al primario y permitiendo arquitecturas de fan-out más limpias: un primario, un puñado de standbys físicos regionales y, colgando de ellos, los subscribers lógicos de cada región. La tercera grieta —la más cacareada y, en mi opinión, la más traicionera— es el soporte para replicación bidireccional. No es multi-master automático: PostgreSQL sigue sin resolver conflictos por ti. Lo que se añade es el andamiaje para construirlo sin extensiones externas, siempre que la aplicación respete particiones de claves primarias que eviten escrituras simultáneas sobre la misma fila en dos nodos. Sin esa disciplina, acabarás con divergencia silenciosa.

Publicaciones, suscripciones y resolución de conflictos

El modelo conceptual sigue siendo el mismo de la 10: en el origen se crea una publicación que declara qué tablas (o todas, con FOR ALL TABLES) salen, y en el destino una suscripción que apunta a esa publicación con una cadena de conexión y opciones como streaming = parallel para activar el apply paralelo. Una vez en marcha, el estado se consulta en pg_stat_subscription (lag de apply, worker activo) y en pg_replication_slots (posición confirmada del slot, retención de WAL acumulada).

Los conflictos merecen párrafo propio. PostgreSQL 16 no resuelve automáticamente una colisión de clave primaria entre un INSERT local y otro llegado por replicación; lo que hace es detenerse, marcar la suscripción como errored y emitir una entrada en el log con el LSN exacto. La operación consiste en decidir manualmente: corregir el origen, saltar la transacción con ALTER SUBSCRIPTION ... SKIP (lsn = ...), o desactivar temporalmente con DISABLE y reactivar tras la reparación. Este último flujo es una de las mejoras prácticas de la 16: en versiones anteriores muchas operaciones requerían DROP y CREATE con resincronización completa, algo impensable sobre tablas grandes.

CREATE PUBLICATION mypub FOR TABLE users, orders;

CREATE SUBSCRIPTION mysub
  CONNECTION 'host=primary port=5432 dbname=mydb user=replicator password=secret'
  PUBLICATION mypub
  WITH (streaming = parallel);

SELECT subname, received_lsn, latest_end_lsn, latest_end_time
FROM pg_stat_subscription;

Lo que sigue fuera del alcance

Pintar un cuadro realista exige enumerar lo que no hace. No replica DDL: un ALTER TABLE ADD COLUMN debe aplicarse manualmente a ambos lados en orden (primero destino, luego origen) para que el subscriber no falle al recibir filas con columnas desconocidas. Las secuencias no se sincronizan automáticamente; tras un cutover hay que avanzar la secuencia del destino con setval. Los large objects (lo_*) siguen fuera del alcance. Y aunque TRUNCATE sí se replica desde la 11, arrastra caveats cuando hay claves foráneas entre tablas publicadas y no publicadas. Para escenarios que necesiten DDL automático, conflict resolution determinista o multi-master genuino, extensiones como pgLogical o soluciones enterprise como BDR siguen siendo la respuesta.

Operación y migraciones

En la práctica la replicación lógica brilla en dos contextos. El primero es la migración con downtime mínimo entre versiones mayores o entre proveedores: se establece la suscripción desde el origen PG14/15 hacia un destino PG16 recién provisionado, se deja converger, se pausa la aplicación unos segundos para el último catch-up, se conmuta la cadena de conexión y se decomisiona el viejo. Minutos de ventana frente a las horas de un pg_dump clásico. El segundo es el CDC hacia un data warehouse, donde Debezium se suscribe al slot lógico y empuja los cambios a Kafka; desde ahí alimenta cualquier destino analítico sin tocar la transaccional.

En operación, tres métricas importan más que el resto. La retención de WAL acumulada por cada slot —un slot huérfano puede llenar el disco del primario en horas—, el lag de apply en el subscriber, y el número de workers paralelos realmente utilizados frente al máximo configurado. Alertar en Prometheus sobre pg_replication_slot_retained_wal_bytes y sobre el delta entre confirmed_flush_lsn y pg_current_wal_lsn() cubre el 90% de los incidentes. El 10% restante son conflictos lógicos que requieren intervención humana, y ahí la disciplina al diseñar claves primarias y particionar escrituras pesa más que cualquier ajuste del servidor.

PostgreSQL 16 no convierte la replicación lógica en un interruptor mágico; sigue siendo una herramienta que premia la comprensión del WAL, de los slots y de la semántica de conflictos. Lo que sí hace es eliminar las excusas técnicas que durante años la mantuvieron relegada a casos marginales. Hoy, para migraciones entre versiones, arquitecturas geo-distribuidas con lecturas locales, o flujos CDC razonables, la respuesta correcta casi siempre empieza por la replicación lógica incluida en la base de datos, no por una herramienta externa.

Entradas relacionadas