高可用定制——PostgreSQL在信创环境下的容灾
75. PostgreSQL流复制在信创环境下的部署:鲲鹏/龙芯/海光节点间复制
76. PostgreSQL同步复制定制:synchronous_standby_names的每一级含义
77. PostgreSQL逻辑复制定制:Publication/Subscription的内核实现和定制
78. PostgreSQL的自动故障切换:Patroni在国产OS上的部署
79. PostgreSQL的负载均衡:Pgpool–II的读写分离和连接负载均衡
80. PostgreSQL的备份策略:pg_basebackup、pgBackRest在信创环境下的实践
81. PostgreSQL的PITR恢复:从WAL归档恢复到任意时间点的完整流程
82. PostgreSQL的时间线切换:主从切换后的时间线管理和数据一致性
83. PostgreSQL的异地容灾:跨机房、跨城市的WAL归档和恢复
84. PostgreSQL的在线DDL:怎么做到加列、加索引不锁表
75. 流复制在信创环境下的部署
大白话:流复制就是主库把 WAL
日志像"流水"一样实时推给备库,备库一边收一边重放,让自己长得跟主库几乎一模一样。信创环境的差别只在 CPU
是鲲鹏(ARM)、龙芯(LoongArch)、海光(x86),这些都不影响流复制本身,影响的是"二进制怎么来"。
完整流程/代码:
主库 postgresql.conf:
conf
wal_level = replica
max_wal_senders = 10
wal_keep_size = 1GB
archive_mode = on
archive_command = 'cp %p /archive/%f'
主库 pg_hba.conf:
host replication repl 192.168.1.0/24 scram–sha–256
备库初始化并自动生成 primary_conninfo:
pg_basebackup –h <主库IP> –U repl –D /var/lib/pgsql/data \\
–Fp –Xs –P –R
–R 会在数据目录写一个 postgresql.auto.conf 里的 primary_conninfo。备库 postgresql.conf:
conf
hot_standby = on
启动后看状态:
— 主库看备库连接
SELECT application_name, state, sent_lsn, replay_lsn FROM pg_stat_replication;
— 备库看接收
SELECT * FROM pg_stat_wal_receiver;
为什么不是别的:逻辑也行,但它是"表级+逻辑行"复制,不复制系统目录、DDL、序列号,做的是数据同步不是容灾。物理流复制把整个
数据目录当字节流照搬,连 catalog 都一致,是容灾的正解。
trade–off:默认是异步的,主库崩溃会丢掉还没推完的 WAL。备库不能写。跨大版本不友好(要 pg_upgrade)。主备的页大小(默认
8KB)和端序必须一致——好消息是这三家CPU 都是小端,PG
数据文件格式跨架构一致,所以备份可以跨架构推送,但二进制包必须每架构各编一份,第三方扩展(pg_stat_statements、pgBackRest
附带的 lib)要在对应架构重编译。
如果重新设计:关键库用同步复制(见 76)保零丢失,其余用异步省钱;引入 Patroni(见 78)做自动切换,别纯手工 pg_basebackup。
—–
76. synchronous_standby_names 每一级的含义
大白话:这个参数回答"主库提交时,要等几个备库确认收到才给客户端回成功"。它是一个分层的配置:[FIRST|ANY] 个数
(名字列表)。
完整流程/代码:
conf
synchronous_standby_names = 'FIRST 1 (s1, s2, s3)'
三层拆开:
– FIRST / ANY: FIRST 按括号里的顺序,排前面的 N 个(或任意)确认才算;ANY 是"任意 N 个确认即可"(quorum 模式)。不写前缀默认
FIRST。
– 数字 N: 要等几个备库。
– 括号里的名字: 候选名单,* 表示"任何连上来的备库"。名字必须和备库 primary_conninfo 里的 application_name 对得上:
conf
# 备库
primary_conninfo = 'host=<主库IP> user=repl application_name=s1'
配合 synchronous_commit 控制"等到哪里":
– remote_apply(最严): 等备库重放完,备库能查到这条数据。
– on(默认): 等备库写入 WAL(flush 到 OS)。
– remote_write: 等备库收到但没落盘,延迟最低,但备库断电可能丢。
– local/off: 不等备库,退化成异步。
为什么不是别的:两个甚至多个库要保持一致,要么同步复制,要么 2PC(分布式事务,太重)。同步复制是单机内建、最轻的一致手段。
trade–off:每笔写多了至少一个网络往返,延迟涨。如果同步备库突然掉线,主库会卡住不提交(取决于 synchronous_commit
设置),可能直接把写堵死,所以生产上要么有多个备库兜底(ANY 2),要么配 primary_conninfo 的自动降级。
如果重新设计:关键业务用 synchronous_commit = remote_apply + ANY
2,把两个确认备库放在不同故障域(比如跨楼、跨机房),一个挂了还有一个能确认,既不丢也不卡。
—–
77. 逻辑复制的 Publication/Subscription 内核和定制
大白话:逻辑复制是"表级"复制。它不是搬数据块,而是把 WAL 里的一行行 insert/update/delete 用逻辑解码"翻译"成行,再发给订阅
端按顺序重放。Publication(发布)是主库声明"哪些表公开",Subscription(订阅)是对方说"我要哪个发布的哪张表、连到哪"。
完整流程/代码:
conf
# 主库
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
— 主库发布
CREATE PUBLICATION pub1 FOR TABLE public.t1, public.t2; — 或 FOR ALL TABLES
— 可选行过滤(PG15+)
CREATE PUBLICATION pub2 FOR TABLE t WHERE (region = 'cn');
— 订阅端
CREATE SUBSCRIPTION sub1
CONNECTION 'host=<主库IP> port=5432 dbname=postgres user=repl'
PUBLICATION pub1;
流程:订阅创建后先做初始表同步(copy 快照),然后逻辑解码持续应用。内部靠一个 output plugin(默认 pgoutput);订阅端有 apply
worker 进程顺序执行。
为什么不是别的:物理流复制不能跨大版本(比如 PG13 →
PG16),只能整库全复制。逻辑复制能跨版本、能选表、能按行过滤,适合做数据集成、多活、平滑升级。
trade–off:每行都被解码,开销比物理复制大;初始同步对大表很慢;DDL
不会自动跟着复制,表结构变了要手动补,不然订阅端会报错;不复制
sequence(所以主键可能撞号)、大对象等。逻辑复制不是容灾方案,是数据分发方案。
如果重新设计:物理复制做容灾兜底,逻辑复制做"跨版本迁移/数据分发"这类专用场景;用工具管 publication/subscription 和 DDL
同步(如 pglogical 或自研的 DDL 触发同步),别手写。
—–
78. Patroni 在国产 OS 上的部署
大白话:Patroni 是个用 Python 写的"高可用管家"。它不自己存状态,而是靠一个分布式配置库(DCS,常用
etcd/zookeeper/consul)来选主。谁在 DCS 里抢到 leader 键,谁就是主;主挂了,键的租约失效,其它节点抢键、变成新主,全程自动。
完整流程/代码:
1. 部署 3 个 etcd 实例(信创环境用离线包/自编译)。
2. 每台 PG 节点装 Patroni,写 patroni.yml。
3. 用 systemd 跑 Patroni。
4. Patroni 启动时注册到 DCS、做健康检查;新备库由 Patroni 自动 pg_basebackup 初始化。
patroni.yml 要点:
scope: pgcluster
namespace: /db/
name: node1
restapi:
listen: 0.0.0.0:8008
connect_address: 192.168.1.1:8008
etcd:
hosts: 192.168.1.10:2379,192.168.1.11:2379
bootstrap:
dcs:
postgresql:
use_pg_rewind: true
synchronous_mode: true
parameters:
wal_level: replica
pg_hba:
– host replication repl 192.168.1.0/24 scram–sha–256
postgresql:
listen: 0.0.0.0:5432
data_dir: /data/pgdata
parameters:
hot_standby: "on"
运维命令:patronictl list、patronictl switchover、patronictl failover。
为什么不是别的:repmgr 轻但功能弱,切换容易脑裂;自己写脚本很容易踩"两个主同时写"的坑。Patroni 是事实标准,和 etcd
的强一致选主配合,成熟。
trade–off:多了一个 DCS 组件要维护,需要 Python 环境(信创 ARM 上要装对应架构的 Python + psycopg2);切换 RTO
通常几十秒;脑裂防护靠 DCS 租约,网络分区时理论上可能"旧主还活着、新主已上位",所以要配合 fencing(比如 use_pg_rewind +
同步复制 + 每节点健康检查)。
如果重新设计:Patroni + etcd 三节点,synchronous_mode 打开,use_pg_rewind 打开;用 patronictl 做运维面,数据面用
Pgpool/PgBouncer 做连接入口。
—–
79. Pgpool–II 的读写分离与连接负载均衡
大白话:Pgpool–II 是你应用和数据库之间的一层代理。应用连它,它再连真正的
PG。它干两件事:把连接池化复用(省掉每次握手的开销),以及"读请求分发给备库、写请求发给主库"(读写分离 + 负载均衡)。
完整流程/代码 pgpool.conf:
conf
listen_addresses = '*'
port = 5432
backend_hostname0 = '192.168.1.1'
backend_port0 = 5432
backend_hostname1 = '192.168.1.2'
backend_port1 = 5432
load_balance_mode = on
connection_cache = on
health_check_period = 10
health_check_user = repl
读写判定的核心开关:
conf
statement_level_load_balance = on
# 白名单/黑名单函数,控制哪些函数调用路由到主库
white_function_list = 'nextval,setval,pg_advisory_lock'
为什么不是别的:单独要连接池可以用 PgBouncer,但它不做读写分离;单独要负载均衡可以上 HAProxy,但它不解析 SQL。Pgpool–II
一个组件把池化和 SQL 级路由都做了。
trade–off:Pgpool 自己成了单点,得再配 VIP + keepalived 做它自己的高可用。它判断"这是写还是读"是靠解析 SQL +
函数名单,没法百分百对——某些有副作用的函数或复杂查询可能被误判路由到备库。复杂SQL 和常见运维场景要谨慎配置。
如果重新设计:用 PgBouncer 专做连接池(稳、性能好),读写分离交给应用层按"读连接/写连接"区分,或用 Patroni
的读写路由;尽可能别让代理去猜 SQL 意图。
—–
80. 备份策略:pg_basebackup 与 pgBackRest
大白话:备份 = 一个全量基线 + 之后的 WAL 日志。pg_basebackup 是最简单的热备工具,直接拷走整个数据目录;pgBackRest
是更专业的一套,支持全量/差分/增量备份、WAL 归档托管、自动清理保留策略、压缩和加密。
完整流程/代码:
pg_basebackup:
pg_basebackup –h <主库IP> –U repl –D /backup/base \\
–Ft –z –Xs –P
pgBackRest,pgbackrest.conf:
conf
[global]
repo–path = /backup/repo
pg1–path = /var/lib/pgsql/data
主库 archive_command:
conf
archive_command = 'pgbackrest —stanza=main archive–push %p'
备份:
pgbackrest backup —stanza=main —type=full
pgbackrest backup —stanza=main —type=diff
pgbackrest backup —stanza=main —type=incr
为什么不是别的:pg_dump 是逻辑备份,适合导单库,不是物理容灾;crond + rsync
一遍遍拷没有增量差异、没有校验、没有自动清理。pgBackRest 提供完整生命周期,恢复点管理清晰。
trade–off:repo 一般要 2–3倍数据空间;增量恢复要从最近的基线开始重放,恢复时间随增量链长度增长;如果 WAL
归档跟不上写入,备份点与归档点之间会有缺口;备份本身吃 IO 和 CPU。
如果重新设计:全量 + 每周差分 + 每日增量,组成分层;WAL 归档推到对象存储(如
MinIO/OSS),异地留一份;定期在一台测试机上做真恢复演练,这是"备份是否可用"的唯一证明。
—–
81. PITR:从 WAL 归档恢复到任意时间点
大白话:PITR = 把库"倒带"到过去的某个瞬间。原理是:拿一个基础备份(全量基线),然后从基础备份之后开始,连续重放 WAL
日志,直到指定的停止时刻为止。
完整流程/代码:
conf
# PG12+ 放一个空文件 recovery.signal 触发恢复
restore_command = 'cp /backup/archive/%f %p'
recovery_target_time = '2026-09-09 12:34:00+08'
recovery_target_timeline = 'latest'
recovery_target_action = 'promote'
(GG < 12 用 recovery.conf)重启后,PG 重放 WAL,到目标时间点后按 recovery_target_action 做 pause / shutdown /
promote。目标也可以精确到 LSN 或 xid:recovery_target_lsn、recovery_target_xid。
为什么不是别的:MySQL 用 binlog 做类似的事,PG 用 WAL。它让"任何时间点恢复"成为内建能力。前提是 archive_mode = on
且归档完整,否则只能在 wal_keep_size 覆盖的窗口里恢复。
trade–off:恢复时间 = 重放全部归档 WAL 的时间,跟日志量成正比,可能几小时甚至更久;漏掉任何一段归档,断点之后全部失效;pause
后"继续重放"的语义有限,要精确操作;时间点很模糊时容易恢复错,需要 LSN/xid 精确指定。
如果重新设计:用 pgBackRest 的 —type=time / —target 简化配置;常做增量备份,把"要重放多少
WAL"缩短;把恢复动作写成脚本/Ansible,反复在测试库上演练,做到"点一下能拉起"。
—–
82. 时间线切换与数据一致性
大白话:每次"备库升级成主库",PG 都会产生一条新时间线(timeline),就像 git 开新分支。原因:旧主可能还存着没推完的
WAL,如果旧主复活并继续写,会和"新主写出来的 WAL"在相同 LSN 上有完全不同内容——不区分时间线的话,重放就会碎。所以每次
promotion,编号加一,并写一个 history 文件记录"几时、在哪分叉"。
完整流程:
– pg_ctl promote(或 SELECT pg_promote());timeline 从 1 变 2。
– 生成 00000002.history;后续 WAL 段名变成 000000020000...。
– 恢复/备库跟随用 recovery_target_timeline = 'latest'(或指一个具体数字/时间),PG 沿着 history 文件判断该走哪条时间线的
WAL。
– 当某节点比新主旧(比如原主被重新拉起),要走 pg_rewind 把它"拽回"新主当前时间线,否则 WAL 会冲突。pg_rewind 需要足够的
WAL,不够就得整体 pg_basebackup 重来。
为什么不是别的:这是 PG 内建的核心设计,不是可选项。没有时间线,系统无法判断"哪个分支的 WAL 是对的"。
trade–off:每次 failover 都多一条时间线,旧的 WAL 归档会持续累积,要有清理策略;旧节点要跟上新主,必须 rewind
或重做基线,这部分占用 RTO;跨多时间线恢复时,历史文件本身也要归档保留,否则无法回放之前的时间线。
如果重新设计:交给 Patroni(use_pg_rewind: true)自动处理 promotion + rewind;归档保留最近 N 个时间线的 WAL
供事后分析;把"旧主复活"做成受控流程,避免脑裂。
—–
83. 异地容灾:跨机房/跨城市
大白话:本地机房再高可用,也扛不住"整个机房没了"。异地容灾是再加一层:在另一个城市/机房放一份能切换的数据。两种做法:热一
点的是把流复制做过去(异地位备库),冷一点的是把 WAL 归档和全量备份同步过去。
完整流程/代码:
– 异地流复制:在主库 pg_hba 放行异地网络,备库 primary_conninfo 连主库,跨城一般走专线/VPN;因为延迟大,普遍用异步复制,延迟
可能几秒到几十秒,可接受——异地主要是防"整体丢失",不是"零丢失"。
– 异地备份:把 archive_command 指向对象存储/远端:
conf
archive_command = 'pgbackrest —stanza=main archive–push %p' # repo 在异地
或 rsync/对象存储客户端同步 WAL,再定期把全量备份推过去。
为什么不是别的:两地三中心(本地同步备 + 异地异步备 + 异地备份)是经典架构。同步复制跨城做不到——延迟几十ms
会让每笔写都堵住,所以异地只能用异步 + 灾备库。
trade–off:跨城延迟大,所以异地备库一定有数据丢失窗口(RPO 受网络带宽和延迟影响);专线带宽有限,写多的话 WAL
传输有压力;异地灾备的恢复演练复杂,还容易"只建不演"——平时不切,真要切时才发现问题。
如果重新设计:两地三中心;异地放"异步流复制备库"做热切换 + 独立一份"异地 WAL
归档/备份"做冷兜底,两条腿;异地备库平时兼做报表/只读查询,不闲置;按季度做一次跨城切换演练。
—–
84. 在线 DDL:加列、加索引不锁表
大白话:在线 DDL = 执行结构变更时,不让所有读写业务停下来等。PG 默认 ALTER TABLE 会拿 ACCESS EXCLUSIVE
锁(阻塞一切),但很多变更可以避开这个长锁。
完整流程/代码:
– 加列 + 常量默认值(PG11+不再重写整表):
ALTER TABLE t ADD COLUMN c int NOT NULL DEFAULT 0;
要求 DEFAULT 是非 volatile 的常量;如果默认值调用 volatile 函数(如 now() 之外的自定义 volatile 函数),PG
会退化成全表重写并锁表。
– 加索引不加锁:
CREATE INDEX CONCURRENTLY idx ON t(col);
(不能放在事务里执行,必须自动提交;中途失败会留下 invalid 索引,重跑一次即可。)
– 重建索引:
REINDEX INDEX CONCURRENTLY idx;
– 防呆超时:
SET lock_timeout = '2s';
ALTER TABLE ...;
– 治 bloat / 改类型:用 pg_repack:pg_repack –d db –t t,重建表不锁表(但开始/结束有短暂锁)。
为什么不是别的:别的路子是(1)开停机窗口,简单但业务不可用;(2)逻辑复制做"影子表迁移",慢且复杂;(3)用分区表按时间滚动,只适
合 append 型数据。内建的 concurrent + 常量默认值是改动最小、风险最低的。
trade–off:CONCURRENTLY 不能进事务、失败会留 invalid 索引;大表并发建索引会吃大量 CPU/IO,可能要并行 worker;对超大表单次
concurrent 也要很久;多个 DDL 并发要防锁竞争。pg_repack 需要额外磁盘放拷贝,且在开始/收尾各拿一次短暂的 ACCESS EXCLUSIVE
锁。
如果重新设计:业务表优先做成分区表,按时间滚动,天然少做 DDL;加列一律用常量默认值并给默认值加校验(确认非
volatile);长任务放低峰,配 lock_timeout 防误伤;定期
pg_repack;真要做到"永不停机"的结构变更,就用逻辑复制做渐进迁移(影子表 + 同步回填 + 切流),代价是要维护一套双写。



