☰
PostgreSQL流复制与pgpool-II高可用故障切换避坑
2026/10/27 2:50:37 网站建设 项目流程

简介:这份文档围绕 PostgreSQL 主从流复制与 pgpool-II 高可用架构展开,面向数据库管理员、运维工程师及需要落地主备容灾与读写分离的后端团队。内容从方案综述切入,对比同步复制与异步复制在一致性、写入性能上的取舍,并说明 pgpool-II 在连接池、故障切换、负载均衡、读写分离及连接数限制方面的作用,同时涉及 pg_rman 备份、环境规划、主备初始化、服务启动与故障模拟验证等环节。压缩包共 1 个 docx 文件,大小约 773KB,按“方案介绍—实践环境—客户环境准备—配置步骤”等章节组织,目录层级清晰,便于按部署阶段查阅。已有 470 人学习,适合作为搭建 PostgreSQL 高可用环境、梳理主备复制链路与 pgpool 配置思路的参考文档,也可用于对照检查网络、hosts、时钟同步、sysctl 与资源限制等基础准备项。

1. 为什么这套「主从 + 中间件」的组合值得拆开看

很多人第一次把 PostgreSQL 主从配通,会默认流复制是「把主库的数据拷到备库」。这个直觉是错的,而且是后面所有误判的源头。备库上的数据不是被搬过去的,是它自己按日志重算出来的:主库只发 WAL 记录,备库的 startup 进程一条条重放,最终落到数据页上。同一套机制上面再叠一层 pgpool-II,难点立刻从「怎么配置」变成「谁来判断主库死没死、判断错了怎么收场」。这两件事决定了整套高可用方案在真实故障面前是兜底还是添乱。搞懂 WAL 是怎么从 walsender 流到 walreceiver、再被重放的,以及 pgpool 凭什么认为某个节点该被踢出集群,比记住几十行配置项有用得多。

2. WAL 到备库:一条日志的三段旅程

2.1 walsender 与 walreceiver:传的不是数据页,是日志流

要理解流复制,先得接受一个前提:PostgreSQL 里所有数据变更在落盘前,都会先写一份 WAL 记录。这些记录按 16MB 一个段文件切分,堆在数据目录的 pg_wal(老版本叫 pg_xlog)下面。写 WAL 的是 walwriter 进程,它周期性把 WAL 缓冲刷到磁盘。数据页本身反而可以滞后很久才刷——这就是所谓的 WAL 先行,崩溃恢复时靠重放 WAL 把不一致的数据页推到一致状态。

流复制做的事情非常朴素:主库多起一个 walsender 进程,专门读 WAL 文件,通过 TCP 把日志流推给备库的 walreceiver 进程;walreceiver 收到后直接写本地磁盘,同时备库的 startup 进程(日志里常见的startup或checkpointer相关进程)不停地重放这些 WAL,把本地数据页改造成和主库一致的样子。整条链路上没有一个环节在传输「表数据」,传输的只有日志。

这个设计带来两个直接后果,值得记住。第一,备库的延迟可以用 LSN 精确度量:主库当前写到哪个位置、备库重放到哪个位置,两个 LSN 相减就是落后的字节数,不存在「大概同步了」这种模糊状态。第二,只要 WAL 没丢,备库就一定能追上;反过来,如果主库在 WAL 还没发出去的时候就崩了,这部分日志就真的没了,备库也不可能凭空变出来。

主库侧需要放开几个开关,否则 walsender 根本不会启动:

# postgresql.conf —— 主库侧最小改动 wal_level = replica # 至少 replica,才会有足够的 WAL 信息供备库重放 max_wal_senders = 10 # 允许同时存在的 walsender 进程数,一个备库占一个 wal_keep_segments = 128 # 12 之前:主库保留的 WAL 段数,防止备库落后太多被删 # wal_keep_size = 1GB # 13 之后:改成按大小保留,语义更直观

wal_level决定 WAL 里记录多少信息,minimal级别连备库需要的信息都不够,必须是replica;max_wal_senders是并发推送进程的上限,配成 0 等于彻底关掉流复制;wal_keep_segments(新版本是wal_keep_size)是给落后的备库留的缓冲,如果备库断线时间超过这个窗口,主库已经把旧 WAL 回收了,备库再连上来就会报「requested WAL segment has already been removed」,只能重做基础备份。这个参数配小了是血泪经验里最常见的一类翻车。

还有一个常被忽略的访问控制文件:

# pg_hba.conf —— 允许复制连接,注意最后一段必须是 replication # TYPE DATABASE USER ADDRESS METHOD host replication repl 192.168.10.0/24 md5 host all all 192.168.10.0/24 md5

replication是一个伪数据库名,专门用于复制连接。如果只写了all而没写replication,主库日志里就会出现no pg_hba.conf entry for replication connection,备库侧看到的现象则是连接被拒、复制起不来。

2.2 同步复制的代价:synchronous_standby_names 卡在哪一步

默认情况下流复制是异步的:主库提交事务时只保证自己的 WAL 写进了本地磁盘,然后立刻返回成功,压根不等备库。这时候如果主库断电,已经提交但还没发出去的事务就丢了。这个窗口通常很小,但不是零。

同步复制就是把这个窗口关掉。配置只需要两项:

# 主库 postgresql.conf synchronous_commit = on # 任意一个备库确认即可提交;括号里是 application_name,不是主机名 synchronous_standby_names = 'ANY 1 (node_a, node_b)'

synchronous_standby_names的语法在 9.6 之后扩展出了FIRST和ANY两种模式。FIRST 1 (node_a, node_b)表示按顺序优先等 node_a,node_a 不在才等 node_b;ANY 1 (node_a, node_b)表示谁先回 ack 就算谁,延迟更低,适合多个备库分担。老版本只支持写一串名字,语义等价于FIRST。

关键在于「等什么」。事务提交时,主库会阻塞在 WAL flush 之后,等满足条件的备库回一个确认说「这段 LSN 我已经收到并落盘了」,才把提交结果返回给客户端。所以同步复制增加的不是网络往返,而是最慢那个备库的磁盘刷写时间。备库磁盘一旦抖动,主库的写事务就会跟着抖——这就是为什么很多团队一开始开着同步,上线后被业务投诉写入变慢,最后又退回去。

监控同步状态看pg_stat_replication:

-- 主库上执行,看每个备库的同步状态和延迟 SELECT application_name, state, sync_state, sent_lsn, write_lsn, flush_lsn, replay_lsn, write_lag, flush_lag, replay_lag FROM pg_stat_replication;

sync_state里sync表示当前是同步备库、potential表示候补、async表示纯异步。write_lsn是备库收到并写入内核缓冲的位置,flush_lsn是刷到磁盘的位置,replay_lsn是真正重放完成的位置——同步复制等的是flush_lsn,不是replay_lsn。这个区别很关键:备库已经落盘但还没来得及重放,主库是可以提交的,此时查备库会看到旧数据,这是正常现象,不是复制坏了。

注意:主从节点的操作系统、PostgreSQL 大版本、时区、字符集最好保持一致。版本不一致在物理流复制下基本没法用,因为 WAL 的二进制格式依赖页结构,跨大版本不兼容。

2.3 归档传送 vs 流复制:一个 WAL 文件的差距

把 WAL 送到备库其实有两种方式,流复制只是其中一种。

第一种是 WAL 归档传送,也就是常说的 base-file 方式:主库把写满的 WAL 段文件通过归档命令(archive_command)拷到一个共享位置,备库再去取、去重放。问题在于「写满」这个前提——一个 WAL 段 16MB,业务量小的时候可能要几十分钟才写满,所以备库永远落后主库至少一个完整的 WAL 文件。用这种方式做主备,延迟是以十分钟甚至小时计的。

流复制绕开了这个限制:walsender 不需要等段文件写满,WAL 记录一产生就可以推走,粒度是记录级别而不是文件级别。这让延迟从「一个 WAL 文件的生成时间」压缩到「一次网络往返 + 一次磁盘刷写」。

两种方式并不互斥,常见的实现思路是同时开:流复制负责实时同步,归档负责兜底——万一备库落后太多、主库已经把旧 WAL 删了,还能从归档目录里把缺的段捞回来补上。归档目录同时也是 PITR(基于时间点恢复)的基础,这部分在后面讲备份时还会碰到。

归档配置的坑主要在 archive_command 的返回值上:

# 只有返回 0 才算成功,返回非 0 主库会不停重试并堆积 WAL archive_mode = on archive_command = 'test ! -f /data/pg_archive/%f && cp %p /data/pg_archive/%f'

%p是 WAL 文件的完整路径,%f是文件名。命令里那句test ! -f是为了幂等——如果目标文件已存在就直接跳过,避免重复拷贝返回非 0 导致主库卡在重试循环里,pg_wal 目录越堆越大,最后把磁盘撑爆。这个坑的典型表现是磁盘告警,但根因在归档命令。

3. 版本分水岭:recovery.conf 消失之后备库怎么「认路」

3.1 12.x 之前:recovery.conf 与 recovery.done 的状态机

在 PostgreSQL 12 之前,一个实例是主库还是备库,由数据目录下有没有 recovery.conf 这个文件决定。文件存在,启动时就走恢复模式,进入备库状态;文件不存在,就当主库启动。

# $PGDATA/recovery.conf —— 12 之前备库的核心配置 standby_mode = 'on' primary_conninfo = 'host=192.168.10.11 port=5432 user=repl password=xxxx application_name=node_b' trigger_file = '/tmp/pg_promote_trigger' restore_command = 'cp /data/pg_archive/%f %p'

standby_mode = 'on'表示这是个持续接收 WAL 的备库;primary_conninfo是上游连接串,里面的application_name会影响主库synchronous_standby_names的匹配;trigger_file是「晋升开关」——手动在这个路径 touch 一个文件,备库就会停止恢复、切换成可写的主库。

这套机制有个很别扭的地方:实例启动成功后,PostgreSQL 会把 recovery.conf 重命名成 recovery.done。所以如果配置写错了想改,会发现文件不见了,得先把 recovery.done 改回去再重启。这个「状态机式的文件改名」是 12 之前运维翻车的经典来源,尤其是自动化脚本里反复启动失败的时候,文件状态会变得很难判断。

3.2 12.x 之后:standby.signal 与 postgresql.conf 的合并

12 版本把恢复配置整个合并进了 postgresql.conf,只留下一个空的信号文件作为开关。

# 12 之后:信号文件只是个标记,内容为空 touch $PGDATA/standby.signal # 对应的参数写进 postgresql.conf
# postgresql.conf —— 12 之后的备库配置 primary_conninfo = 'host=192.168.10.11 port=5432 user=repl password=xxxx application_name=node_b' restore_command = 'cp /data/pg_archive/%f %p' # 注意:standby_mode 这个参数被彻底移除了,写了会启动报错

这个改动的好处很实际:参数进了 postgresql.conf,就能用ALTER SYSTEM SET动态设置,也能用pg_basebackup -R自动生成,不用再跟文件改名打交道。晋升的方式也变了——不再是 touch 触发文件,而是删掉 standby.signal 然后重启(或者用pg_ctl promote做在线晋升)。

升级路径上最典型的坑就在这里:把 12 之前的数据目录直接升到 12,recovery.conf 里的参数不会自动迁移,如果没手动搬进 postgresql.conf,实例会以主库身份启动,然后把备库当成一个可写的独立库跑了——两边数据彻底分叉。

3.3 pg_basebackup 与 PCP 命令在搭建期的角色

搭备库的第一步不是改配置,而是把主库的数据目录完整复制一份过来。这个动作由 pg_basebackup 完成,它底层走的就是复制协议,和流复制共用同一套连接权限。

# 在备库上执行,-R 是 12 之后的关键参数 pg_basebackup \ -h 192.168.10.11 \ -p 5432 \ -U repl \ -D /data/pgdata \ -Fp \ # 普通文件格式输出,方便直接当数据目录用 -Xs \ # 流式传输 WAL,边备份边收,避免备份期间 WAL 被删 -P \ # 显示进度 -R # 自动生成 standby.signal 和 primary_conninfo

-Xs和-R这两个参数决定了备份能不能直接用。-Xs表示用流复制协议同步传输备份期间的 WAL,如果退化成-Xf(归档方式),备份期间主库必须配好归档,否则备份会失败并提示需要 WAL。-R在 12 之后会自动写好 standby.signal 和 primary_conninfo 两样东西,省掉手工配置;12 之前对应的是自动生成 recovery.conf,参数名一样。

搭好之后,pgpool 一侧通过 PCP 协议管理节点状态,pcp_node_info查节点健康度,pcp_attach_node把节点重新拉回集群,pcp_detach_node把节点踢出去。这些命令不直接改数据库,改的是 pgpool 内部维护的一份节点状态表。理解这一点很重要:pgpool 认为节点是死的,不代表数据库真的死了,反过来也一样。

4. pgpool-II 的四件事:连接池、读写分离、健康检查与故障切换

4.1 连接池复用的是连接,不是会话

pgpool-II 站在客户端和 PostgreSQL 之间,最先做的事是把客户端连接和后端数据库连接解耦。客户端连上来,pgpool 从池子里挑一个空闲的后端连接给它用;客户端断开,后端连接不销毁,放回池子。

这解决的是 PostgreSQL 的一个硬约束:每个连接对应一个后端进程,进程模型下连接数上不去,几百个连接就开始抢 CPU,上千个基本就废了。连接池把「客户端连接数」和「数据库进程数」这两件事拆开,客户端可以开到上千,后端保持几十个。

但复用的是连接,不是会话状态。任何依赖于会话本身的特性——临时表、会话级变量(SET)、游标、prepared statement、advisory lock——在连接池模式下都可能出问题,因为同一个客户端的两次请求可能落到不同的后端连接上。

# pgpool.conf —— 连接池核心参数 connection_cache = on num_init_children = 32 # 预派生的前台进程数,决定了并发客户端上限 max_pool = 4 # 每个前台进程最多缓存几个后端连接 child_life_time = 300 # 空闲子进程回收时间,秒 child_max_connections = 0 # 单个子进程处理多少连接后重启,0 表示不限制

num_init_children是最需要算清楚的参数:它能接纳的并发客户端数大致等于num_init_children × max_pool,超过这个数客户端会在 pgpool 层排队,而不是直接报错。这也是文档里提到的「限制超过限度的连接」——它不拒绝,而是把请求挂起,避免瞬间把数据库打穿。副作用是排队期间客户端看起来「卡住了」,如果应用侧超时设置比排队时间短,就会看到大量超时报错。

4.2 读写分离靠 SQL 首词猜,代价是什么

load_balance_mode = on打开后,pgpool 会把读请求分发到备库。但它判断「这是不是读请求」的方式非常朴素:解析 SQL 的第一个关键词,是 SELECT 就走备库,否则走主库。

load_balance_mode = on # 4.0 之后的写法:把会写数据的函数名拉黑,含这些函数的 SELECT 也不走备库 black_function_list = 'nextval,setval,currval,lastval'

坑就在这里。SELECT nextval('seq')首词是 SELECT,但它是写操作,发到备库会直接报「cannot execute nextval() in a read-only transaction」。SELECT ... FOR UPDATE、调用了写入函数的存储过程、带 CTE 的写查询,都属于这一类。black_function_list就是用来兜底的,但它是按函数名匹配的字符串黑名单,覆盖不了自定义函数,也不可能覆盖所有情况。

另一类坑是「刚写完立刻读」。主库提交了,WAL 还没传到备库,这时候的读请求被发到备库,读到的就是旧数据。业务上表现为「我刚保存的东西刷新一下就没了」,用户会以为系统出 bug。常见的处理思路是在应用侧对这种「写后立即读」的场景加注释强制走主库,pgpool 支持/*NO LOAD BALANCE*/这样的注释前缀来跳过负载均衡。

4.3 watchdog 与虚拟 IP:谁有资格宣布主库死了

pgpool 自身也会成为单点,所以生产上通常起两个 pgpool 实例,用 watchdog 组成一个互相监控的小集群。watchdog 干两件事:一是互相心跳,确认对方还活着;二是管理一个虚拟 IP(VIP),保证同一时刻只有一个 pgpool 对外提供服务。

# pgpool.conf —— watchdog 与 VIP 相关 use_watchdog = on wd_hostname = '192.168.10.21' wd_port = 9000 delegate_IP = '192.168.10.100' # 对外的浮动地址 if_up_cmd = 'ip addr add $_IP_$/24 dev eth0 label eth0:0' if_down_cmd = 'ip addr del $_IP_$/24 dev eth0' arping_cmd = 'arping -U $_IP_$ -w 1' wd_interval = 10 # 心跳间隔,秒

if_up_cmd/if_down_cmd里的$_IP_$会被替换成 delegate_IP 的值,这是 pgpool 约定的占位符,写错就完全不生效。arping_cmd的作用是切换后主动广播一次 ARP,让交换机尽快更新 MAC 地址表,否则可能出现「VIP 已经漂过去了,但流量还是打到旧机器」的情况,持续几秒到几十秒不等。

真正决定「主库死没死」的是健康检查,不是 watchdog 心跳:

health_check_period = 10 # 每 10 秒检查一次后端 health_check_timeout = 20 # 单次检查超时,必须大于 0,否则检查会被禁用 health_check_user = 'health' # 检查用的数据库账号,需要有登录权限 health_check_password = 'xxxx' health_check_max_retries = 2 # 失败后重试次数,注意重试会让总耗时成倍增长

health_check_period决定故障发现的速度,health_check_timeout × (max_retries + 1)决定最坏情况下多久才判定一个节点挂了。这两个值配小了会误判——主库只是短暂 IO 抖动,结果被踢出集群;配大了真故障时要等很久才切换。没有标准答案,得按业务能容忍的 RTO 反推。

判定失败后触发failover_command:

# failover_command 会带上这些占位符,按需取用 # %d 被踢掉的节点编号 %H 新主库主机名 %P 旧主库端口 %M 旧主库节点编号 failover_command = '/usr/local/bin/promote.sh %d %H %P %M'

这个脚本里要做的事情包括:把备库晋升成主库(12 之后是删 standby.signal 或执行 pg_ctl promote)、更新 pgpool 自己配置文件里主库的节点编号、必要时重建复制关系。脚本跑失败,集群就停在半死不活的状态——这是这套架构里最容易出事的一环。

4.4 failover 之后:状态、连接与回切

故障切换完成后,pgpool 内部维护的节点状态是这样的:旧主库被标记为 down 并且 detach,备库变成新的 primary。此时如果旧主库恢复了,它不会自动回到集群,需要手动pcp_attach_node拉回来——而且拉回来之后它的角色是备库,数据可能是旧的,得先做一次 pg_rewind 或者干脆重做基础备份。

这一步是很多人忽略的后悔药:切换发生时一切看起来正常,等旧主库修好重新上线,直接 attach 回去,结果两边数据分叉,复制报错,最后只能删库重建。

# 查询集群中各节点状态,0 表示正常,1 表示 down,2 表示被 detach pcp_node_info -h 192.168.10.21 -p 9898 -U pgpool 0 pcp_node_info -h 192.168.10.21 -p 9898 -U pgpool 1 # 故障节点修复后,先确认数据一致,再拉回集群 pcp_attach_node -h 192.168.10.21 -p 9898 -U pgpool 0

pcp_attach_node只是把 pgpool 的认知改回来,它不会帮你修复数据。这个命令和数据库的真实状态之间没有任何校验,是纯粹的「你说是就是」。所以回切前的数据一致性检查必须自己做,通常是比对两边pg_control里的 LSN,或者干脆用 pg_rewind 把旧主库拉回到新主库的时间线。

5. 避坑与排查:那些让集群「看起来正常」的坑

5.1 备库连不上,日志刷 no pg_hba.conf entry for host

现象是备库启动后一直停在waiting for WAL或者干脆连不上,主库日志里反复出现no pg_hba.conf entry for host "192.168.10.12", user "repl", database "replication"。

原因有两层。表层是 pg_hba.conf 里没写 replication 那一行,只写了all的规则;深层是很多人改完 pg_hba.conf 忘了 reload,PostgreSQL 对 pg_hba.conf 的改动需要pg_ctl reload或者SELECT pg_reload_conf()才生效,而 postgresql.conf 里有些参数是必须重启的,两类混在一起就容易漏。

解决办法是先确认规则存在且顺序正确——pg_hba.conf 是自上而下第一条匹配生效,如果上面有一条更宽的host all all 0.0.0.0/0 reject,下面写再多也白搭。然后 reload,用psql "host=... dbname=replication replication=true"单独验证复制连接能不能建起来,别指望通过看备库日志来定位。

5.2 all backend nodes are down

现象是应用连 pgpool 报all backend nodes are down,但直接连主库、备库都好好的。

这个报错的意思是「pgpool 认为自己管理的所有后端节点都不可用」,注意是「pgpool 认为」。常见原因有三类。一是健康检查账号密码配错了,health_check_user这个用户压根不存在或者权限不够,每次检查都失败,于是所有节点被标记为 down。二是backend_hostname/backend_port写错,pgpool 连的根本不是你以为的那台机器。三是 pg_hba.conf 没放行 pgpool 所在的 IP,健康检查连接被拒。

排查顺序应该是:先在 pgpool 机器上用psql -h backend_host -U health_check_user直连一次,确认网络和认证都通;再查pcp_node_info看每个节点的状态码;最后看 pgpool 日志里健康检查失败的具体报错。跳过前两步直接改配置,属于纯靠猜。恢复现场用pcp_attach_node把节点逐个拉回来,注意一次只拉一个,拉回来观察一会儿再拉下一个。

5.3 复制延迟忽大忽小,LSN 卡在某个位置不动

现象是pg_stat_replication里replay_lsn长时间不动,或者replay_lag一直涨,主库磁盘 IO 却不高。

最常见的原因是备库上有长事务或者长查询在跑。备库的 startup 进程重放 WAL 时,如果遇到与正在执行的查询冲突(比如要清理一行正在被读的数据),会等待查询结束。这是 PostgreSQL 的查询冲突机制,max_standby_streaming_delay控制最多等多久,超时就把查询杀掉继续重放。如果这个值配得很大甚至设成 -1,备库的只读查询会无限期阻塞重放,延迟就一路涨上去。

另一个原因是主库上有一个超大事务长期不提交。WAL 是按事务提交顺序可见的,大事务没提交前,它的 WAL 记录不能被重放,备库看上去就「卡住」了。这种情况查pg_stat_activity里的xact_start就能定位。

处理方式分两步:先用pg_stat_activity在备库找出阻塞重放的查询,看看能不能杀;再看主库有没有超长事务。hot_standby_feedback = on能让备库把查询冲突信息反馈给主库,减少主库清理造成的冲突,代价是主库的 vacuum 会被拖住、表膨胀变快,属于典型的取舍。

5.4 切换之后 VIP 漂过去了,应用还是连不上

现象是 failover 触发后,VIP 在新的 pgpool 节点上能看到,ip addr里也确实有,但应用连上去报连接超时或者连到了已经挂掉的旧机器。

原因是 ARP 缓存没刷新。VIP 是从一台机器挪到另一台机器,但交换机和上游路由器的 ARP 表里记的还是旧机器的 MAC 地址,流量继续往已经死掉的网卡上打,一直到 ARP 表项自然过期。arping_cmd就是为了解决这个问题,但如果命令写错、pgpool 进程没有执行该命令的权限(绑定 IP 需要 root 或 CAP_NET_ADMIN),或者if_up_cmd里网卡名写错,整个机制就静默失效。

排查方法是切换后立刻在另一台机器上arping一下 VIP,看响应来自哪个 MAC。同时确认 pgpool 是用什么身份跑的——很多人为了省事用 root 跑 pgpool,这在安全上是另一个问题,常见做法是给 pgpool 二进制加setcap cap_net_admin,cap_net_raw+ep,让它有改 IP 的能力又不用 root。

5.5 认证方式不匹配,pool_passwd 里的密码永远对不上

现象是应用连 pgpool 报认证失败,但同样的账号密码直连数据库完全正常。

pgpool 做连接池,必须自己保存一份用户密码才能替客户端去连后端,这份密码存在pool_passwd文件里。生成方式是在数据库上查出密码的哈希,用pg_md5写进去。问题出在 PostgreSQL 14 之后默认的password_encryption变成了scram-sha-256,而较早版本的 pgpool 不支持 SCRAM,pool_passwd里存的还是 MD5 格式,两边对不上,认证就永远失败。

解决路径有两条:要么把 pgpool 升到支持 SCRAM 的版本,要么在这种混合场景下单独给池子用的账号保留 md5 认证。第二条路能跑通,但会在数据库里留下弱哈希,属于拿安全换兼容。判断依据是看pg_hba.conf里那条规则写的md5还是scram-sha-256,以及SHOW password_encryption的输出,两者必须一致。

6. 取舍:零丢失、可用性与复杂度三者只能选两个

6.1 同步复制不是免费的

把synchronous_standby_names打开的那一刻,主库的写延迟就锚定在了最慢那个备库的磁盘上。业务量上来以后,这个锚点的抖动会被放大成整条链路的毛刺。常见的折中是ANY 1 (node_a, node_b)配两个备库,容忍一个掉队;或者干脆退回异步,用「WAL 归档 + PITR」去覆盖极小概率的丢数据窗口。前者靠冗余换低延迟,后者靠恢复能力换性能,没有哪个是标准答案。

6.2 中间件引入的新故障点

pgpool 解决了连接数和读写分离,代价是集群里多了一个必须高可用的组件。它自己的双机 watchdog、VIP 漂移、failover 脚本、pool_passwd 同步,每一项都是新的失败模式。如果业务侧的读压力本来就不大,只想要一个故障时能切过去的主备,用 pgpool 反而是给自己加戏——纯流复制加上一套清晰的手工切换 SOP,可能比自动切换更可控,因为自动切换最怕的就是误判。

场景流复制 + 手工切换流复制 + pgpool
读扩展需求需应用侧自己配多数据源中间件分发
故障切换时间分钟级,依赖人工秒级,依赖健康检查参数
新增故障点无pgpool 自身、VIP、脚本
版本兼容约束无需匹配 PostgreSQL 认证方式

6.3 备份是最后一道防线

在线复制防的是机器故障,防不了误操作。一条DELETE少写了 WHERE,几秒内就会被复制到备库,两边同时干净。所以离线备份这条线不能省,全量加归档日志的组合能恢复到任意时间点,配合定期做恢复演练——只备份不恢复演练,等于没备份。

从那以后我每次搭这套环境,都会在切换测试之后强制再走一遍「旧主库重新上线」的完整回切流程,尤其是数据一致性检查那一步。切换成功只是及格线,回切不翻车才算这套方案真的立住了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询