Postgres 流式复制让备用节点保持温热
物理流复制把 PostgreSQL WAL 发送到备库,备库通过重放记录维持集群的另一份副本。发送不必等待整个日志段结束。这与选择部分表变更进行传送的逻辑复制不同。接收进程正在运行、备库可以连接,都是有价值的健康信号,但任何一个信号都不能单独证明某条已经提交的业务记录此刻在备库可见。 坏掉的样子 流复制默认是异步的。主库可以先向客户端确认提交,备库之后才收到或应用相关记录。若主库恰好在这个窗口丢失,切换到备库时可能丢掉最近已经确认的工作。备库也可能保持连接并持续接收日志,却由于暂停回放而一直返回旧行。因此网络交付在推进,并不能证明查询新鲜度也在推进。 应区分不同进度标记。备库的接收位置函数报告通过流复制已经接收并同步到磁盘的位置,回放位置函数则报告已经应用的位置,两者差值可以暴露回放积压。最后一次重放事务的时间戳,即使在完全空闲的系统上也会不断变旧;单纯用当前时间减去它,不能可靠判断究竟有多少尚未处理的工作。 快速验证 使用非超级操作系统账户,在 Linux 环境中准备 PostgreSQL 18 服务端与客户端程序。本脚本已经在 PostgreSQL 18.6 上执行。它创建私有临时集群及 Unix 套接字目录,不打开网络监听。本地信任认证仅用于这些可丢弃的实验实例。少量日志预留支持这个有界演练,脚本没有配置永久复制槽。 #!/bin/sh set -eu umask 077 lab=$(mktemp -d /tmp/xbcatm-streaming.XXXXXX) mkdir "$lab/socket" primary=$lab/primary standby=$lab/standby cleanup() { for cluster in "$standby" "$primary"; do if test -f "$cluster/postmaster.pid"; then pg_ctl -D "$cluster" -m fast -w stop >/dev/null fi done } trap cleanup EXIT initdb -D "$primary" --no-locale --encoding=UTF8 --auth-local=trust --auth-host=reject >/dev/null cat >> "$primary/postgresql.conf" <<CONF listen_addresses = '' unix_socket_directories = '$lab/socket' port = 56432 wal_level = replica max_wal_senders = 4 wal_keep_size = '64MB' CONF pg_ctl -D "$primary" -l "$lab/primary.log" -w -t 15 start >/dev/null export PGHOST=$lab/socket PGPORT=56432 PGDATABASE=postgres PGUSER=$(id -un) export PGCONNECT_TIMEOUT=2 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' CREATE TABLE marker (id integer PRIMARY KEY,value text); INSERT INTO marker VALUES (1,'base'); SQL pg_basebackup -D "$standby" -R -X stream -c fast -w cat >> "$standby/postgresql.conf" <<CONF port = 56433 hot_standby = on CONF pg_ctl -D "$standby" -l "$lab/standby.log" -w -t 15 start >/dev/null PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT pg_wal_replay_pause()' >/dev/null attempt=0 while test "$(PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT pg_get_wal_replay_pause_state()='paused'")" != t; do attempt=$((attempt+1)) test "$attempt" -lt 100 || { echo 'replay did not pause' >&2; exit 1; } sleep 0.1 done psql -X -v ON_ERROR_STOP=1 -qAt -c "INSERT INTO marker VALUES (2,'later')" target=$(psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT pg_current_wal_lsn()') attempt=0 while test "$(PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT pg_last_wal_receive_lsn()>='$target'::pg_lsn")" != t; do attempt=$((attempt+1)) test "$attempt" -lt 150 || { echo 'receiver did not catch up' >&2; exit 1; } sleep 0.1 done psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT state,sync_state FROM pg_stat_replication' PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<SQL SELECT pg_last_wal_receive_lsn()>='$target'::pg_lsn, pg_last_wal_replay_lsn()<'$target'::pg_lsn, (SELECT count(*) FROM marker); DO \$\$ BEGIN IF (pg_last_wal_receive_lsn()>='$target'::pg_lsn AND pg_last_wal_replay_lsn()<'$target'::pg_lsn AND (SELECT string_agg(id||':'||value,',' ORDER BY id) FROM marker)='1:base') IS NOT TRUE THEN RAISE EXCEPTION 'paused replay result mismatch'; END IF; END \$\$; SQL echo stale_read_while_receiving PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT pg_wal_replay_resume()' >/dev/null attempt=0 while test "$(PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT pg_last_wal_replay_lsn()>='$target'::pg_lsn")" != t; do attempt=$((attempt+1)) test "$attempt" -lt 150 || { echo 'replay did not catch up' >&2; exit 1; } sleep 0.1 done PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' SELECT id,value FROM marker ORDER BY id; DO $$ BEGIN IF (SELECT string_agg(id||':'||value,',' ORDER BY id) FROM marker) IS DISTINCT FROM '1:base,2:later' THEN RAISE EXCEPTION 'resumed replay result mismatch'; END IF; END $$; SQL pg_ctl -D "$primary" -m fast -w stop >/dev/null echo old_primary_stopped pg_ctl -D "$standby" -w -t 15 promote >/dev/null PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' SELECT pg_is_in_recovery(); INSERT INTO marker VALUES (3,'promoted'); SELECT id,value FROM marker ORDER BY id; DO $$ BEGIN IF pg_is_in_recovery() OR (SELECT count(*) FROM marker)<>3 THEN RAISE EXCEPTION 'promotion result mismatch'; END IF; END $$; SQL printf 'lab_directory %s\n' "$lab" 脚本通过带恢复配置选项的物理基础备份建立副本,这个选项会写入备库连接信息并创建备库信号文件。副本在另一个套接字端口启动后,先请求暂停回放,再等待状态确实变成已经暂停。随后主库提交第二行,并捕获提交之后的日志位置,作为接收与回放比较的明确目标。 ...