被动故障切换

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

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

最后修改 2026-01-14: add local katex resources (89c8cc2)