10. Replicación y alta disponibilidad
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
El script de la lección es ops/10-replication.sh, y los ejercicios están en ops/10-exercises.sh. Son scripts de shell, no scripts SQL: la replicación necesita varios servidores. check.sh ops los ejecuta en el contenedor del curso y compara su salida con expected/10-replication.txt y expected/10-exercises.txt.
| SQL Server | PostgreSQL |
|---|---|
| registro de transacciones | write-ahead log (WAL), en archivos de segmento de 16 MB |
| grupo de disponibilidad, réplica secundaria | un servidor standby que reproduce el WAL del primario (replicación física) |
| secundaria legible | hot standby |
| modos de confirmación síncrona y asíncrona | synchronous_standby_names y synchronous_commit |
| conmutación por error automática con un gestor de clúster | pg_promote(); la detección y la decisión quedan para otras herramientas |
| trasvase de registros | archivado del WAL y restore_command (lección 11) |
| replicación transaccional, publicaciones y suscripciones | replicación lógica, publicaciones y suscripciones |
Tres clústeres en un contenedor
Sección titulada «Tres clústeres en un contenedor»Todo cambio en los datos se escribe primero en el write-ahead log, como el registro de transacciones de SQL Server. Un standby es un servidor que recibe ese WAL del primario y lo reproduce, de modo que sus archivos siguen siendo una copia de los del primario: la replicación física.
La replicación necesita varios servidores. En lugar de varios contenedores, el script arranca pequeños clústeres dentro del contenedor del curso, junto al servidor del curso: initdb crea el directorio de un clúster, pg_ctl lo arranca en su propio puerto, y psql lo alcanza por el socket Unix. Se ejecuta con:
bash server.sh startbash server.sh sh < ops/10-replication.shserver.sh sh pasa el script a bash en el contenedor, como usuario postgres. Primero vienen unas cuantas funciones auxiliares (líneas 10-28):
# say <text>: un título en la salida; lsn: oculta las posiciones del WAL, que cambian de una ejecución a otrasay() { printf '\n-- %s\n' "$*"; }lsn() { sed -E 's#[0-9A-F]+/[0-9A-F]{1,8}#X/XXXXXXXX#g'; }# start <node> <port>: el puerto y el nombre que comunican los standbys van en la línea de comandos, y la configuración copiada no guarda ningunostart() { pg_ctl -D "$1" -l "$1.log" -o "-p $2 -c cluster_name=$1" -w start > /dev/null || exit 1; }# until_true <port> <database> <query>: repite hasta que la consulta devuelve true, 30 segundos como muchountil_true() { for _ in $(seq 150); do [ "$(psql -X -Atq -p "$1" -d "$2" -c "$3" 2>/dev/null)" = t ] && return 0 sleep 0.2 done echo "timed out on port $1: $3"}sql() { local port=$1; shift; psql -X -p "$port" -d learn "$@" 2>&1; }# add_run <port> <run id> <workflow name> [psql options]: inserta una ejecución de la rama mainadd_run() { sql "$1" "${@:4}" -c "INSERT INTO ci.runs VALUES ($2, '$3', 'push', 'success', 'main', repeat('a', 40), 1, '2026-09-16 12:00+00', '2026-09-16 12:00+00', '2026-09-16 12:00+00')"}until_truerepite una consulta: la replicación es asíncrona, y el script espera a un estado antes de imprimirlo, para que la salida sea la misma en cada ejecución.lsnoculta las posiciones del WAL, que cambian de una ejecución a otra.startda a cada nodo su puerto y sucluster_nameen la línea de comandos: un standby copia los archivos de configuración del primario, y si no copiaría también su puerto.
El primer nodo, con el esquema del curso (líneas 30-41):
say "node1: a new cluster, with enough WAL for logical decoding, and the course schema"initdb -D node1 --auth=trust > /dev/nullcat >> node1/postgresql.conf <<'EOF'shared_buffers = 32MBwal_level = logical# pg_rewind lee el WAL hasta el último checkpoint que comparten ambos nodos: conservar unos segmentos en lugar de reciclarloswal_keep_size = 128MBEOFstart node1 5433psql -X -q -p 5433 -c 'CREATE DATABASE learn'psql -X -q -p 5433 -d learn -c '\o /dev/null' -c '\i /course/sql/schema.sql'sql 5433 -Atc 'SHOW data_checksums'-- node1: a new cluster, with enough WAL for logical decoding, and the course schemaonwal_level = logical escribe WAL suficiente para la replicación lógica, al final de la lección; el valor por defecto, replica, basta para un standby. data_checksums está a on: PostgreSQL 18 «change[d] initdb default to enable data checksums» (notas de versión), lo que pg_rewind necesitará.
Un standby
Sección titulada «Un standby»pg_basebackup copia un clúster en marcha a través de una conexión de replicación (líneas 43-50):
say "node2: a copy of node1 taken over a replication connection, with a slot and the settings of a standby (-R)"pg_basebackup -p 5433 -D node2 -R -C -S node2 -X stream -c fastls node2/*.signalgrep -o "primary_slot_name = .*\|port=[0-9]*" node2/postgresql.auto.confstart node2 5434until_true 5433 learn "SELECT EXISTS (SELECT FROM pg_stat_replication WHERE application_name = 'node2' AND state = 'streaming')"sql 5433 -c "SELECT application_name, state, sync_state FROM pg_stat_replication"sql 5433 -c "SELECT slot_name, slot_type, active, wal_status FROM pg_replication_slots"-- node2: a copy of node1 taken over a replication connection, with a slot and the settings of a standby (-R)node2/standby.signalport=5433primary_slot_name = 'node2' application_name | state | sync_state------------------+-----------+------------ node2 | streaming | async(1 row)
slot_name | slot_type | active | wal_status-----------+-----------+--------+------------ node2 | physical | t | reserved(1 row)-Rescribestandby.signal, que arranca la copia como standby, yprimary_conninfoenpostgresql.auto.conf, que le dice dónde está el primario.-C -S node2crea un slot de replicación en el primario y lo usa: el primario conserva el WAL quenode2aún no ha recibido, incluso mientrasnode2está parado.wal_status = reservedindica que el WAL se conserva. Un slot cuyo standby no vuelve nunca conserva WAL para siempre;max_slot_wal_keep_sizelo limita.-X streamtransmite con la copia el WAL escrito durante ella.pg_stat_replicationen el primario enumera sus standbys:node2, elcluster_nameque comunica, está enstreaming, de forma asíncrona.
Un standby es un hot standby: responde a consultas, y rechaza las escrituras (líneas 52-57):
say "A write on node1 is replayed on node2, which answers read-only queries"add_run 5433 1 'Replication test' -qlsn=$(psql -X -Atq -p 5433 -c 'SELECT pg_current_wal_lsn()')until_true 5434 learn "SELECT pg_last_wal_replay_lsn() >= '$lsn'"sql 5434 -c "SELECT pg_is_in_recovery(), workflow_name FROM ci.runs WHERE run_id = 1"sql 5434 -c "DELETE FROM ci.runs WHERE run_id = 1"-- A write on node1 is replayed on node2, which answers read-only queries pg_is_in_recovery | workflow_name-------------------+------------------ t | Replication test(1 row)
ERROR: cannot execute DELETE in a read-only transactionEl script espera hasta que node2 haya reproducido la posición que node1 había alcanzado tras el INSERT, y después lee. Sin esa espera, una lectura en un standby puede no ver una escritura recién confirmada en el primario: el retraso que la lección 3 describió para las Aurora Replicas.
En este punto, los dos clusters funcionan así: las escrituras van a node1, y node2 reproduce su WAL y responde a las consultas de solo lectura.
flowchart LR writes["escrituras"] --> node1["node1: primario, puerto 5433"] node1 -->|"WAL, en streaming por el slot node2"| node2["node2: hot standby, puerto 5434"] reads["consultas de solo lectura"] --> node2 delete["DELETE en node2"] -.->|"rechazado"| node2Replicación síncrona
Sección titulada «Replicación síncrona»Por defecto, un commit vuelve en cuanto el primario ha escrito su WAL. synchronous_standby_names hace que espere también a los standbys (líneas 59-73):
say "Synchronous replication: node1's commits now wait for node2 to confirm"sql 5433 -qc "ALTER SYSTEM SET synchronous_standby_names = 'FIRST 1 (node2)'" -c 'SELECT pg_reload_conf()' -o /dev/nulluntil_true 5433 learn "SELECT EXISTS (SELECT FROM pg_stat_replication WHERE sync_state = 'sync')"sql 5433 -c "SELECT application_name, sync_state FROM pg_stat_replication"pg_ctl -D node2 -m fast -w stop > /dev/nullecho "node2 stopped"# El commit se escribe localmente, y luego espera a un standby que no está. statement_timeout no cubre esa espera:# otra sesión la cancela, lo que termina la espera pero no el commitadd_run 5433 2 'Committed locally' > insert.log &until_true 5433 learn "SELECT EXISTS (SELECT FROM pg_stat_activity WHERE wait_event = 'SyncRep')"sql 5433 -c "SELECT wait_event_type, wait_event, pg_cancel_backend(pid) FROM pg_stat_activity WHERE wait_event = 'SyncRep'"waitcat insert.logsql 5433 -c "SELECT run_id, workflow_name FROM ci.runs WHERE run_id < 100 ORDER BY run_id"sql 5433 -qc "ALTER SYSTEM RESET synchronous_standby_names" -c 'SELECT pg_reload_conf()' -o /dev/null-- Synchronous replication: node1's commits now wait for node2 to confirm application_name | sync_state------------------+------------ node2 | sync(1 row)
node2 stopped wait_event_type | wait_event | pg_cancel_backend-----------------+------------+------------------- IPC | SyncRep | t(1 row)
WARNING: canceling wait for synchronous replication due to user requestDETAIL: The transaction has already committed locally, but might not have been replicated to the standby.INSERT 0 1 run_id | workflow_name--------+------------------- 1 | Replication test 2 | Committed locally(2 rows)FIRST 1 (node2)espera a un standby, el primero de la lista que esté conectado.sync_statepasa async.- Con
node2parado, elINSERTespera:pg_stat_activitylo muestra en el evento de esperaSyncRep, y esperaría para siempre. - El commit ya está en el WAL de node1. Cancelar la espera no lo deshace: el aviso lo dice, y la ejecución 2 está en la tabla. Un cliente que pierde su conexión durante esa espera no puede saber si su transacción se confirmó.
- Una primera versión de este script ponía
SET statement_timeout = '2s'antes delINSERT: elINSERTesperó igualmente, diez minutos, hasta que lo cancelé a mano. La espera viene después de la instrucción, en el commit: PostgreSQL desactiva el timeout de la instrucción antes de confirmar (postgres.c,finish_xact_command, enREL_18_6).
synchronous_commit fija a qué espera el primario: on, al vaciado a disco del standby; remote_write, a su escritura; remote_apply, a su reproducción, que «will cause each commit to wait until the current synchronous standbys report that they have replayed the transaction, making it visible to user queries» (la documentación). Se puede fijar por transacción, para las escrituras que no se deben perder.
Conmutación por error, y el commit perdido
Sección titulada «Conmutación por error, y el commit perdido»De nuevo con replicación asíncrona y node2 todavía parado, node1 confirma la ejecución 3 y se detiene. node2 arranca y se promueve con pg_promote() (líneas 75-85):
say "Asynchronous again, node2 still down: node1 commits a run that node2 will never receive"add_run 5433 3 'Lost in the failover' -qpg_ctl -D node1 -m fast -w stop > /dev/nullecho "node1 stopped"
say "Failover: node2 starts, is promoted, and becomes a primary on a new timeline"start node2 5434sql 5434 -c "SELECT pg_promote()"sql 5434 -c "SELECT pg_is_in_recovery(), substr(pg_walfile_name(pg_current_wal_lsn()), 1, 8) AS timeline"sql 5434 -c "SELECT run_id, workflow_name FROM ci.runs WHERE run_id < 100 ORDER BY run_id"add_run 5434 4 'After the failover' -q-- Asynchronous again, node2 still down: node1 commits a run that node2 will never receivenode1 stopped
-- Failover: node2 starts, is promoted, and becomes a primary on a new timeline pg_promote------------ t(1 row)
pg_is_in_recovery | timeline-------------------+---------- f | 00000002(1 row)
run_id | workflow_name--------+------------------ 1 | Replication test(1 row)node2es ahora un primario, en la timeline 2: los ocho primeros dígitos hexadecimales del nombre de un archivo WAL. Una nueva timeline empieza en cada promoción, para que el WAL escrito después no se pueda confundir con el WAL que el antiguo primario haya podido escribir.- Faltan las ejecuciones 2 y 3. La ejecución 2 se confirmó mientras
node2estaba parado, y la 3 también. Una conmutación por error a un standby asíncrono pierde lo que el standby no había recibido. - PostgreSQL no decide por sí mismo conmutar: «PostgreSQL does not provide the system software required to identify a failure on the primary and notify the standby database server» (conmutación por error). Herramientas externas a PostgreSQL vigilan el primario, promueven un standby y redirigen a los clientes; en Aurora, lo hace AWS.
Recuperar el antiguo primario con pg_rewind
Sección titulada «Recuperar el antiguo primario con pg_rewind»node1 tiene las ejecuciones 2 y 3 en sus archivos, en la timeline 1, después del punto en que node2 divergió. No puede seguir a node2 tal como está. pg_rewind copia desde node2 los bloques que han cambiado desde su último checkpoint común, en lugar de una copia completa nueva (líneas 87-94):
say "node1 can't simply follow node2: its WAL went further on the old timeline. pg_rewind takes it back."pg_rewind -D node1 --source-server='port=5434 dbname=postgres' -R 2>&1 | lsnsql 5434 -Atc "SELECT pg_create_physical_replication_slot('node1') IS NOT NULL" -o /dev/nullpg_ctl -D node1 -l node1.log -o "-p 5433 -c cluster_name=node1 -c primary_slot_name=node1" -w start > /dev/nulluntil_true 5434 learn "SELECT EXISTS (SELECT FROM pg_stat_replication WHERE application_name = 'node1' AND state = 'streaming')"lsn=$(psql -X -Atq -p 5434 -c 'SELECT pg_current_wal_lsn()')until_true 5433 learn "SELECT pg_last_wal_replay_lsn() >= '$lsn'"sql 5433 -c "SELECT pg_is_in_recovery(), run_id, workflow_name FROM ci.runs WHERE run_id < 100 ORDER BY run_id"-- node1 can't simply follow node2: its WAL went further on the old timeline. pg_rewind takes it back.pg_rewind: servers diverged at WAL location X/XXXXXXXX on timeline 1pg_rewind: rewinding from last common checkpoint at X/XXXXXXXX on timeline 1pg_rewind: Done! pg_is_in_recovery | run_id | workflow_name-------------------+--------+-------------------- t | 1 | Replication test t | 4 | After the failover(2 rows)pg_rewind«requires that the target server either has thewal_log_hintsoption enabled inpostgresql.confor data checksums enabled», y necesita el WAL del destino hasta el checkpoint común. Una primera versión del script falló concould not open file "node1/pg_wal/000000010000000000000002":node1ya había reciclado ese segmento.wal_keep_size = 128MBlo conserva.-Rescribestandby.signaly unprimary_conninfoque apunta anode2.node1vuelve como standby denode2, a través de un slot creado para él: las ejecuciones 2 y 3 han desaparecido, la ejecución 4 está.
Replicación lógica
Sección titulada «Replicación lógica»La replicación física copia un clúster entero, bloque a bloque, a servidores de la misma versión mayor. La replicación lógica envía filas: los cambios de las tablas elegidas, decodificados del WAL, a cualquier servidor PostgreSQL que tenga tablas equivalentes. node3 es un clúster nuevo e independiente (líneas 96-108):
say "Logical replication: node3, a separate cluster, subscribes to the runs table of node2"initdb -D node3 --auth=trust > /dev/nullecho "shared_buffers = 32MB" >> node3/postgresql.confstart node3 5435psql -X -q -p 5435 -c 'CREATE DATABASE learn'# La replicación lógica transporta filas, no el esquema: la tabla se crea primeropsql -X -q -p 5435 -d learn -c 'CREATE EXTENSION btree_gist'pg_dump -p 5434 -d learn --schema-only --schema=ci | psql -X -q -p 5435 -d learn > /dev/nullsql 5434 -c "CREATE PUBLICATION runs_on_main FOR TABLE ci.runs WHERE (head_branch = 'main')"sql 5435 -c "CREATE SUBSCRIPTION runs_from_node2 CONNECTION 'port=5434 dbname=learn' PUBLICATION runs_on_main"until_true 5435 learn "SELECT count(*) = 0 FROM pg_subscription_rel WHERE srsubstate <> 'r'"sql 5434 -c "SELECT head_branch = 'main' AS on_main, count(*) FROM ci.runs GROUP BY 1 ORDER BY 1"sql 5435 -c "SELECT head_branch = 'main' AS on_main, count(*) FROM ci.runs GROUP BY 1 ORDER BY 1"-- Logical replication: node3, a separate cluster, subscribes to the runs table of node2CREATE PUBLICATIONNOTICE: created replication slot "runs_from_node2" on publisherCREATE SUBSCRIPTION on_main | count---------+------- f | 2 t | 125(2 rows)
on_main | count---------+------- t | 125(1 row)- «The database schema and DDL commands are not replicated» (restricciones):
pg_dump --schema-onlycrea primero las tablas ennode3, que es también lo que sugiere la documentación. CREATE PUBLICATIONen el publicador enumera las tablas, aquí con un filtro de filas: solo las ejecuciones demain.CREATE SUBSCRIPTIONen el suscriptor crea un slot de replicación lógica en el publicador, copia las filas existentes, y después aplica los cambios a medida que llegan.pg_subscription_relmuestra el estado de cada tabla;rsignifica listo.- 125 ejecuciones de
mainennode3, y no las otras dos.
Tras el failover, pg_rewind y la suscripción, los tres clusters quedan conectados así: replicación física de node2 a node1, replicación lógica de node2 a node3.
flowchart LR node2["node2: primario, timeline 2"] node1["node1: standby, tras pg_rewind"] node3["node3: cluster independiente, suscripción runs_from_node2"] node2 -->|"física: WAL por el slot node1"| node1 node2 -->|"lógica: filas de ci.runs en main, publicación runs_on_main"| node3El suscriptor es un primario como cualquier otro, y nada impide escribir en él (líneas 110-119):
say "The subscriber is an ordinary primary: it accepts writes, which can conflict with replicated rows"add_run 5435 5 'Written on node3'add_run 5434 5 'Written on node2' -quntil_true 5435 learn "SELECT confl_insert_exists > 0 FROM pg_stat_subscription_stats WHERE subname = 'runs_from_node2'"sql 5435 -c "SELECT subname, apply_error_count > 0 AS apply_errors, confl_insert_exists > 0 AS insert_conflicts FROM pg_stat_subscription_stats"grep -o 'ERROR: conflict detected on relation "ci.runs": conflict=insert_exists' node3.log | head -1# Borrar la fila del suscriptor permite al apply worker, que reintenta, superar el conflictosql 5435 -qc "DELETE FROM ci.runs WHERE run_id = 5"until_true 5435 learn "SELECT EXISTS (SELECT FROM ci.runs WHERE run_id = 5)"sql 5435 -c "SELECT run_id, workflow_name FROM ci.runs WHERE run_id < 100 ORDER BY run_id"-- The subscriber is an ordinary primary: it accepts writes, which can conflict with replicated rowsINSERT 0 1 subname | apply_errors | insert_conflicts-----------------+--------------+------------------ runs_from_node2 | t | t(1 row)
ERROR: conflict detected on relation "ci.runs": conflict=insert_exists run_id | workflow_name--------+-------------------- 1 | Replication test 4 | After the failover 5 | Written on node2(3 rows)- La ejecución 5 se inserta en
node3, y después ennode2. Cuando llega la fila denode2, la clave ya existe: un conflictoinsert_exists. «In this case, an error will be raised until the conflict is resolved manually.» - PostgreSQL 18 cuenta los conflictos por tipo en
pg_stat_subscription_stats. - El apply worker se detiene en el error y vuelve a arrancar más tarde, fallando cada vez: la replicación de toda la suscripción queda bloqueada detrás de esa única fila. Borrar la fila de
node3la desbloquea, y llega la versión denode2. La documentación describe también cómo saltarse la transacción conALTER SUBSCRIPTION … SKIPy el LSN escrito en el log.
Dos novedades de PostgreSQL 17 completan el panorama, no ejecutadas aquí:
pg_createsubscriber«creates a new logical replica from a physical standby server», sin volver a copiar los datos.- Los slots lógicos pueden seguir una conmutación por error: una suscripción creada con
failover = true, ysync_replication_slotsen el standby, permiten a los suscriptores «resume replication from the new primary server after failover» (conmutación por error de la replicación lógica).
En Aurora
Sección titulada «En Aurora»Por verificar: nada de esta sección se ejecutó en AWS. Aurora no replica sus instancias como lo hace esta lección.
- Aurora Replicas. «The cluster volume is shared among all instances in your Aurora PostgreSQL DB cluster. Thus, no extra work is needed to replicate a copy of the data for each Aurora Replica» (replicación con Aurora PostgreSQL). No hay
pg_basebackupque ejecutar, ni slot que vigilar, y un clúster «can contain up to 15 Aurora Replicas» (replicación de Aurora), con un retraso «usually much less than 100 milliseconds». La lección 12 trata su conmutación por error. - Ningún standby físico externo. Las opciones de replicación que esa página enumera para Aurora PostgreSQL son las Aurora Replicas, Global Database, la replicación lógica, y un origen RDS for PostgreSQL hacia Aurora; un standby propio, alimentado por
pg_basebackupy streaming, no está entre ellas. No encontré ninguna página que lo diga con esas palabras. - La replicación lógica funciona como en esta lección, una vez activada: fija
rds.logical_replicationa 1 en un grupo de parámetros de clúster personalizado, y después «reboot the writer instance of your Aurora PostgreSQL DB cluster so that your changes takes effect» (configurar la replicación lógica). La misma página advierte que «leaving a logical replication slot inactive prevents the vacuum from removing obsolete tuples from tables». - Desde réplicas. «PostgreSQL 16 added support for logical decoding from read replicas. This feature isn’t supported on Aurora PostgreSQL» (replicación lógica en Aurora).
- pglogical también está disponible: «all currently available Aurora PostgreSQL versions support the pglogical extension» (pglogical).
- Construido sobre la replicación lógica. Los Blue/Green Deployments (lección 11) y las integraciones zero-ETL con Amazon Redshift, que hacen «transactional data available in your analytics destination after it is written to an Aurora DB cluster». La tabla de versiones de zero-ETL de AWS solo da Aurora PostgreSQL 16 y 17.
Puntos clave
Sección titulada «Puntos clave»- Un standby reproduce el WAL del primario;
pg_basebackup -Rcrea uno, y un slot conserva el WAL que aún necesita. - Los standbys responden a consultas de solo lectura, con un poco de retraso respecto al primario.
- Un commit síncrono se escribe primero localmente: una espera cancelada, o una conexión perdida, no indican que haya fallado.
statement_timeoutno cubre la espera. - Una conmutación por error a un standby asíncrono pierde los commits que no había recibido, y empieza una nueva timeline.
pg_rewindconvierte el antiguo primario en un standby, descartando sus commits divergentes; necesita checksums owal_log_hints, y el WAL hasta la divergencia.- La replicación lógica envía filas de las tablas elegidas, no el esquema, y un conflicto bloquea toda la suscripción hasta que alguien lo resuelve.
- PostgreSQL promueve, pero no decide cuándo: eso es trabajo de una herramienta, o de AWS en Aurora.
Ejercicios
Sección titulada «Ejercicios»Las soluciones están en ops/10-exercises.sh, que arranca node1 y dos standbys, node2 y node3 (líneas 20-31).
- Pausa la reproducción en
node2conpg_wal_replay_pause(), escribe ennode1, y muestra desdenode1qué standby va con retraso, en bytes de WAL. Después reanúdala.
Solución
say "Exercise 1: node2 pauses its replay; the lag, in bytes of WAL, grows on node1's side, then goes back to 0"sql 5434 -Atc 'SELECT pg_wal_replay_pause()' -o /dev/nulluntil_true 5434 learn "SELECT pg_get_wal_replay_pause_state() = 'paused'"sql 5433 -q -c "INSERT INTO notes SELECT i, repeat('x', 100) FROM generate_series(1, 1000) AS i"lag="SELECT application_name, pg_wal_lsn_diff(sent_lsn, replay_lsn) > 0 AS behind FROM pg_stat_replication ORDER BY 1"until_true 5433 learn "SELECT bool_and(sent_lsn = pg_current_wal_lsn()) AND bool_and(replay_lsn = sent_lsn) FILTER (WHERE application_name = 'node3') FROM pg_stat_replication"sql 5433 -c "$lag"sql 5434 -Atc 'SELECT pg_wal_replay_resume()' -o /dev/nulluntil_true 5433 learn "SELECT bool_and(replay_lsn = sent_lsn) FROM pg_stat_replication"sql 5433 -c "$lag"-- Exercise 1: node2 pauses its replay; the lag, in bytes of WAL, grows on node1's side, then goes back to 0 application_name | behind------------------+-------- node2 | t node3 | f(2 rows)
application_name | behind------------------+-------- node2 | f node3 | f(2 rows)pg_wal_lsn_diff resta dos posiciones del WAL, en bytes. sent_lsn ha avanzado para ambos standbys: node2 sigue recibiendo WAL mientras su reproducción está en pausa, así que sent_lsn - replay_lsn es el retraso de reproducción. El script imprime si es positivo, ya que el número exacto de bytes cambia de una ejecución a otra.
- Haz que los commits esperen a cualquiera de los dos standbys,
ANY 1 (node2, node3), y después deténnode2. ¿Sigue pasando un commit?
Solución
say "Exercise 2: ANY 1 (node2, node3): a commit waits for one of the two, so it goes through with node2 stopped"sql 5433 -qc "ALTER SYSTEM SET synchronous_standby_names = 'ANY 1 (node2, node3)'" -c 'SELECT pg_reload_conf()' -o /dev/nulluntil_true 5433 learn "SELECT count(*) = 2 FROM pg_stat_replication WHERE sync_state = 'quorum'"sql 5433 -c "SELECT application_name, sync_state FROM pg_stat_replication ORDER BY 1"pg_ctl -D node2 -m fast -w stop > /dev/nullsql 5433 -c "INSERT INTO notes VALUES (1001, 'one standby is enough')"sql 5433 -qc "ALTER SYSTEM RESET synchronous_standby_names" -c 'SELECT pg_reload_conf()' -o /dev/null-- Exercise 2: ANY 1 (node2, node3): a commit waits for one of the two, so it goes through with node2 stopped application_name | sync_state------------------+------------ node2 | quorum node3 | quorum(2 rows)
INSERT 0 1Con ANY, un quórum: ambos standbys son candidatos quorum, y basta con una confirmación. FIRST 1 (node2, node3) también habría continuado con node3, como primer standby conectado de la lista; la diferencia se ve cuando ambos están activos, donde FIRST espera siempre a node2, y ANY al que responda primero.
- Promueve
node3para que pase a ser independiente, y replica hacia él una tabla con una columna generada almacenada. Ennode3, la columna es una columna normal. ¿Qué recibe?
Solución
say "Exercise 3: node3 becomes a primary of its own, and subscribes to a table with a stored generated column"sql 5435 -Atc 'SELECT pg_promote()' -o /dev/nullsql 5433 -q -c "CREATE TABLE sizes (id int PRIMARY KEY, body text NOT NULL, length int GENERATED ALWAYS AS (length(body)) STORED)"# En node3 la columna es una columna normal: recibe los valores calculados en node1sql 5435 -q -c "CREATE TABLE sizes (id int PRIMARY KEY, body text NOT NULL, length int)"sql 5433 -c "INSERT INTO sizes (id, body) VALUES (1, 'partition'), (2, 'replication')"sql 5433 -c "CREATE PUBLICATION sizes_all FOR TABLE sizes WITH (publish_generated_columns = stored)"sql 5435 -c "CREATE SUBSCRIPTION sizes_from_node1 CONNECTION 'port=5433 dbname=learn' PUBLICATION sizes_all"until_true 5435 learn "SELECT count(*) = 2 FROM sizes"sql 5435 -c "SELECT * FROM sizes ORDER BY id"-- Exercise 3: node3 becomes a primary of its own, and subscribes to a table with a stored generated columnINSERT 0 2CREATE PUBLICATIONNOTICE: created replication slot "sizes_from_node1" on publisherCREATE SUBSCRIPTION id | body | length----+-------------+-------- 1 | partition | 9 2 | replication | 11(2 rows)node3 todavía tiene notes, copiada antes de su promoción, pero no sizes: crea la suya. publish_generated_columns = stored, nuevo en PostgreSQL 18, envía los valores calculados en node1. Por defecto, «generated columns are not published». Solo se pueden publicar las columnas almacenadas: PostgreSQL 18 también añadió las columnas generadas virtuales, el nuevo valor por defecto, que no se publican.
Fuentes
Sección titulada «Fuentes»- Documentación de PostgreSQL 18: WAL, alta disponibilidad y replicación, servidores standby por trasvase de registros, conmutación por error, ajustes de replicación, ajustes del WAL,
initdb,pg_ctl,pg_basebackup,pg_rewind, funciones de control de la recuperación, estadísticas de supervisión, replicación lógica, filtros de filas, columnas generadas, conflictos, restricciones, conmutación por error de los slots lógicos,CREATE PUBLICATION,CREATE SUBSCRIPTION,pg_createsubscriber, notas de versión de PostgreSQL 18, notas de versión de PostgreSQL 17 - SQL Server: modos de disponibilidad, réplicas secundarias legibles, modos de conmutación por error, trasvase de registros, replicación transaccional
- AWS: replicación de Aurora, replicación con Aurora PostgreSQL, replicación lógica, su configuración, pglogical, zero-ETL y sus versiones, todas consultadas el 2026-09-16