由 WAL 流持续喂入、近乎实时的主库副本。

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" 脚本通过带恢复配置选项的物理基础备份建立副本,这个选项会写入备库连接信息并创建备库信号文件。副本在另一个套接字端口启动后,先请求暂停回放,再等待状态确实变成已经暂停。随后主库提交第二行,并捕获提交之后的日志位置,作为接收与回放比较的明确目标。 ...

八月 10, 2027 · 3 分钟 · XB Field Notes
Redis Sentinel 观察者协调故障转移并提供主节点地址。

Redis Sentinel 在主节点停止时提升新主节点

Redis Sentinel 监督非 Cluster 的主从部署,并提供当前主节点地址。它不存储应用数据集,也不代理应用命令。主节点失败后自动提升副本,可以帮助恢复写入可用性,但服务发现、副本数据新鲜度、客户端重连以及阻止旧写者继续写入,仍然是不同的问题,不能由“有哨兵”三个字一并保证。 故障认定与选举授权是两道门 一个 Sentinel 在足够长时间收不到可接受回复后,会把实例标记为主观下线;达到配置的 quorum 都认为主节点不可达,才形成客观下线判断。这个故障判定阈值不等于故障转移领导者获得授权所需的多数。选举还必须达到配置的 quorum,因此将 quorum 设置得更高,也可能进一步提高授权门槛。 例如五个已知 Sentinel 配置 quorum 为二,两个观察者可以同意主节点发生故障,但授权提升仍需要三个构成多数。反过来,quorum 为一也不意味着三个已知观察者只剩一个时,它能独自授权。SENTINEL CKQUORUM 会检查这两项可达性条件;某个 Redis 进程看上去健康,并不能证明观察者满足条件。 稳健部署至少需要三个处在独立故障范围内的 Sentinel。同一台笔记本上的三个进程能测试协议行为,却会随整台机器一起失效。每个 Sentinel 还需要可写的配置文件,持久保存发现的拓扑与纪元状态;这些不是随时可以丢弃的普通静态配置,也应保护其中可能存放的认证材料。 提升副本不会阻止所有旧连接写入 Redis 使用异步复制,被选中的副本可能没有收到旧主已经确认的一条写入。网络分区时,留在旧主一侧的客户端甚至可能继续向它写入,而另一侧已经提升替代节点。Sentinel 协调选举,并不等于撤销所有旧连接的写权限,更不会把 Redis 自动变成强一致存储。 min-replicas-to-write、min-replicas-max-lag 等设置可以在复制健康度下降时限制写入,同时牺牲部分可用性;它们不是通用的隔离旧写者协议。仍需独立备份和明确的应用恢复规则。网络分区、复制落后和单纯杀死一个进程应分别验证,不能用其中一个结果替代其他情形。 客户端需要重新发现并建立连接 支持 Sentinel 的客户端先查询观察者拿到主节点地址,再直接连接 Redis。因此,它要处理暂时发现不到主节点、连接断开和角色改变。只认旧地址的客户端,不会因为某个哨兵已经更新答案就自动恢复;同样,写请求超时也不能证明操作从未执行。 下面的代码在发现新的主节点后,明确断开应用的旧连接池,再检查新连接。这样能够单独验证重连环节,而不是承诺故障转移对调用方完全不可见。切换窗口内不能盲目重复非幂等写操作,节点提升并没有替业务提供去重机制。 ...

一月 9, 2027 · 3 分钟 · XB Field Notes