Lab 状态:实跑通过
切换、连接恢复与一致性检查通过
安全边界:本 Lab 只允许在一次性虚拟机、kind 或本地 Docker Compose 中进行。不要在生产数据库上执行
pg_ctl promote、STOP REPLICA、SET GLOBAL read_only=OFF或任何切换命令。真实故障转移涉及 RPO/RTO、连接入口、连接池、CDC 和业务写入方,必须按变更流程执行,并由业务方确认可丢失窗口。
准备 Docker 与 Docker Compose v2,预留约 1 GB 内存。实验使用两个本地容器、独立网络和临时数据卷,不映射数据库端口到宿主机,避免被其他进程误连。镜像标签可换成本地已缓存的主版本镜像,不要为了实验随意使用不明来源的镜像。
mkdir -p /tmp/lab-db/backup
docker version --format '{{.ServerVersion}}'
docker compose version
创建实验专用的认证配置和复制账号初始化脚本。这里的 trust 只用于隔离的 Docker 网络,生产环境必须使用强密码、TLS 和最小权限:
cat > /tmp/lab-db/pg_hba.conf <<'EOF'
local all all trust
host all all 0.0.0.0/0 scram-sha-256
host replication replicator 0.0.0.0/0 trust
EOF
cat > /tmp/lab-db/init-replication.sql <<'EOF'
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'labpass';
EOF
编写 Compose 文件。主库显式开启 WAL 归档所需的复制级别;从库先用 pg_basebackup 做基础备份,再由 -R 生成 standby 配置并启动:
cat > /tmp/lab-db/docker-compose.yml <<'EOF'
name: lab-db
services:
pg-primary:
image: postgres:16
container_name: lab-pg-primary
environment:
POSTGRES_USER: lab
POSTGRES_PASSWORD: labpass
POSTGRES_DB: app
command: >
postgres -c wal_level=replica -c max_wal_senders=5
-c hot_standby=on -c hba_file=/etc/postgresql/pg_hba.conf
volumes:
- pgprimary:/var/lib/postgresql/data
- ./pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
- ./init-replication.sql:/docker-entrypoint-initdb.d/10-replication.sql:ro
pg-replica:
image: postgres:16
container_name: lab-pg-replica
# PostgreSQL 拒绝以 root 运行;自定义 bash 命令不会触发镜像的重启降权
user: postgres
environment:
PGDATA: /var/lib/postgresql/replica
depends_on:
- pg-primary
command: >
bash -c "until pg_isready -h pg-primary -U lab; do sleep 1; done;
pg_basebackup -h pg-primary -U replicator -D /var/lib/postgresql/replica -Fp -Xs -R -P;
exec postgres -c hba_file=/etc/postgresql/pg_hba.conf -c hot_standby=on"
volumes:
- ./pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
volumes:
pgprimary:
networks:
default:
name: lab-db-net
EOF
timeout 240 docker compose -f /tmp/lab-db/docker-compose.yml up -d
timeout 180 bash -c 'until docker exec lab-pg-replica pg_isready -U lab -d app; do sleep 2; done'
确认主从关系、复制状态和从库只读。pg_is_in_recovery() 为 true 表示从库仍处于恢复模式,只能读不能写:
timeout 30 docker exec lab-pg-primary psql -U lab -d app -c \
"SELECT application_name, state, sync_state, sent_lsn, replay_lsn, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"SELECT pg_is_in_recovery(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;"
创建业务表、写入基线数据,并做一次可验证的逻辑备份。备份文件要复制到容器外,且用 pg_restore --list 验证可读:
timeout 30 docker exec lab-pg-primary psql -U lab -d app -c \
"CREATE TABLE accounts (id bigserial PRIMARY KEY, owner text NOT NULL, balance numeric(12,2) NOT NULL);"
timeout 30 docker exec lab-pg-primary psql -U lab -d app -c \
"INSERT INTO accounts (owner, balance) VALUES ('alice', 100), ('bob', 200);"
timeout 30 docker exec lab-pg-primary psql -U lab -d app -Atc \
"SELECT count(*), coalesce(sum(balance),0), max(id) FROM accounts;" \
| tee /tmp/lab-db/backup/baseline.txt
timeout 30 docker exec lab-pg-primary psql -U lab -d app -Atc \
"SELECT pg_current_wal_lsn();" | tee /tmp/lab-db/backup/lsn-before.txt
timeout 120 docker exec lab-pg-primary pg_dump -U lab -d app -Fc -f /tmp/app-before.dump
timeout 60 docker cp lab-pg-primary:/tmp/app-before.dump /tmp/lab-db/backup/app-before.dump
timeout 60 docker exec lab-pg-primary pg_restore --list /tmp/app-before.dump | head
等待从库追平后,用从库上的查询结果与主库基线对比。复制延迟为 0 只代表采样时刻没有延迟,不代表故障时不会丢失最后一段事务。
timeout 30 docker exec lab-pg-replica psql -U lab -d app -Atc \
"SELECT count(*), coalesce(sum(balance),0), max(id) FROM accounts;"
把主库容器停掉,模拟主库进程不可用。docker stop 是受控停止,不等同于断电;真实宕机可能丢失尚未发送或尚未回放的事务,因此故障转移必须明确 RPO:
timeout 60 docker stop lab-pg-primary
timeout 30 docker ps -a --filter name=lab-pg-primary --format '{{.Names}} {{.Status}}'
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"SELECT pg_is_in_recovery(), pg_last_wal_replay_lsn();"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"INSERT INTO accounts (owner, balance) VALUES ('blocked', 1);"; echo "write_before_promote_exit=$?"
提升前从库应保持只读,写入会失败。确认旧主已经停止、不会被应用继续写入后,再提升从库。提升命令必须使用容器内实际的数据目录,并加超时:
timeout 60 docker exec -u postgres lab-pg-replica pg_ctl promote -D /var/lib/postgresql/replica
sleep 3
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"SELECT pg_is_in_recovery(), pg_current_wal_lsn();"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"INSERT INTO accounts (owner, balance) VALUES ('carol', 50);"
提升成功后,新主库 pg_is_in_recovery() 返回 false,写入成功。此时旧主仍处于停止状态,绝不能直接 docker start 后把它接回读写入口;那会形成两个可写主库。
主库停止后,从库的 WAL receiver 会断开,日志中出现连接失败或复制中断;从库仍可接受只读查询,但写入报 cannot execute INSERT in a read-only transaction,这是热备的正常行为,不是从库损坏。提升后,从库退出恢复模式,时间线推进,新写入可以提交;旧主容器仍是 Exited 状态,没有被应用连接。
故障转移成功的判据是:旧主已隔离;新主 pg_is_in_recovery() 为 false 且能提交写入;新主 WAL 位置持续增长;剩余从库能重新指向新主并追平;应用完成写入、读取和事务回滚验证;行数、汇总值和最大 ID 与切换前基线及故障后写入量能对上。
失败的判据同样要提前写死:提升命令失败或新主仍为只读;旧主仍在接受写入;两个节点同时可写;从库复制中断且无法追平;应用仍连旧主;关键表数据缺失、重复或约束失效;GTID/LSN 出现无法解释的缺口。任意一条成立都应停止流量切换,先隔离写入方。
pg_is_in_recovery()、pg_stat_replication、pg_stat_wal_receiver 和 pg_last_wal_replay_lsn() 能区分主库、从库、复制中断和回放延迟;不要只看容器是否 Running。pg_is_in_recovery() 为 false 就以为应用已经切换;复制延迟为 0 就以为没有数据丢失;docker start 旧主就以为完成了回切。SHOW REPLICA STATUS、read_only/super_read_only、GTID 和 CHANGE REPLICATION SOURCE TO。不要把 PostgreSQL 的 LSN、复制槽命令与 MySQL 的 GTID 命令混用;字段名和语法随版本变化,以官方文档为准。先区分两种情形。若新主还没有接受任何业务写入,回滚相对简单:停止新主,重新启动旧主,确认旧主数据完整后再恢复原入口,然后把新主按从库重建。若新主已经接受写入,就不能直接把旧主拉起来当主库,否则会形成双主和数据分叉。
下面演示“已发生写入后”的向前恢复:把旧主重建为指向新主的从库。旧容器先保留,不删除其数据卷,便于回滚和取证;新容器使用新的临时数据目录,通过基础备份重新加入:
timeout 30 docker rm -f lab-pg-old-rebuilt 2>/dev/null || true
timeout 240 docker run -d --name lab-pg-old-rebuilt --network lab-db-net \
--user postgres \
-e PGDATA=/var/lib/postgresql/replica \
-v /tmp/lab-db/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro \
postgres:16 \
bash -c "until pg_isready -h lab-pg-replica -U lab; do sleep 1; done;
pg_basebackup -h lab-pg-replica -U replicator -D /var/lib/postgresql/replica -Fp -Xs -R -P;
exec postgres -c hba_file=/etc/postgresql/pg_hba.conf -c hot_standby=on"
重建容器需要先完成 pg_basebackup 才会启动 PostgreSQL;pg_isready 只代表实例可连接,pg_is_in_recovery() 才代表已进入恢复模式。查询前先等实例就绪:
timeout 180 bash -c 'until docker exec lab-pg-old-rebuilt pg_isready -U lab -d app; do sleep 2; done'
确认旧主已作为从库运行,并在新主上看到复制连接:
timeout 30 docker exec lab-pg-old-rebuilt psql -U lab -d app -c \
"SELECT pg_is_in_recovery();"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"SELECT application_name, state, sent_lsn, replay_lsn, replay_lag FROM pg_stat_replication;"
如果提升失败或新主不可用,停止新主并从剩余从库中选择数据最完整的一台重新提升;不要回到旧主直接写入。如果出现双写,立即停止一侧写入,比较 LSN、WAL 和业务数据,确定唯一权威主库;另一侧必须重建,不能靠手工合并猜测。若数据损坏且逻辑备份可用,可在独立实例上用 pg_restore 恢复,再按业务确认的时间点补数据;需要精确到时间点时使用 WAL 归档和 PITR,参考 runbook-postgres-pitr。
回切应在业务稳定、复制追平、数据核对完成后按计划执行,不要在新主刚接管时立即回切。回切步骤与切换对称:冻结写入、确认追平、摘流量、提升原主、切换入口、重建从库。
实验结束只清理本实验的容器、网络和卷,不要对任何生产容器执行 down -v:
timeout 60 docker rm -f lab-pg-old-rebuilt 2>/dev/null || true
timeout 120 docker compose -f /tmp/lab-db/docker-compose.yml down -v --remove-orphans
rm -rf /tmp/lab-db
恢复后的数据一致性检查包括:在新主和重建从库上分别统计行数、金额汇总、最大 ID 和重复 ID;确认故障前基线加上故障后成功写入能够解释当前结果;检查主键、唯一约束和外键没有被破坏;在新主写入一条带唯一标记的记录,等待复制后在从库读到;确认旧主没有保留故障后的冲突写入;记录可能丢失的事务范围和 RPO 违约情况。不要用“复制正常”掩盖已经丢失的数据。
最终验证要同时覆盖数据库角色、复制、应用和数据四个层面:
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"SELECT pg_is_in_recovery(), pg_current_wal_lsn();"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"SELECT application_name, state, sent_lsn, replay_lsn, replay_lag FROM pg_stat_replication;"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -Atc \
"SELECT count(*), coalesce(sum(balance),0), max(id) FROM accounts;"
timeout 30 docker exec lab-pg-old-rebuilt psql -U lab -d app -Atc \
"SELECT count(*), coalesce(sum(balance),0), max(id) FROM accounts;"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c \
"INSERT INTO accounts (owner, balance) VALUES ('verify', 1) RETURNING id;"
sleep 2
timeout 30 docker exec lab-pg-old-rebuilt psql -U lab -d app -Atc \
"SELECT count(*) FROM accounts WHERE owner = 'verify';"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -Atc \
"SELECT count(*) FROM (SELECT id FROM accounts GROUP BY id HAVING count(*) > 1) AS dup;"
pg_is_in_recovery() 为 false,能持续接受写入,WAL 位置增长。pg_is_in_recovery() 为 true,复制状态为 streaming,延迟在阈值内。pg_restore --list 可用,回切材料和旧主重建步骤已验证。pg_rewind 重建为从库,不能直接启动后接回读写入口。
小程序版
微信扫码,随时查命令