Skip to content

自动故障转移方案 ​

手工故障转移(pg_ctl promote)是所有高可用的底层原语,但它依赖人在场:判定主库不可用、选择候选备库、执行提升、切换流量、善后旧主库——每个环节都要几分钟到几十分钟。对多数业务而言,这个 RTO 是不可接受的。自动故障转移方案把这套流程交给软件:故障发生后的秒级到分钟级内,由工具自动完成判定、提升与流量切换。

但自动化不是免费的。自动判定「主库死了还是联系不上」引入了误判风险;自动提升引入了脑裂风险;自动流量切换引入了「切到一半」的中间状态。因此成熟的自动故障转移方案的核心设计,不是「怎么提升」,而是「怎么确认只有一个人有资格提升」——这就是共识层(DCS)与 fencing 的意义。

本页先拆解自动故障转移的组成结构,再对比主流方案(Patroni、repmgr、pg_auto_failover、Pgpool-II、云托管),重点讲清共识层的作用、脑裂与 fencing、流量切换层的设计,以及监控、演练与选型建议。手工流程的细节(提升命令、时间线、pg_rewind)已在前几页展开,本页聚焦「自动化如何组织这些原语」。

前置知识

建议先阅读:时间线与时间线历史

基础概念 ​

自动故障转移引入了一批分布式系统的术语。先把它们与 PostgreSQL 原生概念对应起来,看方案文档时才不会迷失。

概念英文说明
DCSDistributed Configuration Store分布式配置存储(etcd / Consul / ZooKeeper),提供共识与选主
领导者租约leader lease主库通过 DCS 持有的「任职凭证」,到期未续约即失格
Fencing隔离确保旧主库无法继续作为写入点,是防脑裂的核心手段
STONITHShoot The Other Node In The Headfencing 的极端形式:直接关掉旧节点电源
脑裂split-brain两个节点同时作为主库接受写入
仲裁quorum超过半数成员同意才允许关键动作(如提升)
健康检查health check周期性探测主库可用性的机制
降级degraded集群失去部分冗余但仍在服务的状态(如仅剩主库无备库)
委托 IPdelegate IP由 watchdog 组件管理的虚拟 IP,随主库漂移
提升候选candidate被选中执行提升的备库

自动故障转移的组成结构 ​

任何自动故障转移方案,都可以拆成四个组件。理解这个框架,就能把任何新方案快速归位:

text
┌──────────────┐   ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│  1. 健康检查  │ → │ 2. 仲裁/选举  │ → │  3. 提升      │ → │ 4. 流量切换   │
│  探测主库状态 │   │  决定谁有资格 │   │  promote     │   │  VIP/DNS/代理 │
└──────────────┘   └──────────────┘   └──────────────┘   └──────────────┘
  • 健康检查:周期性探测主库(连接、只读探针、磁盘、进程),持续 N 次失败才判定故障,排除瞬时抖动;
  • 仲裁 / 选举:这是自动方案与手工方案的本质区别。谁判定故障、谁授权提升,需要一个超越单个数据库节点的裁决者——即共识层;
  • 提升:本质还是前几页讲的 pg_ctl promote / pg_promote(),自动化只是包装了前置检查与善后(含 pg_rewind);
  • 流量切换:让客户端找到新主库。VIP、HAProxy、DNS、应用内多地址,各有取舍。

DCS 是什么 ​

DCS(Distributed Configuration Store)是独立于数据库之外的一组小集群(etcd 通常 3 或 5 个节点),提供两样关键能力:分布式锁与带租约的键值存储。数据库节点把「我是主库」这个事实写入 DCS 并持续续租;租约到期未续约,DCS 上的主库记录自动消失,其余节点才有资格竞选。

关键性质:DCS 自己用多数派(quorum)保证一致性。3 节点的 etcd 挂 1 个仍可工作,挂 2 个就整体只读——此时没有任何数据库节点能拿到主库租约,集群宁可「不可写」也不会「双主」。这个「宁可不可用也不脑裂」的取舍,正是共识层的价值所在。

主流方案一图流 ​

text
                 ┌─────────────────────────────────────────┐
                 │              DCS (etcd ×3)               │
                 └───────┬─────────────┬─────────────┬─────┘
                         │             │             │
              ┌──────────┴───┐ ┌───────┴──────┐ ┌────┴─────────┐
              │ Patroni      │ │ Patroni      │ │ Patroni      │
              │ node1 (主)    │ │ node2 (备)    │ │ node3 (备)    │
              └──────┬───────┘ └──────┬───────┘ └────┬─────────┘
                     │ :8008          │ :8008        │ :8008
                ┌────┴────────────────┴──────────────┴────┐
                │   HAProxy(健康检查路由写流量)              │
                └──────────────────┬───────────────────────┘
                                   │ :5432
                                 应用

语法与用法 ​

自动故障转移方案的「语法」是各工具的配置文件与命令行。这里给出三个主流方案的最小骨架,让你对部署形态有直观认识;完整配置以各自官方文档为准。

Patroni 最小配置 ​

Patroni 用 YAML 描述集群,节点之间通过 DCS 感知彼此:

yaml
# patroni.yml(三个节点各一份,name 各不相同)
scope: pg-cluster
name: node1

restapi:
  listen: 0.0.0.0:8008
  connect_address: 10.0.0.10:8008

etcd3:
  hosts: 10.0.1.10:2379,10.0.1.11:2379,10.0.1.12:2379

bootstrap:
  pg_hba:
    - host replication replicator 10.0.0.0/24 scram-sha-256
    - host all all 10.0.0.0/24 scram-sha-256

postgresql:
  listen: 0.0.0.0:5432
  connect_address: 10.0.0.10:5432
  data_dir: /var/lib/postgresql/18/main
  authentication:
    superuser:
      username: postgres
      password: 'Ch4ngeM3_Str0ng'
    replication:
      username: replicator
      password: 'Repl_Ch4ngeM3'
bash
# 启动与管理
patroni /etc/patroni/patroni.yml          # 前台启动(生产配 systemd)

patronictl -c /etc/patroni/patroni.yml list    # 查看集群成员状态
patronictl -c /etc/patroni/patroni.yml switchover   # 计划内切换
patronictl -c /etc/patroni/patroni.yml failover    # 手动指定候选执行故障转移

repmgr 最小结构 ​

repmgr 的形态是「repmgr 元数据库 + repmgrd 守护进程 + 命令行工具」:

conf
# repmgr.conf
node_id=1
node_name=node1
conninfo='host=10.0.0.10 user=repmgr dbname=repmgr connect_timeout=2'
data_directory='/var/lib/postgresql/18/main'
failover=automatic
promote_command='repmgr standby promote'
follow_command='repmgr standby follow'
bash
repmgr cluster show        # 查看集群拓扑与状态
repmgr standby promote     # 手工提升(repmgrd 自动提升时内部也调用它)
repmgr node rejoin --force-rewind   # 旧主库带 pg_rewind 重新接入

pg_auto_failover 最小结构 ​

pg_auto_failover 引入一个独立的 monitor 节点承担仲裁职责:

bash
# monitor 节点(独立机器或与数据库混部,生产建议独立)
pg_autoctl create monitor --pgdata /var/lib/postgresql/monitor \
  --hostname monitor.internal

# 数据节点
pg_autoctl create postgres --pgdata /var/lib/postgresql/18/main \
  --monitor 'postgres://auto_failover@monitor.internal/pg_auto_failover' \
  --hostname node1.internal

pg_autoctl show state      # 查看节点状态机
pg_autoctl perform failover   # 计划内切换

详细说明 ​

四个组成部分在主流方案中的落位 ​

组成Patronirepmgrpg_auto_failoverPgpool-II
健康检查自身进程存活即参与 + DCS 租约续期repmgrd 心跳写入元库节点向 monitor 上报health_check_period
仲裁/选举DCS(etcd/Consul/ZK)多数派repmgr 元数据库投票monitor 节点裁决watchdog 多数派
提升调用 pg_ctl promoterepmgr standby promotemonitor 指挥节点提升failover_command 脚本
流量切换依赖外部(HAProxy/VIP)依赖外部monitor 提供拓扑供路由内置(前端直接连 Pgpool)

可以看到没有方案是「全栈自足」的:Patroni 专注数据层,把流量层留给 HAProxy/VIP;Pgpool-II 反过来专注流量层与连接池,数据层仲裁相对薄弱。这正是组合使用的现实原因。

手工流程与自动化流程的对应 ​

自动方案的价值可以用一张对照表说透——手工 Runbook 里的每一步,都被自动化压缩成了什么:

手工步骤(Runbook)自动化后的形态典型耗时
连续 N 次探活 + 多探针交叉验证工具内置健康检查周期 + 租约机制秒级
人工决策「是否值得切换」判定规则固化(可保留人工确认开关)秒级或人工
比较各备库 replay_lsn 选候选工具按内置规则自动选毫秒级
pg_ctl promote / pg_promote()工具调用同一命令秒级
VIP / DNS / 连接池切换Watchdog / 路由健康检查自动完成秒级
旧主库 pg_rewind 善后节点重新加入时自动执行与变更量相关
记录事件与时间线变化事件日志 + 监控采集自动

对照表揭示了一个重要事实:自动化没有改变任何底层动作,它改变的是「谁来执行、多快执行」。这也意味着手工流程中强调的所有检查(提升前量化丢失、善后验证),自动方案都以配置或内置逻辑的形式存在——理解手工流程依然是理解自动方案的前提。

主流方案对比 ​

维度Patronirepmgrpg_auto_failoverPgpool-II云托管(RDS/Aurora 等)
架构DCS + 每节点 Agent元库 + 每节点守护进程monitor + 节点独立代理层平台托管
共识来源etcd/Consul/ZK 多数派元数据库monitor 单点(可高可用)watchdog 多数派平台内部
自动故障转移成熟,业界事实标准成熟成熟,开箱即用有,但历史问题较多成熟,平台负责
读写分离不内置不内置不内置内置负载均衡平台提供只读端点
连接池不内置不内置不内置内置平台提供
pg_rewind 支持自动node rejoin --force-rewind内部处理有限无需关心
运维复杂度高(多组件)中中低中低
可控性完全完全完全完全低(黑盒)

没有「最好」的方案,只有与团队运维能力、业务 SLA 匹配的方案。选型建议见文末。

共识层的作用与 DCS 不可用时的行为 ​

共识层解决的是一个朴素的问题:当两个节点对「主库还活着吗」产生分歧时,听谁的?

没有共识层的方案,每个节点自己判定,判定结果可能相反:node1 认为主库挂了于是自己提升,主库其实只是和 node1 断了网还在正常服务——脑裂就此产生。有了 DCS 之后,判定权收归多数派:

  1. 主库周期性地在 DCS 中续租「领导权」;
  2. 候选备库只有在 DCS 中确认「领导租约已失效」且自己能获得多数派支持时,才会提升;
  3. 被网络分区隔离的节点拿不到多数派支持,会主动把自己降级为只读(甚至暂停),绝不多写。

DCS 不可用时的行为是理解 Patroni 的关键:

DCS 状态集群行为
DCS 多数派存活一切正常
DCS 少数派节点挂掉无影响
DCS 整体不可用(多数派丢失)主库租约到期后自动降级只读,集群拒绝写入,等待 DCS 恢复
DCS 恢复节点重新加入,主库凭连续性恢复领导权(或重新选举)

「DCS 挂了数据库就不让写」听起来激进,但这是正确的工程取舍:DCS 不可用意味着系统已经无法区分「谁该当主库」,此时唯一安全的选择是停止接受写入。可用性可以事后恢复,双主的数据冲突很难收拾。

DCS 的高可用就是集群的高可用

DCS 部署在单机上、或与数据库节点共享故障域(同机柜、同可用区),等于把仲裁的命脉放进了与数据库相同的故障半径。etcd 的 3 或 5 个节点应分布在不同故障域上,且数量为奇数。

脑裂、误判与 fencing ​

自动方案把故障判定从人转移给了软件,但软件的判定一样会错。两类最危险的情形:

误判(假故障):主库只是慢(磁盘 IO 抖动、长事务、网络抖动),健康检查超时后被判定死亡,备库提升。此时旧主库还活着,且仍然接受写入——两个写入点出现。

分区(假失联):网络分区把主库与仲裁体隔开,主库自己完全正常。与误判同样产生双主,且更难察觉(主库会向连着它的那部分客户端正常服务)。

对这两类风险,唯一的系统性解法是 fencing(隔离):提升新主库之前或同时,确保旧主库不可能再作为写入点工作。工程上按强度递增:

fencing 手段强度说明
租约失效即自我降级基础主库失去 DCS 租约后自动转只读(Patroni 内置)
提升 branch 校验增强提升前确认旧主库在 DCS 中的领导记录已消失
关闭数据库进程强对旧节点执行停止(工具通过 Agent/SSH)
关闭主机电源(STONITH)最强通过带外管理(IPMI 等)关机,防止主机本身失智

PostgreSQL 有一个天然的辅助机制:旧主库重接入前必须做 pg_rewind 或重建。即使短暂出现双写,善后时至少能通过时间线与 WAL 精确识别「哪边写了什么」,把冲突范围圈出来。但这只是事后审计能力,不是预防手段,不能替代 fencing。

与同步复制、复制槽的组合 ​

自动故障转移解决「快速切换」,同步复制解决「切换时丢多少」。两者组合才有完整的 RPO 承诺:

组合效果代价
自动故障转移 + 异步复制RTO 小,RPO > 0(丢最近一段写入)无
自动故障转移 + 同步复制(remote_apply)RPO = 0提交延迟上升;同步备库不可用时写入阻塞
自动故障转移 + 同步复制 + 多备库 quorumRPO = 0 且容忍单备库故障架构与运维复杂度最高

组合时有两个工程要点:

  1. 提升候选必须是数据最靠前的备库。异步复制下要比较各备库的 replay_lsn;同步复制下天然只有同步备库是安全的候选。主流工具都内置了这个规则,但「最近被排除出同步名单的备库」等边角情况值得演练验证。
  2. 复制槽要随拓扑自动管理。节点重新加入、角色互换后,物理复制槽需要重建或迁移。手工管理槽在这种动态拓扑下容易出错(槽闲置导致 pg_wal 膨胀),应依赖工具管理或配置 max_slot_wal_keep_size 兜底。

同步复制的等级与取舍详见同步复制等级,复制槽管理见复制槽。

流量切换:VIP / HAProxy / DNS / 负载均衡 ​

提升只解决「谁是主库」,客户端还要「找到」主库。四种主流路由方式:

方式原理切换速度优缺点
VIP(keepalived / vip-manager)虚拟 IP 漂移到新主库秒级对应用完全透明;要求同二层网络
HAProxy / 负载均衡器按健康检查把连接转发到当前主库秒级天然支持多备库读分发;多一层组件
DNS修改域名记录指向新主库TTL 级(秒~分钟)实现简单;受客户端 DNS 缓存拖累
应用内多地址应用配置多个候选,失败转移取决于实现无外部依赖;需要应用改造

HAProxy + Patroni 是当前最流行的组合,原理是利用 Patroni 的 REST API(默认 8008 端口)做健康检查:主库返回 200,备库返回 503,HAProxy 据此把写流量只发给主库:

conf
# haproxy.cfg —— 写流量路由(读流量可类似配置多后端轮询)
listen postgres-primary
    bind *:5432
    mode tcp
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server node1 10.0.0.10:5432 check port 8008
    server node2 10.0.0.11:5432 check port 8008
    server node3 10.0.0.12:5432 check port 8008

on-marked-down shutdown-sessions 很关键:节点被判不健康时立即切断既有连接,避免应用继续在「已失格的主库」上写。

这套路由层自身的单点问题必须解决:HAProxy 部署两台 + keepalived 管 VIP,或使用云负载均衡器。路由层挂了,数据库集群再健康也无济于事。

客户端重连策略 ​

无论路由层多完善,切换瞬间总有一部分连接被断开。应用侧的重连行为决定了用户体验的下限:

text
推荐的重连策略:
1. 识别连接类错误(连接断开、无法连接、角色相关错误),与业务错误区分开
2. 指数退避 + 抖动重试(如 1s / 2s / 4s ± 随机),避免全体客户端同时重试打爆新主库
3. 有限次数(如 3~5 次)后放弃并报错,避免无限重试掩盖问题
4. 重试前重新解析路由(重新取连接池连接、刷新 DNS)
5. 写操作重试前确认幂等性:非幂等写入(INSERT 无唯一键)盲目重试可能重复数据
6. 连接池设置探测与自动重建(如 PgBouncer 的 server_lifetime / server_check_query)

一个常被忽略的细节:切换后旧主库连接必须被清除。应用持有到旧主库的存量连接,即使新主库已就绪,这些连接要么报错(旧主库降级只读)要么继续在只读状态下「成功」执行只读操作——让排障者误以为服务正常。HAProxy 的 shutdown-sessions、PgBouncer 的 RECONNECT 都是为此。

监控与告警 ​

自动故障转移方案的监控要覆盖四个层次:

层次关键指标告警建议
数据库层复制延迟(replay_lag)、连接数、磁盘、WAL 堆积延迟超阈值、pg_wal 水位异常
集群层节点角色(谁是主)、成员数量、timeline_id角色意外变化、成员缺失、时间线跳变
仲裁层DCS 健康度、租约状态DCS 节点掉线、租约异常
路由层HAProxy 后端健康数、VIP 位置、连接成功率可用后端数下降、切换事件

两个高价值的具体检查:

sql
-- 集群层:从任一节点确认角色与时间线
SELECT pg_is_in_recovery(),
       timeline_id
FROM pg_control_checkpoint();
text
 pg_is_in_recovery | timeline_id
-------------------+-------------
 t                 |           2
(1 row)
sql
-- 数据层:主库上确认备库是否在正常跟随
SELECT application_name, state, sync_state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
FROM pg_stat_replication;
text
 application_name |   state   | sync_state | lag_bytes
------------------+-----------+------------+-----------
 node2            | streaming | async      |         0
 node3            | streaming | async      |     16432
(2 rows)

事件类告警(「发生过切换」)往往比指标类告警更重要:一次计划外的自动切换,即使服务无感知,也说明主库出现过健康检查失败——必须事后归因,否则同样的故障下次可能造成真实业务影响。

半自动与全自动的取舍 ​

不是所有集群都应该全自动。按自动化程度排列:

模式判定与提升由谁做RTO适用场景
纯手工人十分钟~小时级开发测试、数据一致性要求极端严格
半自动(人判定,工具执行)人判定故障,一条命令完成提升+切换分钟级有 DBA 值班、可接受分钟级中断
全自动(failover)软件判定并提升秒级~1 分钟7×24 业务、无法容忍分钟级中断
全自动(托管)云平台平台承诺值团队无数据库运维能力

半自动模式(例如 Patroni 的 failover 手动触发、repmgr 的 --manual 模式、pg_auto_failover 的 PGOPTIONS 控制)常被低估:它保留了人对「是否值得切换」的判断(比如主库只是短暂抖动,等 30 秒可能比切换更划算),同时用工具消除执行环节的手误。对多数中小团队,半自动是投入产出比最好的起点,全自动可以在半自动流程跑熟之后逐步开启。

演练与爆炸半径控制 ​

自动故障转移的可靠性只能靠演练证明。演练设计有两个要点:

演练频率与场景覆盖:至少覆盖「干净故障」(停主库进程)、「脏故障」(kill -9)、网络分区(iptables 断流)三类,分别验证判定与 fencing 的表现。

爆炸半径控制:演练与真实故障时都要限制影响面:

  1. 单集群演练:不要同时对多个生产集群做演练;
  2. 可快速回退:演练前确认「切回原主库」的路径可用(切换是双向可逆的,故障转移往往不是);
  3. 错峰:避开业务高峰与大事务窗口;
  4. 灰度流量:条件允许时,演练期间把部分只读流量摘除,降低切换抖动的影响面;
  5. 事件留痕:每次演练记录触发方式、判定耗时、提升耗时、流量切换耗时、应用感知时间,形成 RTO 的真实台账。

云托管方案的能力与限制 ​

RDS、Aurora、Cloud SQL 等托管数据库提供开箱即用的高可用,但能力边界与自建不同:

能力表现
故障转移平台负责,通常秒级到分钟级,应用重连即可
可观测性平台指标丰富,但看不到操作系统与进程内部
主机访问通常无法 SSH 登录实例主机
工具兼容性部分 local 工具(pg_rewind、pg_controldata、部分扩展)不可用或受限
参数自由度部分参数不可修改(重启相关、文件系统相关)
复制槽提供度不一,部分平台限制物理复制槽的使用

对团队的真正含义是:自建集群的很多排障手段在托管环境里失效。例如无法在故障现场用 pg_controldata 检查控制文件,无法用 pg_rewind 手工回收旧主库(平台自己处理)。选择托管意味着接受「更少的控制权换更少的运维负担」,这个交换是否划算取决于业务对可控性的要求。

选型建议 ​

场景推荐
有专职 DBA、要求完全可控Patroni + etcd + HAProxy/VIP
团队较小、想要相对简单的自建pg_auto_failover 或 repmgr
已大量使用 PostgreSQL 生态、还要连接池与读写分离Pgpool-II(或 Pgpool-II + Patroni 组合,注意职责划分)
无专职运维、可接受平台约束云托管高可用版
强一致要求(RPO=0 硬指标)Patroni + 同步复制(remote_apply)+ quorum 多备库

组合使用的提醒:Patroni 与 Pgpool-II 的组合中,两者职责必须划清——Patroni 管数据层的选主与提升,Pgpool-II 只做连接路由与负载均衡,不要让 Pgpool-II 的 failover 机制与 Patroni 的选主同时生效,否则两套「主库判定」互相打架。

实战示例 ​

示例一:部署前的基础设施核对 ​

在部署任何自动故障转移方案之前,逐项核对清单:

bash
# 1. 各节点 PostgreSQL 版本一致
psql -h 10.0.0.10 -c "SHOW server_version;"
psql -h 10.0.0.11 -c "SHOW server_version;"

# 2. pg_rewind 前提已具备(自动方案善后依赖它)
psql -h 10.0.0.10 -c "SHOW wal_log_hints;"

# 3. 节点间网络互通(包括 DCS 端口)
for host in 10.0.0.10 10.0.0.11 10.0.0.12; do
    echo -n "$host pg: "; pg_isready -h $host -p 5432 -t 2
    echo -n "$host etcd: "; timeout 2 bash -c "echo > /dev/tcp/$host/2379" \
      && echo "open" || echo "closed"
done
text
10.0.0.10 pg: 10.0.0.10:5432 - accepting connections
10.0.0.11 pg: 10.0.0.11:5432 - accepting connections
10.0.0.10 etcd: open
10.0.0.11 etcd: open

示例二:用 Patroni 完成一次演练故障转移 ​

bash
# 1. 观察当前集群状态
patronictl -c /etc/patroni/patroni.yml list
text
+ Cluster: pg-cluster -----+---------+---------+----+-----------+
| Member | Host      | Role    | State   | TL | Lag in MB |
+--------+-----------+---------+---------+----+-----------+
| node1  | 10.0.0.10 | Leader  | running |  1 |           |
| node2  | 10.0.0.11 | Replica | running |  1 |         0 |
| node3  | 10.0.0.12 | Replica | running |  1 |         0 |
+--------+-----------+---------+---------+----+-----------+
bash
# 2. 模拟主库故障(演练环境)
ssh 10.0.0.10 "sudo systemctl stop patroni"

# 3. 等待自动提升完成后查看状态
patronictl -c /etc/patroni/patroni.yml list
text
+ Cluster: pg-cluster -----+---------+---------+----+-----------+
| Member | Host      | Role    | State   | TL | Lag in MB |
+--------+-----------+---------+---------+----+-----------+
| node1  | 10.0.0.10 | Replica | running |  2 |         0 |
| node2  | 10.0.0.11 | Leader  | running |  2 |           |
| node3  | 10.0.0.12 | Replica | running |  2 |         0 |
+--------+-----------+---------+---------+----+-----------+

解读:node2 被自动提升,时间线 1 → 2;node1 恢复 Patroni 后没有手工干预就自动以备库身份回归(TL 列同为 2)——这正是 Patroni 内部调用 pg_rewind 完成善后的表现。记录从停止到提升完成的耗时,就是这套方案的实测 RTO。

示例三:验证 DCS 不可用时的自我保护 ​

bash
# 演练环境:停掉全部 etcd(模拟 DCS 整体不可用)
ssh etcd1 "sudo systemctl stop etcd"
ssh etcd2 "sudo systemctl stop etcd"
ssh etcd3 "sudo systemctl stop etcd"

# 等待主库租约到期后,验证写入被拒绝
psql -h 10.0.0.11 -c "CREATE TABLE dcs_down_test(id int);"
text
ERROR:  cannot execute CREATE TABLE in a read-only transaction
bash
# 恢复 etcd,观察集群回到可写状态
for h in etcd1 etcd2 etcd3; do ssh $h "sudo systemctl start etcd"; done

解读:主库在失去仲裁后自动降级只读,宁可不可写也不产生无主状态。这是验证 fencing 生效的最直接方法。演练后记得清理测试表。

示例四:HAProxy 路由层验证 ​

bash
# 模拟应用视角:写流量应始终落到当前主库
psql -h haproxy.internal -p 5432 -c \
  "SELECT inet_server_addr(), pg_is_in_recovery();"
text
 inet_server_addr | pg_is_in_recovery
------------------+-------------------
       10.0.0.11  | f
(1 row)
bash
# 触发一次切换,再查一次(应自动指向新主库)
patronictl -c /etc/patroni/patroni.yml switchover --leader node2 --candidate node3
sleep 10
psql -h haproxy.internal -p 5432 -c \
  "SELECT inet_server_addr(), pg_is_in_recovery();"
text
 inet_server_addr | pg_is_in_recovery
------------------+-------------------
       10.0.0.12  | f
(1 row)

应用侧无任何配置变更,写连接自动落到新主库——这就是路由层与仲裁层协同工作的证明。

示例五:切换事件的监控留痕 ​

sql
-- 巡检脚本片段:核对角色、时间线、复制健康
SELECT current_setting('cluster_name') AS cluster,
       pg_is_in_recovery() AS is_standby,
       (SELECT timeline_id FROM pg_control_checkpoint()) AS tl,
       (SELECT count(*) FROM pg_stat_replication) AS standby_count;
text
  cluster   | is_standby | tl | standby_count
------------+------------+----+---------------
 pg-cluster | f          |  2 |             2
(1 row)

把这个查询接入监控采集,tl 字段的变化即为提升事件信号,standby_count 下降即为降级告警信号。

示例六:RTO 台账模板 ​

演练与真实事件的耗时数据是评估「自动故障转移是否达标」的唯一依据。用一个固定格式的台账积累:

text
┌─────────────────────────────────────────────────────────────┐
│ 事件台账 #2026-09-30-01(演练:干净停库)                       │
├─────────────────────────────────────────────────────────────┤
│ 触发方式     systemctl stop patroni(主库 node1)              │
│ 判定耗时     故障发生 → 仲裁确认失格 = 14s                     │
│ 提升耗时     失格确认 → 新主可写     = 6s                      │
│ 路由耗时     新主可写 → 应用获得可写连接 = 9s                   │
│ 应用感知     第一条成功写请求 = 提升后 12s                     │
│ 善后耗时     node1 以备库身份回归 = 45s(pg_rewind 自动)       │
│ 总 RTO      ≈ 29s(判定+提升+路由)                           │
│ 遗留事项     应用侧两条连接重试告警,需核对退避配置               │
└─────────────────────────────────────────────────────────────┘

台账要固定字段、每次演练必填,季度汇总时就能回答两个问题:RTO 是否稳定达标、哪个环节的耗时在恶化。多数团队的「自动故障转移达不到 SLA」,拆到台账层面都能定位到单一环节(通常是路由或善后)。

注意事项 ​

自动故障转移不能修复「主库挂了但 WAL 没到备库」造成的数据丢失。 异步复制下自动切换照旧丢数据,对 RPO 有硬要求的业务必须叠加同步复制。

不要关闭工具的 fencing / 租约机制来「减少麻烦」。 DCS 不可用时拒绝写入是特性不是缺陷;绕过它的每一次「优化」都在给脑裂开绿灯。

自动提升的候选选择规则要事前确认。 默认规则(数据最靠前)未必符合业务优先级(例如希望同可用区优先),主流工具都支持配置优先级与候选排除。

路由层自身的单点必须消除。 单台 HAProxy、单个 keepalived 节点都会成为新瓶颈;路由层的高可用与数据库层同等重要。

旧主库重接入依赖 pg_rewind 前提。 wal_log_hints 或数据校验和必须提前开启,否则自动善后会退化为全量重建,RTO 被显著拉长。

应用重连与幂等重试是切换体验的另一半。 数据库侧做得再好,应用不重连或盲目重试非幂等写入,切换照样出事故。

复制槽要随动态拓扑管理。 节点角色频繁互换的环境里,手工管理复制槽容易产生闲置槽撑爆 pg_wal;用工具管理并配置 max_slot_wal_keep_size 兜底。

演练要覆盖「脏故障」与网络分区。 只演练干净停库验证不了误判防护;不演练分区验证不了 fencing。

时间线跳变要接入告警。 自动提升会递增 timeline_id,监控它比监控进程存活更能反映「集群发生过什么」。

不要在没跑熟半自动前直接上全自动。 把提升前检查、善后流程在半自动模式下演练充分,再放开全自动,出问题时团队的肌肉记忆才够用。

托管数据库要重新评估运维假设。 主机不可登录、部分工具不可用,自建时代的排障手册需要针对性改写。

事件台账是演练的最终产出物。 只演练不记录,等于验证完即遗忘;判定、提升、路由、善后各环节的耗时趋势,是判断集群健康度的长期依据。

最佳实践 ​

  1. 按「健康检查 + 仲裁 + 提升 + 流量切换」四件套评估任何方案。 缺任何一件的方案都要想清楚补位手段,否则就是裸奔。

  2. DCS(etcd 等)至少 3 节点、分布在不同故障域、奇数个。 DCS 的可用性与故障域隔离程度直接决定整个集群的可用性。

  3. wal_log_hints 作为自动方案集群的强制基线配置。 自动善后依赖 pg_rewind,pg_rewind 依赖 hints 或校验和,这条依赖链要在部署阶段闭合。

  4. 用 HAProxy + Patroni REST API 的组合做写流量路由。 健康检查直接反映主库资格,配置简单且行为可预期;务必保留 on-marked-down shutdown-sessions。

  5. 同步复制与自动故障转移按业务要求组合。 RPO=0 用 remote_apply;接受少量丢失用异步但要监控复制延迟上限。

  6. 客户端实现指数退避 + 有限次数 + 幂等判断的重连策略。 把重连策略写进应用的错误处理规范,而不是各服务自由发挥。

  7. 对「切换事件」与「时间线变化」设置独立告警。 指标超标告警之外,事件类告警是发现静默故障(如周期性抖动导致的反复切换)的唯一手段。

  8. 每季度演练三类故障:干净停库、进程强杀、网络分区。 记录每次的判定耗时、提升耗时、流量切换耗时与业务感知时间,形成 RTO 台账。

  9. 从半自动模式起步,流程跑熟后开启全自动。 团队对提升前检查与善后流程建立了肌肉记忆,全自动才有可靠的行为边界。

  10. 爆炸半径控制写进演练与应急流程。 单集群演练、可回退路径预确认、错峰执行、流量灰度,四条都是低成本高收益的约束。

  11. 选择托管方案时逐项确认能力边界。 主机访问、工具可用性、参数自由度、复制槽支持,都在采购阶段确认,不要等故障时才发现手段失效。

  12. 文档化「谁有资格执行提升」。 即使全自动,也要写清手动干预的入口(patronictl failover、repmgr standby promote)、审批流程与操作步骤,避免事故中临时找权限。

下一节

继续学习:PgBouncer 连接池

参考链接 ​