这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

故障切换模型

详细分析三种经典故障检测/恢复路径下,最差,最优,平均 RTO 的计算逻辑与结果

Patroni 故障按故障对象分类可以分为以下 10 类,按照检测路径不同,可以进一步归纳为五类,在本节内详细展开。

#故障场景描述最终走哪条路径
1PG 进程崩溃crash、OOM killed主动检测
2PG 拒绝连接max_connections主动检测
3PG 假活进程在但无响应主动检测 (检测超时)
4Patroni 进程崩溃kill -9、OOM被动检测
5Patroni 假活进程在但卡住Watchdog
6节点宕机断电、硬件故障被动检测
7节点假活IO hang、CPU 饥饿Watchdog
8主库 ↔ DCS 网络中断防火墙、交换机故障网络分区
9存储故障磁盘坏、磁盘满、挂载失败主动检测Watchdog
10手动切换Switchover/Failover手动触发

但是在 RTO 计算上,最终所有故障都会收敛到两条路径上,本节深入探讨了这两种情况下的 RTO 上下限与均值。

flowchart LR
    A([主库故障]) --> B{Patroni<br/>检测到?}

    B -->|PG崩溃| C[尝试本地重启]
    B -->|节点宕机| D[等待 TTL 过期]

    C -->|成功| E([本地恢复])
    C -->|失败/超时| F[释放 Leader 锁]

    D --> F
    F --> G[从库竞选]
    G --> H[执行 Promote]
    H --> I[HAProxy 感知]
    I --> J([服务恢复])

    style A fill:#dc3545,stroke:#b02a37,color:#fff
    style E fill:#198754,stroke:#146c43,color:#fff
    style J fill:#198754,stroke:#146c43,color:#fff

1 - 被动故障切换

节点宕机,导致领导者租约过期触发集群领导竞选的故障路径

RTO 时序图


故障模型

项目最好最坏平均说明
租约过期ttl - loopttlttl - loop/2最好:即将刷新时宕机
最坏:刚刷新完就宕机
从库检测0looploop / 2最好:恰好在检测点
最坏:刚错过检测点
抢锁提拔021最好:直接抢锁提升
最坏:API 超时+Promote
健康检查(rise-1) × fastinter(rise-1) × fastinter + inter(rise-1) × fastinter + inter/2最好:检查前状态变化
最坏:检查后瞬间状态变化

被动故障与主动故障的核心区别

场景Patroni 状态租约处理主要等待时间
主动故障(PG 崩溃)存活,健康主动尝试重启 PG,超时后释放租约primary_start_timeout
被动故障(节点宕机)随节点一起死亡无法主动释放,只能等待 TTL 过期ttl

在被动故障场景中,Patroni 随节点一起宕机,无法主动释放 Leader Key。 DCS 中的租约只能等待 TTL 自然过期后触发集群选举。


时序分析

阶段 1:租约过期

Patroni 主库会在每个 loop_wait 周期刷新 Leader Key,将 TTL 重置为配置值。

时间线:
     t-loop        t          t+ttl-loop    t+ttl
       |           |              |           |
    上次刷新    故障发生        最好情况      最坏情况
       |←── loop ──→|              |           |
       |←──────────── ttl ─────────────────────→|
  • 最好情况:故障发生在即将刷新租约之前(距上次刷新已过 loop),剩余 TTL = ttl - loop
  • 最坏情况:故障发生在刚刷新租约之后,需等待完整 ttl
  • 平均情况ttl - loop/2
Texpire={ttlloop最好ttlloop/2平均ttl最坏T_{expire} = \begin{cases} ttl - loop & \text{最好} \\ ttl - loop/2 & \text{平均} \\ ttl & \text{最坏} \end{cases}

阶段 2:从库检测

从库在 loop_wait 周期醒来后检查 DCS 中的 Leader Key 状态。

时间线:
    租约过期      从库醒来
       |            |
       |←── 0~loop ─→|
  • 最好情况:租约过期时从库恰好醒来,等待 0
  • 最坏情况:租约过期后从库刚进入睡眠,等待 loop
  • 平均情况loop/2
Tdetect={0最好loop/2平均loop最坏T_{detect} = \begin{cases} 0 & \text{最好} \\ loop/2 & \text{平均} \\ loop & \text{最坏} \end{cases}

阶段 3:抢锁提拔

从库发现 Leader Key 过期后,开始竞选过程,获得 Leader Key 的从库执行 pg_ctl promote,将自己提升为新主库。

  1. 通过 Rest API,并行发起查询,查询各从库的复制位置,通常 10ms,硬编码 2 秒超时。
  2. 比较 WAL 位置,确定最优候选,各从库尝试创建 Leader Key(CAS 原子操作)
  3. 执行 pg_ctl promote 提升自己为主库(很快,通常忽略不计)
选举流程:
  从库A ──→ 查询复制位置 ──→ 比较 ──→ 尝试抢锁 ──→ 成功
  从库B ──→ 查询复制位置 ──→ 比较 ──→ 尝试抢锁 ──→ 失败
  • 最好情况:单从库或直接抢到锁并提升,常数开销 0.1s
  • 最坏情况:DCS API 调用超时:2s
  • 平均情况1s 常数开销
Telect={0.1最好1平均2最坏T_{elect} = \begin{cases} 0.1 & \text{最好} \\ 1 & \text{平均} \\ 2 & \text{最坏} \end{cases}

阶段 4:健康检查

HAProxy 检测新主库上线,需要连续 rise 次健康检查成功。

检测时序:
  新主提升    首次检查    第二次检查   第三次检查(UP)
     |          |           |           |
     |←─ 0~inter ─→|←─ fast ─→|←─ fast ─→|
  • 最好情况:新主提升时恰好赶上检查,(rise-1) × fastinter
  • 最坏情况:新主提升后刚错过检查,(rise-1) × fastinter + inter
  • 平均情况(rise-1) × fastinter + inter/2
Thaproxy={(rise1)×fastinter最好(rise1)×fastinter+inter/2平均(rise1)×fastinter+inter最坏T_{haproxy} = \begin{cases} (rise-1) \times fastinter & \text{最好} \\ (rise-1) \times fastinter + inter/2 & \text{平均} \\ (rise-1) \times fastinter + inter & \text{最坏} \end{cases}

RTO 公式

将各阶段时间相加,得到总 RTO:

最好情况

RTOmin=ttlloop+0.1+(rise1)×fastinterRTO_{min} = ttl - loop + 0.1 + (rise-1) \times fastinter

平均情况

RTOavg=ttl+1+inter/2+(rise1)×fastinterRTO_{avg} = ttl + 1 + inter/2 + (rise-1) \times fastinter

最坏情况

RTOmax=ttl+loop+2+inter+(rise1)×fastinterRTO_{max} = ttl + loop + 2 + inter + (rise-1) \times fastinter

模型计算

将四种 RTO 模型的参数带入上面的公式:

pg_rto_plan:  # [ttl, loop, retry, start, margin, inter, fastinter, downinter, rise, fall]
  fast: [ 20  ,5  ,5  ,15 ,5  ,'1s' ,'0.5s' ,'1s' ,3 ,3 ]  # rto < 30s
  norm: [ 30  ,5  ,10 ,25 ,5  ,'2s' ,'1s'   ,'2s' ,3 ,3 ]  # rto < 45s
  safe: [ 60  ,10 ,20 ,45 ,10 ,'3s' ,'1.5s' ,'3s' ,3 ,3 ]  # rto < 90s
  wide: [ 120 ,20 ,30 ,95 ,15 ,'4s' ,'2s'   ,'4s' ,3 ,3 ]  # rto < 150s

四种模式计算结果(单位:秒,格式:min / avg / max)

阶段fastnormsafewide
租约过期15 / 17 / 2025 / 27 / 3050 / 55 / 60100 / 110 / 120
从库检测0 / 3 / 50 / 3 / 50 / 5 / 100 / 10 / 20
抢锁提拔0 / 1 / 20 / 1 / 20 / 1 / 20 / 1 / 2
健康检查1 / 2 / 22 / 3 / 43 / 5 / 64 / 6 / 8
总计16 / 23 / 2927 / 34 / 4153 / 66 / 78104 / 127 / 150

2 - 主动故障检测

PostgreSQL 主库进程崩溃,Patroni 存活并尝试重启,超时后触发故障切换的路径

RTO 时序图


故障模型

项目最好最坏平均说明
故障检测0looploop/2最好:PG 恰好在检测前崩溃
最坏:PG 刚检测完就崩溃
重启超时0startstart最好:PG 瞬间自愈
最坏:等满 start 超时才释放租约
从库检测0looploop/2最好:恰好在检测点
最坏:刚错过检测点
抢锁提拔021最好:直接抢锁提升
最坏:API 超时 + Promote
健康检查(rise-1) × fastinter(rise-1) × fastinter + inter(rise-1) × fastinter + inter/2最好:检查前状态变化
最坏:检查后瞬间状态变化

主动故障与被动故障的核心区别

场景Patroni 状态租约处理主要等待时间
主动故障(PG 崩溃)存活,健康主动尝试重启 PG,超时后释放租约primary_start_timeout
被动故障(节点宕机)随节点一起死亡无法主动释放,只能等待 TTL 过期ttl

在主动故障场景中,Patroni 仍然存活,能够主动检测到 PG 崩溃并尝试重启。 如果重启成功,服务自愈;如果超时仍未恢复,Patroni 会主动释放 Leader Key,触发集群选举。


时序分析

阶段 1:故障检测

Patroni 在每个 loop_wait 周期检查 PostgreSQL 状态(通过 pg_isready 或检查进程)。

时间线:
    上次检测      PG崩溃      下次检测
       |           |           |
       |←── 0~loop ─→|          |
  • 最好情况:PG 恰好在 Patroni 检测前崩溃,立即被发现,等待 0
  • 最坏情况:PG 刚检测完就崩溃,需等待下一个周期,等待 loop
  • 平均情况loop/2
Tdetect={0最好loop/2平均loop最坏T_{detect} = \begin{cases} 0 & \text{最好} \\ loop/2 & \text{平均} \\ loop & \text{最坏} \end{cases}

阶段 2:重启超时

Patroni 检测到 PG 崩溃后,会尝试重启 PostgreSQL。此阶段有两种可能的结果:

时间线:
  检测到崩溃     尝试重启     重启成功/超时
      |           |             |
      |←──── 0 ~ start ────────→|

路径 A:自愈成功(最好情况)

  • PG 成功重启,服务恢复
  • 不触发故障切换,RTO 极短
  • 等待时间:0(相对于 Failover 路径)

路径 B:需要 Failover(平均/最坏情况)

  • 等待 primary_start_timeout 超时后 PG 仍未恢复
  • Patroni 主动释放 Leader Key
  • 等待时间:start
Trestart={0最好(自愈成功)start平均(需要 Failover)start最坏T_{restart} = \begin{cases} 0 & \text{最好(自愈成功)} \\ start & \text{平均(需要 Failover)} \\ start & \text{最坏} \end{cases}

注意:平均情况假设需要进行故障切换。如果 PG 能够快速自愈,则整体 RTO 会大幅降低。

阶段 3:从库检测

从库在 loop_wait 周期醒来后检查 DCS 中的 Leader Key 状态。当主库 Patroni 释放 Leader Key 后,从库发现后开始竞选。

时间线:
    租约释放      从库醒来
       |            |
       |←── 0~loop ─→|
  • 最好情况:租约释放时从库恰好醒来,等待 0
  • 最坏情况:租约释放后从库刚进入睡眠,等待 loop
  • 平均情况loop/2
Tstandby={0最好loop/2平均loop最坏T_{standby} = \begin{cases} 0 & \text{最好} \\ loop/2 & \text{平均} \\ loop & \text{最坏} \end{cases}

阶段 4:抢锁提拔

从库发现 Leader Key 空缺后,开始竞选过程,获得 Leader Key 的从库执行 pg_ctl promote,将自己提升为新主库。

  1. 通过 Rest API,并行发起查询,查询各从库的复制位置,通常 10ms,硬编码 2 秒超时。
  2. 比较 WAL 位置,确定最优候选,各从库尝试创建 Leader Key(CAS 原子操作)
  3. 执行 pg_ctl promote 提升自己为主库(很快,通常忽略不计)
选举流程:
  从库A ──→ 查询复制位置 ──→ 比较 ──→ 尝试抢锁 ──→ 成功
  从库B ──→ 查询复制位置 ──→ 比较 ──→ 尝试抢锁 ──→ 失败
  • 最好情况:单从库或直接抢到锁并提升,常数开销 0.1s
  • 最坏情况:DCS API 调用超时:2s
  • 平均情况1s 常数开销
Telect={0.1最好1平均2最坏T_{elect} = \begin{cases} 0.1 & \text{最好} \\ 1 & \text{平均} \\ 2 & \text{最坏} \end{cases}

阶段 5:健康检查

HAProxy 检测新主库上线,需要连续 rise 次健康检查成功。

检测时序:
  新主提升    首次检查    第二次检查   第三次检查(UP)
     |          |           |           |
     |←─ 0~inter ─→|←─ fast ─→|←─ fast ─→|
  • 最好情况:新主提升时恰好赶上检查,(rise-1) × fastinter
  • 最坏情况:新主提升后刚错过检查,(rise-1) × fastinter + inter
  • 平均情况(rise-1) × fastinter + inter/2
Thaproxy={(rise1)×fastinter最好(rise1)×fastinter+inter/2平均(rise1)×fastinter+inter最坏T_{haproxy} = \begin{cases} (rise-1) \times fastinter & \text{最好} \\ (rise-1) \times fastinter + inter/2 & \text{平均} \\ (rise-1) \times fastinter + inter & \text{最坏} \end{cases}

RTO 公式

将各阶段时间相加,得到总 RTO:

最好情况(PG 瞬间自愈)

RTOmin=0+0+0+0.1+(rise1)×fastinter(rise1)×fastinterRTO_{min} = 0 + 0 + 0 + 0.1 + (rise-1) \times fastinter \approx (rise-1) \times fastinter

平均情况(需要 Failover)

RTOavg=loop+start+1+inter/2+(rise1)×fastinterRTO_{avg} = loop + start + 1 + inter/2 + (rise-1) \times fastinter

最坏情况

RTOmax=loop×2+start+2+inter+(rise1)×fastinterRTO_{max} = loop \times 2 + start + 2 + inter + (rise-1) \times fastinter

模型计算

将四种 RTO 模型的参数带入上面的公式:

pg_rto_plan:  # [ttl, loop, retry, start, margin, inter, fastinter, downinter, rise, fall]
  fast: [ 20  ,5  ,5  ,15 ,5  ,'1s' ,'0.5s' ,'1s' ,3 ,3 ]  # rto < 30s
  norm: [ 30  ,5  ,10 ,25 ,5  ,'2s' ,'1s'   ,'2s' ,3 ,3 ]  # rto < 45s
  safe: [ 60  ,10 ,20 ,45 ,10 ,'3s' ,'1.5s' ,'3s' ,3 ,3 ]  # rto < 90s
  wide: [ 120 ,20 ,30 ,95 ,15 ,'4s' ,'2s'   ,'4s' ,3 ,3 ]  # rto < 150s

四种模式计算结果(单位:秒,格式:min / avg / max)

阶段fastnormsafewide
故障检测0 / 3 / 50 / 3 / 50 / 5 / 100 / 10 / 20
重启超时0 / 15 / 150 / 25 / 250 / 45 / 450 / 95 / 95
从库检测0 / 3 / 50 / 3 / 50 / 5 / 100 / 10 / 20
抢锁提拔0 / 1 / 20 / 1 / 20 / 1 / 20 / 1 / 2
健康检查1 / 2 / 22 / 3 / 43 / 5 / 64 / 6 / 8
总计1 / 24 / 292 / 35 / 413 / 61 / 734 / 122 / 145

与被动故障对比

阶段主动故障(PG 崩溃)被动故障(节点宕机)说明
检测机制Patroni 主动检测TTL 被动过期主动检测更快发现故障
核心等待startttlstart 通常小于 ttl,但需要额外的故障检测时间
租约处理主动释放被动过期主动释放更及时
自愈可能✅ 有❌ 无主动检测可尝试本地恢复

RTO 对比(平均情况):

模式主动故障(PG 崩溃)被动故障(节点宕机)差异
fast24s23s+1s
norm35s34s+1s
safe61s66s-5s
wide122s127s-5s

分析:在 fastnorm 模式下,主动故障的 RTO 略高于被动故障,因为需要等待 primary_start_timeoutstart); 但在 safewide 模式下,由于 start < ttl - loop,主动故障反而更快。 不过主动故障有自愈的可能性,最好情况下 RTO 可以极短。

3 - 网络分区

主库与 DCS 网络分区,导致租约过期并触发脑裂防护与故障切换的路径

RTO 时序图


故障模型

项目最好最坏平均说明
主库降级retryloop + retryloop/2 + retryPatroni 检测分区后重试,超时后主动降级
租约过期ttl - loop - retryttl - loop - retryttl - loop - retry降级后剩余的 TTL 时间(近似常数)
从库检测0looploop/2最好:恰好在检测点
最坏:刚错过检测点
抢锁提拔021最好:直接抢锁提升
最坏:API 超时+Promote
健康检查(rise-1) × fastinter(rise-1) × fastinter + inter(rise-1) × fastinter + inter/2最好:检查前状态变化
最坏:检查后瞬间状态变化

网络分区与节点宕机的核心区别

场景Patroni 状态PostgreSQL 状态租约处理脑裂风险
节点宕机(过期故障)随节点死亡完全不可用被动等待 TTL 过期
网络分区(本文场景)存活但无法访问 DCS可能仍在运行(需要主动降级)被动等待 TTL 过期有,需防护

在网络分区场景中,主库 PostgreSQL 可能仍在运行并接受写入,这会导致脑裂问题。 Patroni 通过主动降级机制解决:当无法刷新 Leader Key 时,主动将 PostgreSQL 降级为只读或关闭。


时序分析

阶段 1:主库降级

当主库 Patroni 与 DCS 网络分区后,无法刷新 Leader Key,开始重试。

时间线:
  分区发生      检测分区      重试超时      主库降级
     |           |            |            |
     |←── loop ──→|←── retry ──→|
  • 检测延迟:分区发生后,需要等待下一个 loop_wait 周期才能检测到
  • 重试阶段:Patroni 会在 retry_timeout 期间持续重试 DCS 操作
  • 主动降级:重试超时后,Patroni 主动降级 PostgreSQL(防止脑裂)
Tdemote={retry最好(分区恰好在检测前)loop/2+retry平均loop+retry最坏(分区刚好在刷新后)T_{demote} = \begin{cases} retry & \text{最好(分区恰好在检测前)} \\ loop/2 + retry & \text{平均} \\ loop + retry & \text{最坏(分区刚好在刷新后)} \end{cases}

关键设计:Patroni 要求参数满足约束 loop_wait + 2 × retry_timeout ≤ ttl,确保主库在 TTL 过期之前完成降级。

阶段 2:租约过期

主库降级后,Leader Key 仍然存在于 DCS 中,需要等待 TTL 自然过期。

时间线:
  主库降级                   TTL 过期
     |                         |
     |←── ttl - (loop + retry) ──→|

由于主库已经降级,此阶段的等待时间是 TTL 剩余时间。由于分区检测和 TTL 剩余时间是负相关的(分区发生得越早,检测越慢,但 TTL 剩余越长),两者相加是常数:

Texpire=ttlloopretry(近似常数)T_{expire} = ttl - loop - retry \quad \text{(近似常数)}

注意:主库降级 + 租约过期的总时间仍然约等于 ttl,与过期故障相同。

阶段 3:从库检测

从库在 loop_wait 周期醒来后检查 DCS 中的 Leader Key 状态。

时间线:
    租约过期      从库醒来
       |            |
       |←── 0~loop ─→|
  • 最好情况:租约过期时从库恰好醒来,等待 0
  • 最坏情况:租约过期后从库刚进入睡眠,等待 loop
  • 平均情况loop/2
Tdetect={0最好loop/2平均loop最坏T_{detect} = \begin{cases} 0 & \text{最好} \\ loop/2 & \text{平均} \\ loop & \text{最坏} \end{cases}

阶段 4:抢锁提拔

从库发现 Leader Key 过期后,开始竞选过程。

选举流程:
  从库A ──→ 查询复制位置 ──→ 比较 ──→ 尝试抢锁 ──→ 成功
  从库B ──→ 查询复制位置 ──→ 比较 ──→ 尝试抢锁 ──→ 失败
  • 最好情况:单从库或直接抢到锁并提升,≈ 0
  • 最坏情况:DCS API 调用超时,2s
  • 平均情况1s
Telect={0最好1平均2最坏T_{elect} = \begin{cases} 0 & \text{最好} \\ 1 & \text{平均} \\ 2 & \text{最坏} \end{cases}

阶段 5:健康检查

HAProxy 检测新主库上线,需要连续 rise 次健康检查成功。

检测时序:
  新主提升    首次检查    第二次检查   第三次检查(UP)
     |          |           |           |
     |←─ 0~inter ─→|←─ fast ─→|←─ fast ─→|
  • 最好情况(rise-1) × fastinter
  • 最坏情况(rise-1) × fastinter + inter
  • 平均情况(rise-1) × fastinter + inter/2
Thaproxy={(rise1)×fastinter最好(rise1)×fastinter+inter/2平均(rise1)×fastinter+inter最坏T_{haproxy} = \begin{cases} (rise-1) \times fastinter & \text{最好} \\ (rise-1) \times fastinter + inter/2 & \text{平均} \\ (rise-1) \times fastinter + inter & \text{最坏} \end{cases}

RTO 公式

将各阶段时间相加,得到总 RTO。

由于主库降级 + 租约过期 ≈ ttl,网络分区的 RTO 公式与过期故障相同:

最好情况

RTOmin=ttlloop+0.1+(rise1)×fastinterRTO_{min} = ttl - loop + 0.1 + (rise-1) \times fastinter$$RTO_{min} \approx ttl - loop + (rise-1) \times fastinter$$

平均情况

RTOavg=ttl+1+inter/2+(rise1)×fastinterRTO_{avg} = ttl + 1 + inter/2 + (rise-1) \times fastinter$$RTO_{avg} = ttl + 1 + inter/2 + (rise-1) \times fastinter$$

最坏情况

RTOmax=ttl+loop+2+inter+(rise1)×fastinterRTO_{max} = ttl + loop + 2 + inter + (rise-1) \times fastinter$$RTO_{max} = ttl + loop + 2 + inter + (rise-1) \times fastinter$$

模型计算

将四种 RTO 模型的参数带入上面的公式:

pg_rto_plan:  # [ttl, loop, retry, start, margin, inter, fastinter, downinter, rise, fall]
  fast: [ 20  ,5  ,5  ,15 ,5  ,'1s' ,'0.5s' ,'1s' ,3 ,3 ]  # rto < 30s
  norm: [ 30  ,5  ,10 ,25 ,5  ,'2s' ,'1s'   ,'2s' ,3 ,3 ]  # rto < 45s
  safe: [ 60  ,10 ,20 ,45 ,10 ,'3s' ,'1.5s' ,'3s' ,3 ,3 ]  # rto < 90s
  wide: [ 120 ,20 ,30 ,95 ,15 ,'4s' ,'2s'   ,'4s' ,3 ,3 ]  # rto < 150s

Patroni 约束验证loop + 2×retry ≤ ttl):

模式loopretryTTLloop + 2×retry满足约束?
fast5520s15s✓ 安全
norm51030s25s✓ 安全
safe102060s50s✓ 安全
wide2030120s80s✓ 安全

四种模式计算结果(单位:秒,格式:min / avg / max)

阶段fastnormsafewide
主库降级5 / 8 / 1010 / 13 / 1520 / 25 / 3030 / 40 / 50
租约过期10153070
从库检测0 / 3 / 50 / 3 / 50 / 5 / 100 / 10 / 20
抢锁提拔0 / 1 / 20 / 1 / 20 / 1 / 20 / 1 / 2
健康检查1 / 2 / 22 / 3 / 43 / 5 / 64 / 6 / 8
总计16 / 23 / 2927 / 34 / 4153 / 66 / 78104 / 127 / 150

结论:网络分区的 RTO 与过期故障(节点宕机)相同,因为瓶颈都是 TTL 过期时间。


脑裂防护

网络分区的最大风险是脑裂:老主库可能仍在运行并接受写入。Patroni 提供多重防护机制:

1. 主库自我降级

Patroni 的核心防护机制:当无法刷新 Leader Key 时,主动降级 PostgreSQL。

# Patroni 伪代码逻辑
if not can_refresh_leader_key():
    retry_until(retry_timeout)
    if still_cannot_refresh():
        demote_postgresql()  # 降级为只读或关闭

2. Linux Watchdog

如果 Patroni 进程卡住无法执行降级,Linux watchdog 会强制重启系统。

# patroni.yml 配置
watchdog:
  mode: required  # 要求 watchdog 可用
  device: /dev/watchdog
  safety_margin: 5

3. Fencing 机制

可以配置 fencing 脚本来强制隔离老主库(如关闭网络接口、停止服务等)。


特殊场景

场景 A:主库与 DCS 分区,从库正常

这是最常见的网络分区场景,本文主要分析此场景。

┌─────────┐         ╳         ┌─────────┐
│  主库   │ ←── 分区 ──→ │   DCS   │
│ Patroni │                   │  etcd   │
└─────────┘                   └─────────┘
                                  ↑
                              正常连接
                                  ↓
                              ┌─────────┐
                              │  从库   │
                              │ Patroni │
                              └─────────┘
  • 主库 Patroni 无法刷新 Leader Key → 主动降级
  • 从库正常检测到 TTL 过期 → 竞选成为新主库
  • RTO ≈ 过期故障 RTO

场景 B:主库正常,从库与 DCS 分区

┌─────────┐                   ┌─────────┐
│  主库   │ ←── 正常 ──→ │   DCS   │
│ Patroni │                   │  etcd   │
└─────────┘                   └─────────┘
                                  ╳
                              分区
                                  ╳
                              ┌─────────┐
                              │  从库   │
                              │ Patroni │
                              └─────────┘
  • 主库正常刷新 Leader Key
  • 从库无法参与竞选(但复制仍可继续)
  • 不会触发故障切换,服务继续正常运行

场景 C:所有节点与 DCS 分区

┌─────────┐         ╳         ┌─────────┐
│  主库   │ ←── 分区 ──→ │   DCS   │
│ Patroni │                   │  etcd   │
└─────────┘                   └─────────┘
                                  ╳
┌─────────┐         ╳             │
│  从库   │ ←── 分区 ──────────────┘
│ Patroni │
└─────────┘
  • 主库降级,从库无法竞选
  • 集群完全不可用
  • 需要人工干预恢复 DCS 连接

与其他故障对比

故障类型主库状态租约处理RTO脑裂风险
过期故障节点宕机被动等待 TTL 过期16s ~ 150s
崩溃故障PG 崩溃,Patroni 存活重启超时后主动释放1s ~ 111s
网络分区存活但与 DCS 隔离被动等待 TTL 过期16s ~ 150s有,需防护
人工切换正常或故障直接释放/获取1s ~ 11s

关键洞察:网络分区的 RTO 与过期故障相同,但需要额外的脑裂防护机制。 确保满足 loop_wait + 2 × retry_timeout ≤ ttl 约束是防止脑裂的关键设计。