这是本节的多页打印视图。 .
PG 高可用
概览
Pigsty 的 PostgreSQL 集群带有开箱即用的高可用方案,由 Patroni、Etcd 和 HAProxy 提供核心能力。
当您的 PostgreSQL 集群含有两个或更多实例时,您无需任何配置即拥有了硬件故障自愈的数据库高可用能力 —— 只要集群中有任意实例存活,集群就可以对外提供完整的服务,而客户端只要连接至集群中的任意节点,即可获得完整的服务,而无需关心主从拓扑变化。
默认 norm 模式的目标 RTO 为 45 秒内;异步复制的 pg_rpo=1MiB 是 Patroni 的候选从库采样落后阈值,并非实际丢失量硬上限。使用 crit.yml 的严格同步模式可让已确认事务在故障切换时保持 RPO = 0。以上行为可通过参数按实际硬件与可靠性要求 配置。
Pigsty 内置了 HAProxy 负载均衡器用于自动流量切换,提供 DNS/VIP/LVS 等多种接入方式供客户端选用。故障切换与主动切换对业务侧除零星闪断外几乎无感知,应用不需要修改连接串重启。 极小的维护窗口需求带来了极大的灵活便利:您完全可以在无需应用配合的情况下滚动维护升级整个集群。硬件故障可以等到第二天再抽空善后处置的特性,让研发,运维与 DBA 都能在故障时安心睡个好觉。

许多大型组织与核心机构已经在生产环境中长时间使用 Pigsty,最大的部署有 25K CPU 核心与 220+ PostgreSQL 超大规格实例(64c / 512g / 3TB NVMe SSD);在这一部署案例中,五年内经历了数十次硬件故障与各类事故,但依然可以保持高于 99.999% 的总体可用性战绩。
高可用(High-Availability)解决什么问题?
- 将 数据安全 C/IA 中的可用性提高到一个新高度:RPO ≈ 0,RTO < 45s。
- 获得无缝滚动维护的能力,最小化维护窗口需求,带来极大便利。
- 硬件故障可以立即自愈,无需人工介入,运维 DBA 可以睡个好觉。
- 从库可以用于承载只读请求,分担主库负载,让资源得以充分利用。
高可用有什么代价?
- 基础设施依赖:高可用需要依赖 DCS (etcd/zk/consul) 提供共识。
- 起步门槛增加:一个有意义的高可用部署环境至少需要 三个节点。
- 额外的资源消耗:一个新从库就要消耗一份额外资源,不算大问题。
- 复杂度代价显著升高:备份成本显著加大,需要使用工具压制复杂度。
高可用的局限性
因为复制实时进⾏,所有变更被⽴即应⽤⾄从库。因此基于流复制的高可用方案⽆法应对⼈为错误与软件缺陷导致的数据误删误改。(例如:DROP TABLE,或 DELETE 数据)
此类故障需要使用 延迟集群,或使用先前的基础备份与 WAL 归档进行 时间点恢复。
| 配置策略 | RTO | RPO |
|---|---|---|
| 单机 + 什么也不做 | 数据永久丢失,无法恢复 | 数据全部丢失 |
| 单机 + 基础备份 | 取决于备份大小与带宽(几小时) | 丢失上一次备份后的数据(几个小时到几天) |
| 单机 + 基础备份 + WAL 归档 | 取决于备份大小与带宽(几小时) | 丢失最后尚未归档的数据(几十 MB) |
| 主从 + 手工故障切换 | 十分钟 | 丢失复制延迟中的数据(约百 KB) |
| 主从 + 自动故障切换 | 一分钟内 | 丢失复制延迟中的数据(约百 KB) |
| 主从 + 自动故障切换 + 同步提交 | 一分钟内 | 无数据丢失 |
原理
在 Pigsty 中,高可用架构的实现原理如下:
- PostgreSQL 使⽤标准流复制搭建物理从库,主库故障时由从库接管。
- Patroni 负责管理 PostgreSQL 服务器进程,处理高可用相关事宜。
- Etcd 提供分布式配置存储(DCS)能力,并用于故障后的领导者选举
- Patroni 依赖 Etcd 达成集群领导者共识,并对外提供健康检查接口。
- HAProxy 对外暴露集群服务,并利⽤ Patroni 健康检查接口,自动分发流量至健康节点。
- vip-manager 提供一个可选的二层 VIP,从 Etcd 中获取领导者信息,并将 VIP 绑定在集群主库所在节点上。
当主库故障时,将触发新一轮领导者竞选,集群中最为健康的从库将胜出(LSN 位点最高,数据损失最小者),并被提升为新的主库。 胜选从库提升后,读写流量将立即路由至新的主库。 主库故障影响是 写服务短暂不可用:从主库故障到新主库提升期间,写入请求将被阻塞或直接失败,不可用时长通常在 15秒 ~ 30秒,通常不会超过 1 分钟。
当从库故障时,只读流量将路由至其他从库,如果所有从库都故障,只读流量才会最终由主库承载。 从库故障的影响是 部分只读查询闪断:当前从库上正在运行查询将由于连接重置而中止,并立即由其他可用从库接管。
故障检测由 Patroni 和 Etcd 共同完成,集群领导者将持有一个租约,
如果集群领导者未能在租约 TTL 内续租(默认 norm 模式为 30 秒),租约会过期并触发 故障切换(Failover)与新一轮选举。
即使没有出现任何故障,您依然可以主动通过 主动切换(Switchover)变更集群的主库。 在这种情况下,主库上的写入查询将会闪断,并立即路由至新主库执行。这一操作通常可用于滚动维护/升级数据库服务器。
1 - RPO 利弊权衡
RPO(Recovery Point Objective,恢复点目标)定义了在主库发生故障时,允许丢失的最大数据量。
对于金融交易这类数据完整性至关重要的场景,通常要求 RPO = 0,即不允许任何数据丢失;
然而更为严格的 RPO 指标是有代价的,它会引入更高的写入延迟,降低系统吞吐量,并且存在从库故障导致主库不可用的风险。 因此对于常规场景,通常可以接受一定量的数据丢失,以换取更高的可用性与性能。
利弊权衡
通常在异步复制场景下,从库和主库之间会存在一定的复制延迟(取决于网络和吞吐量,正常在 10KB-100KB / 100µs-10ms 的数量级), 这意味着当主库发生故障时,从库可能还没有完全同步主库的最新数据。这时候如果出现故障切换,新的主库可能会丢失一些尚未复制的数据。
pg_rpo 会被写入 Patroni 的 maximum_lag_on_failover,默认值为 1048576(1MiB)。它是 候选从库参与竞选时允许的采样落后阈值,不是实际数据丢失量的硬上限。
当集群主库宕机时,如果有任何一个从库的复制延迟在这个值以内,Pigsty 将自动提升该从库为新的主库。 然而当所有从库副本的复制延迟都超出这个阈值时,Pigsty 将拒绝进行 [自动故障切换] 以避免数据丢失。 此时需要人工介入进行决策 —— 等待主库恢复(可能永远也不会恢复),还是接受数据损失并强制提升一个从库为新的主库。
由于主库 WAL 位置并非实时采样,异步复制最坏情况下的实际丢失量还可能包含最近一个 ttl 窗口内产生的 WAL(平均约再加 loop_wait/2 时间内的 WAL)。您需要结合业务写入速率配置该阈值;增大它会提高自动故障切换的成功率,但也会放宽候选资格。
当您指定 pg_rpo = 0 时,Pigsty 将启用 同步复制,确保主库在确认至少一个从库持久化数据后才返回写入成功。
这种配置能确保没有复制延迟,但会带来显著的写入延迟,并降低整体的吞吐量。
flowchart LR
A([主库故障]) --> B{同步复制?}
B -->|否| C{延迟 < RPO?}
B -->|是| D{同步从库<br/>可用?}
C -->|是| E[有损自动故障切换<br/>候选采样落后在阈值内]
C -->|否| F[拒绝自动切换<br/>等待主库恢复<br/>或人工介入决策]
D -->|是| G[无损自动故障切换<br/>RPO = 0]
D -->|否| H{严格模式?}
H -->|否| C
H -->|是| F
style A fill:#dc3545,stroke:#b02a37,color:#fff
style E fill:#F0AD4E,stroke:#146c43,color:#fff
style G fill:#198754,stroke:#146c43,color:#fff
style F fill:#BE002F,stroke:#565e64,color:#fff
保护模式
Pigsty 提供三种保护模式,以帮助用户在不同的 RPO 要求下进行利弊权衡,类似于 Oracle Data Guard 的数据保护模式。
- 默认模式,异步复制,事务提交仅需本地 WAL 持久化,无需等待从库,从库故障对主库完全透明,不影响服务
- 主库故障时可能丢失尚未发送/接收的 WAL;默认候选采样落后阈值为 1MiB,但它不是实际丢失量的硬上限
- 针对性能优化,适用于常规业务场景,容许在故障时损失少量数据。
- 配置有
pg_rpo = 0,启用 Patroni 同步提交模式:synchronous_mode: true - 正常情况下等待至少一个从库确认,实现零数据丢失。当 所有 同步从库故障时,自动降级为异步模式继续服务
- 兼顾数据安全与服务可用性,是生产环境 核心业务 的推荐配置
- 使用
crit.yml模板,启用 Patroni 严格同步模式:synchronous_mode: true/synchronous_mode_strict: true - 当所有同步从库故障时,主库将拒绝写入 以防止数据丢失,事务必须在至少一个从库持久化后才返回成功。
- 适用于金融交易、医疗记录等对数据完整性要求极高的场景
| 名称 | 最大性能 Performance | 最大可用 Availability | 最大保护 Protection |
|---|---|---|---|
| 复制方式 | 异步复制 | 同步复制 | 严格同步复制 |
| 数据丢失 | 可能丢失(复制延迟量) | 正常零丢失,降级少量丢失 | 零丢失 |
| 主库写延迟 | 最低 | 中等(+1 次网络往返) | 中等(+1 次网络往返) |
| 吞吐量 | 最高 | 降低 | 降低 |
| 从库故障影响 | 无影响 | 自动降级,继续服务 | 主库停写 |
| RPO | 可能丢失;默认候选阈值 1MiB | 正常 = 0 / 降级后可能丢失 | = 0 |
| 适用场景 | 常规业务、性能优先 | 重要业务、安全优先 | 金融核心、安全合规第一 |
| 配置方法 | 默认配置 | pg_rpo = 0 |
pg_conf: crit.yml |
实现原理
三种保护模式的区别在于 Patroni 的两个核心参数:synchronous_mode 与 synchronous_mode_strict 如何配置:
synchronous_mode:Patroni 是否启用同步复制,如果启用,再看synchronous_mode_strict是否启用严格同步模式。synchronous_mode_strict = false,默认配置,允许当从库故障时降级为异步模式,主库继续服务(最大可用性)synchronous_mode_strict = true,禁止降级,主库停止写入 直到同步从库恢复(最大保护)
| 模式 | synchronous_mode |
synchronous_mode_strict |
复制模式 | 从库故障行为 |
|---|---|---|---|---|
| 最大性能 | false |
- | 异步复制 | 无影响 |
| 最大可用 | true |
false |
同步复制 | 自动降级为异步 |
| 最大保护 | true |
true |
严格同步复制 | 主库拒绝写入 |
通常情况下,您只需要将 pg_rpo 参数设置为 0,即可打开 synchronous_mode 开关,启用 最大可用性模式。
如果您使用 pg_conf = crit.yml 模板,则会同时额外打开 synchronous_mode_strict 严格模式开关,启用 最大保护模式。
此外,您可以启用 watchdog,在节点/Patroni 假死场景下直接 Fencing 主库而不是降级,实现与 Oracle 最大保护模式相同的行为表现
当然,您可以直接按需 配置 这些 Patroni 参数,您还可以参阅 Patroni 与 PostgreSQL 文档,通过配置实现更强的数据保护,例如:
- 可以指定 同步从库列表,配置更多同步从库以提高容灾能力,使用法定人数同步,甚至要求所有从库都执行同步提交。
- 您可以 配置
synchronous_commit:'remote_apply',严格确保主从读写一致性。(Oracle 最大保护模式相当于remote_write)
配置建议
最大性能模式(异步复制)是 Pigsty 默认使用的模式,对于绝大多数业务来说已经足够使用。
容许故障时丢失少量数据,换来更大的性能吞吐量与服务可用性水平。
在这种情况下,可以通过 pg_rpo 调整候选从库的采样落后阈值;
实际最坏数据损失还取决于写入速率、ttl 与采样时机。
最大可用性模式(同步复制)适用于数据完整性要求高的场景;同步从库正常时可做到已确认事务零丢失(同步副本全部不可用后会降级)
在这种模式下,最少需要一主一从的两节点 PostgreSQL 集群才有意义。
将 pg_rpo 设置为 0 即可启用该模式。
最大保护模式 (严格同步复制) 适用于金融交易、医疗记录等对数据完整性要求极高的场景,我们建议至少使用一主二从的三节点集群, 因为两节点的情况下,只要从库故障,主库就会停止写入,导致业务不可用,这会降低系统的整体可靠性。而三节点的规格下,如果只有一个从库故障,主库仍然可以继续服务。
2 - RTO 利弊权衡
RTO(Recovery Time Objective,恢复时间目标)定义了在主库发生故障时,系统恢复写入能力所需的最长时间。
对于核心交易系统这类可用性至关重要的场景,通常要求 RTO 尽可能短,例如一分钟内。
然而更短的 RTO 指标是有代价的,它会增加误切风险:网络抖动可能被误判为故障,导致不必要的故障切换。 因此对于跨机房/跨地域部署的场景,通常需要放宽 RTO 要求(例如 1-2 分钟),以降低误切风险。
利弊权衡
故障切换时的不可用时长上限由 pg_rto 参数控制。Pigsty 提供了四种预设的 RTO 模式:
fast、norm、safe、wide,分别针对不同的网络条件与部署场景进行了优化,默认使用 norm 模式(约 45 秒)。
当主库发生故障时,整个恢复流程涉及多个阶段:Patroni 检测故障、DCS 锁过期、新主选举、执行 promote、HAProxy 感知新主。 减小 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
四种模式
Pigsty 提供四种 RTO 模式,以帮助用户在不同的网络条件下进行利弊权衡。
| 名称 | fast | norm | safe | wide |
|---|---|---|---|---|
| 适用场景 | 同机柜 | 同机房内(默认) | 同省跨机房 | 跨地域/跨洲 |
| 网络条件 | < 1ms,极稳定 | 1-5ms,正常 | 10-50ms,跨机房 | 100-200ms,公网 |
| 目标 RTO | 30s | 45s | 90s | 150s |
| 误切风险 | 较高 | 中等 | 较低 | 极低 |
| 配置方法 | pg_rto: fast |
pg_rto: norm |
pg_rto: safe |
pg_rto: wide |
- 适用于网络延迟极低(< 1ms)且非常稳定的场景,例如同机柜或同交换机部署
- 平均 RTO: 14s,最坏情况: 29s,TTL 仅 20s,检测间隔 5s
- 对网络质量要求最高,任何抖动都可能触发切换,误切风险较高
- 默认模式,适用于同机房部署,网络延迟 1-5ms,质量正常,丢包率合理
- 平均 RTO: 21s,最坏情况: 43s,TTL 为 30s,提供合理的容错窗口
- 平衡了恢复速度与稳定性,适合绝大多数生产环境
- 适用于同省/同区域跨机房部署,网络延迟 10-50ms,可能存在偶发抖动
- 平均 RTO: 43s,最坏情况: 91s,TTL 为 60s,更长的容错窗口
- 主库重启等待时间较长(60s),给予更多本地恢复机会,误切风险较低
- 适用于跨地域甚至跨大洲部署,网络延迟 100-200ms,可能有公网级别的丢包率
- 平均 RTO: 92s,最坏情况: 207s,TTL 为 120s,极宽的容错窗口
- 牺牲恢复速度换取极低的误切率,适合异地容灾场景
RTO时序图
Patroni / PG HA 有两条关键故障路径:主动故障检测(PG 崩溃后 Patroni 检测到并尝试重启)与 被动租约过期(节点宕机后等待 TTL 过期触发选举)。
tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: $fn:fmt }
legend: { top: 0, itemGap: 10, data: [租约过期, 故障检测, 重启超时, 从库检测, 抢锁提拔, 健康检查] }
grid: { left: 110, right: 24, bottom: 32, top: 40 }
xAxis: { type: value, name: 秒, nameLocation: end, max: 160, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: true, lineStyle: { type: dashed, opacity: 0.5 } }, minorTick: { show: true, splitNumber: 5 }, minorSplitLine: { show: true, lineStyle: { type: dotted, opacity: 0.2 } } }
yAxis: { type: category, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: false }, axisLabel: { fontSize: 9, fontFamily: monospace }, data: [wide-passive-max, wide-passive-avg, wide-passive-min, wide-active-max, wide-active-avg, wide-active-min, "", safe-passive-max, safe-passive-avg, safe-passive-min, safe-active-max, safe-active-avg, safe-active-min, "", norm-passive-max, norm-passive-avg, norm-passive-min, norm-active-max, norm-active-avg, norm-active-min, "", fast-passive-max, fast-passive-avg, fast-passive-min, fast-active-max, fast-active-avg, fast-active-min] }
series:
- { name: 租约过期, type: bar, stack: main, barWidth: 16, z: 2, emphasis: { focus: series }, itemStyle: { color: "#e15759" }, data: [120, 110, 100, "-", "-", "-", "-", 60, 55, 50, "-", "-", "-", "-", 30, 27, 25, "-", "-", "-", "-", 20, 17, 15, "-", "-", "-"] }
- { name: 故障检测, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#b07aa1" }, data: ["-", "-", "-", 20, 10, 0, "-", "-", "-", "-", 10, 5, 0, "-", "-", "-", "-", 5, 3, 0, "-", "-", "-", "-", 5, 3, 0] }
- { name: 重启超时, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#f28e2c" }, data: ["-", "-", "-", 95, 95, 0, "-", "-", "-", "-", 45, 45, 0, "-", "-", "-", "-", 25, 25, 0, "-", "-", "-", "-", 15, 15, 0] }
- { name: 从库检测, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#edc949" }, data: [20, 10, 0, 20, 10, 0, "-", 10, 5, 0, 10, 5, 0, "-", 5, 3, 0, 5, 3, 0, "-", 5, 3, 0, 5, 3, 0] }
- { name: 抢锁提拔, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#59a14f" }, data: [2, 1, 0, 2, 1, 0, "-", 2, 1, 0, 2, 1, 0, "-", 2, 1, 0, 2, 1, 0, "-", 2, 1, 0, 2, 1, 0] }
- { name: 健康检查, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#4e79a7" }, data: [8, 6, 4, 8, 6, 4, "-", 6, 5, 3, 6, 5, 3, "-", 4, 3, 2, 4, 3, 2, "-", 2, 2, 1, 2, 2, 1] }
- { name: RTO总计, type: bar, barGap: "-100%", barWidth: 16, z: 1, itemStyle: { color: "#888", opacity: 0 }, emphasis: { itemStyle: { opacity: 0 } }, data: [150, 127, 104, 145, 122, 4, "-", 78, 66, 53, 73, 61, 3, "-", 41, 34, 27, 41, 35, 2, "-", 29, 23, 16, 29, 24, 1] }
- { name: RTO预算, type: bar, barGap: "-100%", barWidth: 16, z: 0, itemStyle: { color: "rgba(0,0,0,0.08)" }, emphasis: { itemStyle: { color: "rgba(0,0,0,0.12)" } }, data: [150, 150, 150, 150, 150, 150, "-", 90, 90, 90, 90, 90, 90, "-", 45, 45, 45, 45, 45, 45, "-", 30, 30, 30, 30, 30, 30] }实现原理
四种 RTO 模式的区别在于以下 10 个 Patroni 与 HAProxy HA 相关参数如何配置。
| 组件 | 参数 | fast | norm | safe | wide | 说明 |
|---|---|---|---|---|---|---|
patroni |
ttl |
20 | 30 | 60 | 120 | Leader 锁生存时间(秒) |
loop_wait |
5 | 5 | 10 | 20 | HA 循环检查间隔(秒) | |
retry_timeout |
5 | 10 | 20 | 30 | DCS 操作重试超时(秒) | |
primary_start_timeout |
15 | 25 | 45 | 95 | 主库重启等待时间(秒) | |
safety_margin |
5 | 5 | 10 | 15 | Watchdog 安全边际(秒) | |
haproxy |
inter |
1s | 2s | 3s | 4s | 正常状态检查间隔 |
fastinter |
0.5s | 1s | 1.5s | 2s | 状态变化期检查间隔 | |
downinter |
1s | 2s | 3s | 4s | DOWN 状态检查间隔 | |
rise |
3 | 3 | 3 | 3 | 标记 UP 所需连续成功次数 | |
fall |
3 | 3 | 3 | 3 | 标记 DOWN 所需连续失败次数 |
Patroni 参数
ttl:Leader 锁生存时间,主库须在此时间内续租,否则锁过期触发选举,直接决定被动故障的检测延迟。loop_wait:Patroni 主循环间隔,每个循环执行一次健康检查与状态同步,影响故障发现的及时性。retry_timeout:DCS 操作重试超时,网络分区时 Patroni 在此期间持续重试,超时后主库主动降级防止脑裂。primary_start_timeout:PG 崩溃后 Patroni 尝试本地重启的等待时间,超时后释放 Leader 锁触发切换。safety_margin:Watchdog 安全边际,确保故障时有足够时间触发系统重启,避免脑裂。
HAProxy 参数
inter:正常状态下的健康检查间隔,服务状态稳定时使用。fastinter:状态变化期的检查间隔,检测到状态变化时使用更短间隔加速确认。downinter:DOWN 状态下的检查间隔,服务标记为 DOWN 后使用此间隔探测恢复。rise:标记 UP 所需连续成功次数,新主上线后需连续通过rise次检查才能接收流量。fall:标记 DOWN 所需连续失败次数,服务需连续失败fall次才会被标记为 DOWN。
关键约束
Patroni 核心约束:确保主库能在 TTL 过期前完成降级,防止脑裂。
数据汇总
配置建议
fast 模式 适用于对 RTO 要求极高的场景,但需要确保网络质量足够好(延迟 < 1ms,极低丢包率)。 建议仅在同机柜或同交换机部署时使用,并在生产环境充分测试后再启用。
norm 模式(默认)是 Pigsty 默认使用的配置,对于绝大多数同机房部署的业务来说已经足够使用。 按本文模型,被动/主动路径平均约 34/35 秒,同时提供了合理的容错窗口,避免网络抖动导致的误切。
safe 模式 适用于同城跨机房部署,网络延迟较高或存在偶发抖动的场景。 更长的容错窗口可以有效避免网络抖动导致的误切,是跨机房容灾的推荐配置。
wide 模式 适用于跨地域甚至跨大洲部署,网络延迟高且可能存在公网级别的丢包率。 这种场景下,稳定性比恢复速度更重要,因此使用极宽的容错窗口来确保极低的误切率。
| 模式 | 目标 RTO | 被动检测 RTO | 主动检测 RTO | 场景 |
|---|---|---|---|---|
fast |
30 |
16 / 23 / 29 |
1 / 24 / 29 |
同交换机,高质量网络 |
norm |
45 |
27 / 34 / 41 |
2 / 35 / 41 |
默认,同机房,标准网络 |
safe |
90 |
53 / 66 / 78 |
3 / 61 / 73 |
同城双活 / 跨机房容灾 |
wide |
150 |
104 / 127 / 150 |
4 / 122 / 145 |
异地容灾 / 跨国部署 |
default |
326 |
22 / 34 / 46 |
2 / 314 / 326 |
Patroni 默认参数 |
通常只需将 pg_rto 设为模式名称,Pigsty 会自动配置 Patroni 与 HAProxy 参数。
当前模板通过 pg_rto in pg_rto_plan 查找模式;数字或未知键会直接回退到 norm,不应将这种回退当作“按秒数配置”。
配置模式实际上是从 pg_rto_plan 中加载对应参数集,您可以修改或覆盖此配置以实现自定义 RTO 策略。
3 - 故障切换模型
Patroni 故障按故障对象分类可以分为以下 10 类,按照检测路径不同,可以进一步归纳为五类,在本节内详细展开。
| # | 故障场景 | 描述 | 最终走哪条路径 |
|---|---|---|---|
| 1 | PG 进程崩溃 | crash、OOM killed | 主动检测 |
| 2 | PG 拒绝连接 | max_connections | 主动检测 |
| 3 | PG 假活 | 进程在但无响应 | 主动检测 (检测超时) |
| 4 | Patroni 进程崩溃 | kill -9、OOM | 被动检测 |
| 5 | Patroni 假活 | 进程在但卡住 | 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
3.1 - 被动故障切换
infographic list-row-simple-horizontal-arrow
data
title 租约过期故障切换流程
desc 当整个节点宕机,Patroni 无法主动释放租约,只能等待 TTL 过期
items
- label 租约过期
desc Patroni 失联,被动等待主库租约 TTL 过期
icon mingcute/close-circle-fill
- label 从库检测
desc 从库从循环中醒来后发现租约过期,开始竞选
icon mingcute/key-2-fill
- label 抢锁提拔
desc 从库相互比较并抢锁,胜利者提升自己的PG
icon mingcute/radar-fill
- label 健康检查
desc HAPROXY 健康检查发现新主上线,分配流量
icon mingcute/arrow-up-circle-fill
theme light
palette antvRTO 时序图
tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: $fn:fmt }
legend: { top: 0, itemGap: 12, data: [租约过期, 从库检测, 抢锁提拔, 健康检查] }
grid: { left: 64, right: 24, bottom: 32, top: 40 }
xAxis: { type: value, name: 秒, nameLocation: end, max: 160, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: true, lineStyle: { type: dashed, opacity: 0.5 } }, minorTick: { show: true, splitNumber: 5 }, minorSplitLine: { show: true, lineStyle: { type: dotted, opacity: 0.2 } } }
yAxis: { type: category, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: false }, axisLabel: { fontSize: 10, fontFamily: monospace }, data: [wide-max, wide-avg, wide-min, "", safe-max, safe-avg, safe-min, "", norm-max, norm-avg, norm-min, "", fast-max, fast-avg, fast-min] }
series:
- { name: 租约过期, type: bar, stack: main, barWidth: 20, z: 2, emphasis: { focus: series }, itemStyle: { color: "#e15759" }, data: [120, 110, 100, "-", 60, 55, 50, "-", 30, 27, 25, "-", 20, 17, 15] }
- { name: 从库检测, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#edc949" }, data: [20, 10, 0, "-", 10, 5, 0, "-", 5, 3, 0, "-", 5, 3, 0] }
- { name: 抢锁提拔, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#59a14f" }, data: [2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0] }
- { name: 健康检查, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#4e79a7" }, data: [8, 6, 4, "-", 6, 5, 3, "-", 4, 3, 2, "-", 2, 2, 1] }
- { name: RTO总计, type: bar, barGap: "-100%", barWidth: 20, z: 1, itemStyle: { color: "#888", opacity: 0 }, emphasis: { itemStyle: { opacity: 0 } }, data: [150, 127, 104, "-", 78, 66, 53, "-", 41, 34, 27, "-", 29, 23, 16] }
- { name: RTO预算, type: bar, barGap: "-100%", barWidth: 20, z: 0, itemStyle: { color: "rgba(0,0,0,0.08)" }, emphasis: { itemStyle: { color: "rgba(0,0,0,0.12)" } }, data: [150, 150, 150, "-", 90, 90, 90, "-", 45, 45, 45, "-", 30, 30, 30] }故障模型
| 项目 | 最好 | 最坏 | 平均 | 说明 |
|---|---|---|---|---|
| 租约过期 | ttl - loop |
ttl |
ttl - loop/2 |
最好:即将刷新时宕机 最坏:刚刷新完就宕机 |
| 从库检测 | 0 |
loop |
loop / 2 |
最好:恰好在检测点 最坏:刚错过检测点 |
| 抢锁提拔 | 0 |
2 |
1 |
最好:直接抢锁提升 最坏: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 重置为配置值。
- 最好情况:故障发生在即将刷新租约之前(距上次刷新已过
loop),剩余 TTL =ttl - loop - 最坏情况:故障发生在刚刷新租约之后,需等待完整
ttl - 平均情况:
ttl - loop/2
阶段 2:从库检测
从库在 loop_wait 周期醒来后检查 DCS 中的 Leader Key 状态。
- 最好情况:租约过期时从库恰好醒来,等待
0 - 最坏情况:租约过期后从库刚进入睡眠,等待
loop - 平均情况:
loop/2
阶段 3:抢锁提拔
从库发现 Leader Key 过期后,开始竞选过程,获得 Leader Key 的从库执行 pg_ctl promote,将自己提升为新主库。
- 通过 Rest API,并行发起查询,查询各从库的复制位置,通常 10ms,硬编码 2 秒超时。
- 比较 WAL 位置,确定最优候选,各从库尝试创建 Leader Key(CAS 原子操作)
- 执行
pg_ctl promote提升自己为主库(很快,通常忽略不计)
- 最好情况:单从库或直接抢到锁并提升,常数开销
0.1s - 最坏情况:DCS API 调用超时:
2s - 平均情况:
1s常数开销
阶段 4:健康检查
HAProxy 检测新主库上线,需要连续 rise 次健康检查成功。
- 最好情况:新主提升时恰好赶上检查,
(rise-1) × fastinter - 最坏情况:新主提升后刚错过检查,
(rise-1) × fastinter + inter - 平均情况:
(rise-1) × fastinter + inter/2
RTO 公式
将各阶段时间相加,得到总 RTO:
最好情况
平均情况
最坏情况
模型计算
将四种 RTO 模型的参数带入上面的公式:
四种模式计算结果(单位:秒,格式:min / avg / max)
| 阶段 | fast | norm | safe | wide |
|---|---|---|---|---|
| 租约过期 | 15 / 17 / 20 |
25 / 27 / 30 |
50 / 55 / 60 |
100 / 110 / 120 |
| 从库检测 | 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 |
3.2 - 主动故障检测
infographic list-row-simple-horizontal-arrow
data
title 崩溃故障切换流程
desc 当 Patroni 健康,但 PostgreSQL 因故崩溃时的故障切换流程
items
- label 故障检测
desc Patroni 在循环中检测到 PG 崩溃
icon mingcute/close-circle-fill
- label 重启超时
desc Patroni 尝试重启 PG,超时后释放租约
icon mingcute/refresh-2-fill
- label 从库检测
desc 从库从循环中醒来发现租约释放,开始竞选
icon mingcute/key-2-fill
- label 抢锁提拔
desc 从库相互比较并抢锁,胜利者提升自己的 PG
icon mingcute/radar-fill
- label 健康检查
desc HAProxy 健康检查发现新主上线,分配流量
icon mingcute/arrow-up-circle-fill
theme light
palette antvRTO 时序图
tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: $fn:fmt }
legend: { top: 0, itemGap: 12, data: [故障检测, 重启超时, 从库检测, 抢锁提拔, 健康检查] }
grid: { left: 64, right: 24, bottom: 32, top: 40 }
xAxis: { type: value, name: 秒, nameLocation: end, max: 160, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: true, lineStyle: { type: dashed, opacity: 0.5 } }, minorTick: { show: true, splitNumber: 5 }, minorSplitLine: { show: true, lineStyle: { type: dotted, opacity: 0.2 } } }
yAxis: { type: category, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: false }, axisLabel: { fontSize: 10, fontFamily: monospace }, data: [wide-max, wide-avg, wide-min, "", safe-max, safe-avg, safe-min, "", norm-max, norm-avg, norm-min, "", fast-max, fast-avg, fast-min] }
series:
- { name: 故障检测, type: bar, stack: main, barWidth: 20, z: 2, emphasis: { focus: series }, itemStyle: { color: "#b07aa1" }, data: [20, 10, 0, "-", 10, 5, 0, "-", 5, 3, 0, "-", 5, 3, 0] }
- { name: 重启超时, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#f28e2c" }, data: [95, 95, 0, "-", 45, 45, 0, "-", 25, 25, 0, "-", 15, 15, 0] }
- { name: 从库检测, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#edc949" }, data: [20, 10, 0, "-", 10, 5, 0, "-", 5, 3, 0, "-", 5, 3, 0] }
- { name: 抢锁提拔, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#59a14f" }, data: [2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0] }
- { name: 健康检查, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#4e79a7" }, data: [8, 6, 4, "-", 6, 5, 3, "-", 4, 3, 2, "-", 2, 2, 1] }
- { name: RTO总计, type: bar, barGap: "-100%", barWidth: 20, z: 1, itemStyle: { color: "#888", opacity: 0 }, emphasis: { itemStyle: { opacity: 0 } }, data: [145, 122, 4, "-", 73, 61, 3, "-", 41, 35, 2, "-", 29, 24, 1] }
- { name: RTO预算, type: bar, barGap: "-100%", barWidth: 20, z: 0, itemStyle: { color: "rgba(0,0,0,0.08)" }, emphasis: { itemStyle: { color: "rgba(0,0,0,0.12)" } }, data: [150, 150, 150, "-", 90, 90, 90, "-", 45, 45, 45, "-", 30, 30, 30] }故障模型
| 项目 | 最好 | 最坏 | 平均 | 说明 |
|---|---|---|---|---|
| 故障检测 | 0 |
loop |
loop/2 |
最好:PG 恰好在检测前崩溃 最坏:PG 刚检测完就崩溃 |
| 重启超时 | 0 |
start |
start |
最好:PG 瞬间自愈 最坏:等满 start 超时才释放租约 |
| 从库检测 | 0 |
loop |
loop/2 |
最好:恰好在检测点 最坏:刚错过检测点 |
| 抢锁提拔 | 0 |
2 |
1 |
最好:直接抢锁提升 最坏: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 恰好在 Patroni 检测前崩溃,立即被发现,等待
0 - 最坏情况:PG 刚检测完就崩溃,需等待下一个周期,等待
loop - 平均情况:
loop/2
阶段 2:重启超时
Patroni 检测到 PG 崩溃后,会尝试重启 PostgreSQL。此阶段有两种可能的结果:
路径 A:自愈成功(最好情况)
- PG 成功重启,服务恢复
- 不触发故障切换,RTO 极短
- 等待时间:
0(相对于 Failover 路径)
路径 B:需要 Failover(平均/最坏情况)
- 等待
primary_start_timeout超时后 PG 仍未恢复 - Patroni 主动释放 Leader Key
- 等待时间:
start
注意:平均情况假设需要进行故障切换。如果 PG 能够快速自愈,则整体 RTO 会大幅降低。
阶段 3:从库检测
从库在 loop_wait 周期醒来后检查 DCS 中的 Leader Key 状态。当主库 Patroni 释放 Leader Key 后,从库发现后开始竞选。
- 最好情况:租约释放时从库恰好醒来,等待
0 - 最坏情况:租约释放后从库刚进入睡眠,等待
loop - 平均情况:
loop/2
阶段 4:抢锁提拔
从库发现 Leader Key 空缺后,开始竞选过程,获得 Leader Key 的从库执行 pg_ctl promote,将自己提升为新主库。
- 通过 Rest API,并行发起查询,查询各从库的复制位置,通常 10ms,硬编码 2 秒超时。
- 比较 WAL 位置,确定最优候选,各从库尝试创建 Leader Key(CAS 原子操作)
- 执行
pg_ctl promote提升自己为主库(很快,通常忽略不计)
- 最好情况:单从库或直接抢到锁并提升,常数开销
0.1s - 最坏情况:DCS API 调用超时:
2s - 平均情况:
1s常数开销
阶段 5:健康检查
HAProxy 检测新主库上线,需要连续 rise 次健康检查成功。
- 最好情况:新主提升时恰好赶上检查,
(rise-1) × fastinter - 最坏情况:新主提升后刚错过检查,
(rise-1) × fastinter + inter - 平均情况:
(rise-1) × fastinter + inter/2
RTO 公式
将各阶段时间相加,得到总 RTO:
最好情况(PG 瞬间自愈)
平均情况(需要 Failover)
最坏情况
模型计算
将四种 RTO 模型的参数带入上面的公式:
四种模式计算结果(单位:秒,格式:min / avg / max)
| 阶段 | fast | norm | safe | wide |
|---|---|---|---|---|
| 故障检测 | 0 / 3 / 5 |
0 / 3 / 5 |
0 / 5 / 10 |
0 / 10 / 20 |
| 重启超时 | 0 / 15 / 15 |
0 / 25 / 25 |
0 / 45 / 45 |
0 / 95 / 95 |
| 从库检测 | 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 |
| 总计 | 1 / 24 / 29 |
2 / 35 / 41 |
3 / 61 / 73 |
4 / 122 / 145 |
与被动故障对比
| 阶段 | 主动故障(PG 崩溃) | 被动故障(节点宕机) | 说明 |
|---|---|---|---|
| 检测机制 | Patroni 主动检测 | TTL 被动过期 | 主动检测更快发现故障 |
| 核心等待 | start |
ttl |
start 通常小于 ttl,但需要额外的故障检测时间 |
| 租约处理 | 主动释放 | 被动过期 | 主动释放更及时 |
| 自愈可能 | ✅ 有 | ❌ 无 | 主动检测可尝试本地恢复 |
RTO 对比(平均情况):
| 模式 | 主动故障(PG 崩溃) | 被动故障(节点宕机) | 差异 |
|---|---|---|---|
| fast | 24s | 23s | +1s |
| norm | 35s | 34s | +1s |
| safe | 61s | 66s | -5s |
| wide | 122s | 127s | -5s |
分析:在
fast和norm模式下,主动故障的 RTO 略高于被动故障,因为需要等待primary_start_timeout(start); 但在safe和wide模式下,由于start < ttl - loop,主动故障反而更快。 不过主动故障有自愈的可能性,最好情况下 RTO 可以极短。
3.3 - 网络分区
infographic list-row-simple-horizontal-arrow
data
title 网络分区故障切换流程
desc 主库与 DCS 网络分区,Patroni 主动降级防止脑裂,等待 TTL 过期后切换
items
- label 主库降级
desc Patroni 重试超时后主动降级 PG
icon mingcute/shield-fill
- label 租约过期
desc Leader Key TTL 过期
icon mingcute/close-circle-fill
- label 从库检测
desc 从库发现租约过期,开始竞选
icon mingcute/key-2-fill
- label 抢锁提拔
desc 从库抢锁并提升为新主库
icon mingcute/radar-fill
- label 健康检查
desc HAProxy 检测新主上线
icon mingcute/arrow-up-circle-fill
theme light
palette antvRTO 时序图
tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: $fn:fmt }
legend: { top: 0, itemGap: 12, data: [主库降级, 租约过期, 从库检测, 抢锁提拔, 健康检查] }
grid: { left: 64, right: 24, bottom: 32, top: 40 }
xAxis: { type: value, name: 秒, nameLocation: end, max: 160, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: true, lineStyle: { type: dashed, opacity: 0.5 } }, minorTick: { show: true, splitNumber: 5 }, minorSplitLine: { show: true, lineStyle: { type: dotted, opacity: 0.2 } } }
yAxis: { type: category, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: false }, axisLabel: { fontSize: 10, fontFamily: monospace }, data: [wide-max, wide-avg, wide-min, "", safe-max, safe-avg, safe-min, "", norm-max, norm-avg, norm-min, "", fast-max, fast-avg, fast-min] }
series:
- { name: 主库降级, type: bar, stack: main, barWidth: 20, z: 2, emphasis: { focus: series }, itemStyle: { color: "#76b7b2" }, data: [50, 40, 30, "-", 30, 25, 20, "-", 15, 13, 10, "-", 10, 8, 5] }
- { name: 租约过期, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#e15759" }, data: [70, 70, 70, "-", 30, 30, 30, "-", 15, 15, 15, "-", 10, 10, 10] }
- { name: 从库检测, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#edc949" }, data: [20, 10, 0, "-", 10, 5, 0, "-", 5, 3, 0, "-", 5, 3, 0] }
- { name: 抢锁提拔, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#59a14f" }, data: [2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0] }
- { name: 健康检查, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#4e79a7" }, data: [8, 6, 4, "-", 6, 5, 3, "-", 4, 3, 2, "-", 2, 2, 1] }
- { name: RTO总计, type: bar, barGap: "-100%", barWidth: 20, z: 1, itemStyle: { color: "#888", opacity: 0 }, emphasis: { itemStyle: { opacity: 0 } }, data: [150, 127, 104, "-", 78, 66, 53, "-", 41, 34, 27, "-", 29, 23, 16] }
- { name: RTO预算, type: bar, barGap: "-100%", barWidth: 20, z: 0, itemStyle: { color: "rgba(0,0,0,0.08)" }, emphasis: { itemStyle: { color: "rgba(0,0,0,0.12)" } }, data: [150, 150, 150, "-", 90, 90, 90, "-", 45, 45, 45, "-", 30, 30, 30] }故障模型
| 项目 | 最好 | 最坏 | 平均 | 说明 |
|---|---|---|---|---|
| 主库降级 | 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_wait周期才能检测到 - 重试阶段:Patroni 会在
retry_timeout期间持续重试 DCS 操作 - 主动降级:重试超时后,Patroni 主动降级 PostgreSQL(防止脑裂)
关键设计:Patroni 要求参数满足约束 loop_wait + 2 × retry_timeout ≤ ttl,确保主库在 TTL 过期之前完成降级。
阶段 2:租约过期
主库降级后,Leader Key 仍然存在于 DCS 中,需要等待 TTL 自然过期。
由于主库已经降级,此阶段的等待时间是 TTL 剩余时间。由于分区检测和 TTL 剩余时间是负相关的(分区发生得越早,检测越慢,但 TTL 剩余越长),两者相加是常数:
注意:主库降级 + 租约过期的总时间仍然约等于 ttl,与过期故障相同。
阶段 3:从库检测
从库在 loop_wait 周期醒来后检查 DCS 中的 Leader Key 状态。
- 最好情况:租约过期时从库恰好醒来,等待
0 - 最坏情况:租约过期后从库刚进入睡眠,等待
loop - 平均情况:
loop/2
阶段 4:抢锁提拔
从库发现 Leader Key 过期后,开始竞选过程。
- 最好情况:单从库或直接抢到锁并提升,
≈ 0 - 最坏情况:DCS API 调用超时,
2s - 平均情况:
1s
阶段 5:健康检查
HAProxy 检测新主库上线,需要连续 rise 次健康检查成功。
- 最好情况:
(rise-1) × fastinter - 最坏情况:
(rise-1) × fastinter + inter - 平均情况:
(rise-1) × fastinter + inter/2
RTO 公式
将各阶段时间相加,得到总 RTO。
由于主库降级 + 租约过期 ≈ ttl,网络分区的 RTO 公式与过期故障相同:
最好情况
平均情况
最坏情况
模型计算
将四种 RTO 模型的参数带入上面的公式:
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。
2. Linux Watchdog
如果 Patroni 进程卡住无法执行降级,Linux watchdog 会强制重启系统。
3. Fencing 机制
可以配置 fencing 脚本来强制隔离老主库(如关闭网络接口、停止服务等)。
特殊场景
场景 A:主库与 DCS 分区,从库正常
这是最常见的网络分区场景,本文主要分析此场景。
- 主库 Patroni 无法刷新 Leader Key → 主动降级
- 从库正常检测到 TTL 过期 → 竞选成为新主库
- RTO ≈ 过期故障 RTO
场景 B:主库正常,从库与 DCS 分区
- 主库正常刷新 Leader Key
- 从库无法参与竞选(但复制仍可继续)
- 不会触发故障切换,服务继续正常运行
场景 C:所有节点与 DCS 分区
- 主库降级,从库无法竞选
- 集群完全不可用
- 需要人工干预恢复 DCS 连接
与其他故障对比
| 故障类型 | 主库状态 | 租约处理 | RTO | 脑裂风险 |
|---|---|---|---|---|
| 过期故障 | 节点宕机 | 被动等待 TTL 过期 | 16s ~ 150s | 无 |
| 崩溃故障 | PG 崩溃,Patroni 存活 | 重启超时后主动释放 | 1s ~ 111s | 无 |
| 网络分区 | 存活但与 DCS 隔离 | 被动等待 TTL 过期 | 16s ~ 150s | 有,需防护 |
| 人工切换 | 正常或故障 | 直接释放/获取 | 1s ~ 11s | 无 |
关键洞察:网络分区的 RTO 与过期故障相同,但需要额外的脑裂防护机制。
确保满足 loop_wait + 2 × retry_timeout ≤ ttl 约束是防止脑裂的关键设计。
4 - 服务接入
分离读写操作,正确路由流量,稳定可靠地交付 PostgreSQL 集群提供的能力。
服务 是一种抽象:它是数据库集群对外提供能力的形式,并封装了底层集群的细节。
服务对于生产环境中的 稳定接入 至关重要,在 高可用 集群自动故障时方显其价值,单机用户 通常不需要操心这个概念。
单机用户
“服务” 的概念是给生产环境用的,个人用户/单机集群可以不折腾,直接拿实例名/IP 地址访问数据库。
例如,Pigsty 默认的单节点 pg-meta.meta 数据库,就可以直接用下面三个不同的用户连接上去。
服务概述
在真实世界生产环境中,我们会使用基于复制的主从数据库集群。集群中有且仅有一个实例作为领导者(主库)可以接受写入。 而其他实例(从库)则会从持续从集群领导者获取变更日志,与领导者保持一致。同时,从库还可以承载只读请求,在读多写少的场景下可以显著分担主库的负担, 因此对集群的写入请求与只读请求进行区分,是一种十分常见的实践。
此外对于高频短连接的生产环境,我们还会通过连接池中间件(Pgbouncer)对请求进行池化,减少连接与后端进程的创建开销。但对于 ETL 与变更执行等场景,我们又需要绕过连接池,直接访问数据库。 同时,高可用集群在故障时会出现故障切换(Failover),故障切换会导致集群的领导者出现变更。因此高可用的数据库方案要求写入流量可以自动适配集群的领导者变化。 这些不同的访问需求(读写分离,池化与直连,故障切换自动适配)最终抽象出 服务 (Service)的概念。
通常来说,数据库集群都必须提供这种最基础的服务:
- 读写服务(primary):可以读写数据库
对于生产数据库集群,至少应当提供这两种服务:
- 读写服务(primary):写入数据:只能由主库所承载。
- 只读服务(replica):读取数据:可以由从库承载,没有从库时也可由主库承载
此外,根据具体的业务场景,可能还会有其他的服务,例如:
- 默认直连服务(default):允许(管理)用户,绕过连接池直接访问数据库的服务
- 离线从库服务(offline):不承接线上只读流量的专用从库,用于 ETL 与分析查询
- 同步从库服务(standby):没有复制延迟的只读服务,由 同步备库 /主库处理只读查询
- 延迟从库服务(delayed):访问同一个集群在一段时间之前的旧数据,由 延迟从库 来处理
接入服务
Pigsty 的服务交付边界止步于集群的 HAProxy,用户可以用各种手段访问这些负载均衡器。
典型的做法是使用 DNS 或 VIP 接入,将其绑定在集群所有或任意数量的负载均衡器上。

你可以使用不同的 主机 & 端口 组合,它们以不同的方式提供 PostgreSQL 服务。
主机
| 类型 | 样例 | 描述 |
|---|---|---|
| 集群域名 | pg-test |
由 infra 节点上的 dnsmasq 解析;pg_dns_target: auto 时,有 VIP 则指向 VIP,否则指向主库 IP |
| 集群 VIP 地址 | 10.10.10.3 |
启用 pg_vip_enabled 后,由 vip-manager 管理并绑定到主节点的 L2 VIP |
| 实例主机名 | pg-test-1 |
通过任何实例主机名访问(由 dnsmasq @ infra 节点解析) |
| 实例 IP 地址 | 10.10.10.11 |
访问任何实例的 IP 地址 |
端口
Pigsty 使用不同的 端口 来区分 pg services
| 端口 | 服务 | 类型 | 描述 |
|---|---|---|---|
| 5432 | postgres | 数据库 | 直接访问 postgres 服务器 |
| 6432 | pgbouncer | 中间件 | 访问 postgres 前先通过连接池中间件 |
| 5433 | primary | 服务 | 访问主 pgbouncer (或 postgres) |
| 5434 | replica | 服务 | 访问备份 pgbouncer (或 postgres) |
| 5436 | default | 服务 | 访问主 postgres |
| 5438 | offline | 服务 | 访问离线 postgres |
组合