跳转到主要内容

网络分区

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

RTO 时序图


故障模型

项目 最好 最坏 平均 说明
主库降级 retry loop + retry loop/2 + retry Patroni 检测分区后重试,超时后主动降级
租约过期 ttl - loop - retry ttl - loop - retry ttl - loop - retry 降级后剩余的 TTL 时间(近似常数)
从库检测 0 loop loop/2 最好:恰好在检测点
最坏:刚错过检测点
抢锁提拔 0 2 1 最好:直接抢锁提升
最坏: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 fastinterRTOminttlloop+(rise1)×fastinterRTO_{min} \approx ttl - loop + (rise-1) \times fastinter

平均情况

RTOavg=ttl+1+inter/2+(rise1)×fastinterRTO_{avg} = ttl + 1 + inter/2 + (rise-1) \times fastinterRTOavg=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 fastinterRTOmax=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

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

模式 loop retry TTL loop + 2×retry 满足约束?
fast 5 5 20s 15s ✓ 安全
norm 5 10 30s 25s ✓ 安全
safe 10 20 60s 50s ✓ 安全
wide 20 30 120s 80s ✓ 安全

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

阶段 fast norm safe wide
主库降级 5 / 8 / 10 10 / 13 / 15 20 / 25 / 30 30 / 40 / 50
租约过期 10 15 30 70
从库检测 0 / 3 / 5 0 / 3 / 5 0 / 5 / 10 0 / 10 / 20
抢锁提拔 0 / 1 / 2 0 / 1 / 2 0 / 1 / 2 0 / 1 / 2
健康检查 1 / 2 / 2 2 / 3 / 4 3 / 5 / 6 4 / 6 / 8
总计 16 / 23 / 29 27 / 34 / 41 53 / 66 / 78 104 / 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 约束是防止脑裂的关键设计。