PostgreSQL 高可用集群:流复制、Patroni 与 Pgpool-II

概述

前文《PostgreSQL 架构核心》深入剖析了 WAL 机制在崩溃恢复中的核心作用,而《PostgreSQL 分区表与逻辑复制实战》则展示了逻辑复制的灵活数据同步能力。本文将这些底层机制串联起来,构建真正生产级的高可用集群。从物理流复制的主备数据同步,到 repmgr 和 Patroni 的自动故障转移,再到 Pgpool-II 的读写分离与负载均衡,完整覆盖 PG 高可用技术栈的每一个关键环节。

数据库是企业应用的命脉,其高可用性直接影响业务的连续性。PostgreSQL 的高可用方案经历了从手动切换、脚本化到全自动化的演进,如今已形成以 Patroni + etcd 为核心、repmgr 轻量替代、Pgpool-II 读写分离为辅助的成熟技术栈。本文将逐层拆解物理流复制的 WAL 传输机制、repmgr 的自动故障转移原理、Patroni 的分布式协调架构以及 Pgpool-II 的中间件路由策略,并通过真实的宕机演练和脑裂模拟,让读者深入理解这些高可用组件是如何在故障场景下保障数据不丢失、服务不中断的。

核心要点

  • 物理流复制:WAL Sender/Receiver 原理、同步/异步复制、Replication Slot。
  • repmgr 故障转移repmgrd 守护进程监控、自动 Failover 流程、Witness Server 防脑裂。
  • Patroni + etcd :分布式共识 Leader Key 机制、patronictl 管理、REST API。
  • Pgpool-II 读写分离:SQL 路由、连接池、与 Patroni 集成感知主备切换。
  • 故障模拟:主库宕机自动恢复、脑裂场景与仲裁、Pgpool-II 自动感知 Failover。

文章组织架构图

flowchart TB subgraph S1 ["基础复制层"] n1["1. 物理流复制深度
WAL Sender、同步复制与 Replication Slot"] end subgraph S2 ["高可用方案层"] n2["2. repmgr 自动故障转移
守护进程、选举与 Witness Server"] n3["3. Patroni + etcd 分布式协调
云原生高可用"] n4["4. Pgpool-II 读写分离与连接池实战
中间件路由策略"] end subgraph S3 ["工程实践与总结"] n5["5. 三种高可用方案对比与选型建议"] n6["6. 故障模拟与演练
宕机、脑裂与自动恢复"] n7["7. 面试高频专题
原理、实践与系统设计"] end n1 --> n2 n1 --> n3 n2 --> n5 n3 --> n5 n4 --> n5 n2 --> n6 n3 --> n6 n4 --> n6 n5 --> n7 n6 --> n7 classDef topic fill:#f8f9fa,stroke:#333,stroke-width:2px,rx:5,color:#333; class n1,n2,n3,n4,n5,n6,n7 topic; class S1,S2,S3 topic;

架构图分层说明

  • 总览说明:全文 7 个模块从物理流复制基础开始,逐步深入到 repmgr、Patroni 和 Pgpool-II 三种主流方案,再通过方案对比、故障模拟和面试题完成闭环。基础层是核心,高可用方案层是手段,工程实践与总结层是目的。
  • 逐模块说明
    • 模块 1 建立复制基础,是后续所有高可用方案的数据同步基石。
    • 模块 2-4 逐一剖析三种高可用组件,从轻量级的 repmgr 到云原生标准的 Patroni,再到读写分离中间件 Pgpool-II。
    • 模块 5 提供选型决策依据,帮助工程师在不同场景下做出合理选择。
    • 模块 6 通过真实故障模拟验证理论,将知识转化为实战能力。
    • 模块 7 面试巩固,帮助读者系统化理解高频问题。
  • 关键结论PostgreSQL 的高可用架构核心在于 WAL 流复制 + 自动故障转移 + 读写分离的组合。Patroni + etcd 是云原生环境的首选,repmgr 适合轻量部署,Pgpool-II 是读写分离与连接池的成熟方案。

1. 物理流复制深度:WAL Sender、同步复制与 Replication Slot

物理流复制是 PostgreSQL 高可用体系的基石。它并非简单的文件拷贝,而是一种基于 WAL(Write-Ahead Log)记录的、近乎实时的、物理级别的数据同步机制。与我们前文(详见 PostgreSQL 系列第 3 篇)探讨的 WAL 机制不同,流复制是这一机制在网络环境下的延伸,将 WAL 的"时间旅行"能力从一个实例扩展到了多个实例。

1.1 WAL Sender/Receiver 的数据传输机制

物理流复制的核心是主备库之间的一条 TCP 长连接,其上运行着一个精简而高效的数据流协议。这个协议的主角是两个内部进程:WAL SenderWAL Receiver

1.1.1 启动流程与握手

当一个备库启动时,它会经历一个从"追赶"到"跟随"的状态转换。

  1. 备库发起连接 :备库的启动进程(Startup Process) 根据 primary_conninfo 配置项指定的连接字符串,向主库发起一个标准的 PostgreSQL 客户端连接。在连接参数中,它会声明自己的身份、要复制的 WAL 起始位置(LSN)以及复制槽名称(如果使用)。
  2. 主库启动 WAL Sender :主库的WAL Sender 进程 在收到复制请求后,会首先进行握手。它检查 pg_hba.conf 中的 replication 权限条目,验证请求者身份。一旦通过,WAL Sender 就成为一个专门为该备库服务的后端进程,其生命周期与复制连接绑定。
  3. 握手与参数协商 :主库与备库之间会进行协议参数协商,包括:
    • Timeline(时间线):这是 PostgreSQL 为防止在恢复/提升后产生WAL历史混乱而设计的关键机制。备库告知当前所处的时间线,主库会判断是否兼容。
    • START_REPLICATION 命令:备库发出此命令,告诉主库从哪个 LSN(Log Sequence Number,即 WAL 日志序列号)位置开始发送 WAL 流。这个 LSN 可以是备库本地最新的WAL结束位置,也可以是复制槽中记录的位置。

1.1.2 流式传输

握手完成后,流复制进入连续传输阶段,这是其性能的核心。WAL Sender 的工作不再是简单的"读取-发送",而是与 WAL Writer 进程精密协作。

  • 主库侧(WAL Sender) :WAL Sender 进入一个循环,等待新的 WAL 记录产生。当有事务提交时,WAL Writer 将 WAL 记录写入磁盘上的 WAL 段文件,并立即通知 WAL Sender。WAL Sender 无需轮询,而是通过内部的闩锁(Latch) 机制被唤醒,然后从共享内存的 WAL 缓冲区(WAL Buffer) 或磁盘上的 WAL 文件中读取新的记录,打包并发送给备库。
  • 备库侧(WAL Receiver) :备库的 WAL Receiver 进程通过 recv() 系统调用持续接收网络数据包。它首先将接收到的 WAL 记录写入本地磁盘的 WAL 段文件,这个过程保证了即使备库突然崩溃,已经接收到的 WAL 数据也不会丢失。接着,WAL Receiver 会通过共享内存向启动进程(Startup Process)发送信号,通知其有新的 WAL 日志可被应用。
sequenceDiagram participant PS as 主库: Startup Process participant WALS as 主库: WAL Sender (forked) participant WALW as 主库: WAL Writer participant OS as 主库: WAL Segments (Disk) participant SS as 备库: Startup Process participant WALR as 备库: WAL Receiver participant RPS as 备库: Recovery Startup Process (Apply) Note over PS, RPS: 初始连接与握手阶段 PS->>WALS: 连接请求 (身份, Slot, start LSN) WALS->>WALS: 检查 pg_hba.conf (replication权限) WALS-->>PS: Handshake Response (Timeline, OK) PS->>SS: 备库启动逻辑已完成,进入流复制模式 Note over PS, RPS: 连续流复制阶段 loop 事务流 WALW->>WALW: 事务Commit,生成WAL记录 WALW->>OS: 写入WAL Segment文件 WALW->>WALS: 唤醒 (Latch) WALS->>OS: 读取新WAL记录 WALS->>WALR: 持续发送 WAL Stream WALR->>OS: 1. 写入本地 WAL Segments WALR->>RPS: 2. 信号通知有新WAL RPS->>OS: 读取并应用WAL记录到数据文件 end

图 1:物理流复制数据流序列图

  • 图表主旨概括:此序列图清晰展示了从备库发起连接到进入稳态流复制,以及主备库各进程如何协作完成一条WAL记录的传输、接收和应用过程。
  • 逐层/逐元素分解
    • 生命周期WAL Sender 进程不是常驻的,而是为每个新的备库连接动态 fork 出来的,其生命周期与连接绑定。
    • 协作机制 :主库侧的 WAL Writer 与 WAL Sender 通过 Latch 机制进行异步唤醒,避免了昂贵且低效的轮询。
    • 双写机制(备库)WAL Receiver 负责"写WAL",Recovery Startup Process 负责"应用WAL"。这种分离保证了即使在应用过程中发生错误,已落盘的 WAL 数据依然安全。
  • 设计原理映射 :这是生产者-消费者模式的完美体现。主库是 WAL 生产者,备库是消费者。通过 TCP 流式传输和信号量唤醒,实现了高效、低延迟的跨进程、跨网络的数据同步。
  • 工程联系与关键结论此流程是理解所有高可用切换的基础。在 Failover 发生时,原主库宕机,WAL Sender 进程消失,备库的 WAL Receiver 会因连接中断而报错,这恰好是 repmgr/Patroni 监控服务检测到主库故障的直接信号。

1.2 同步复制与异步复制的数据一致性权衡

数据的安全性与系统的性能,在分布式系统中是永恒的矛盾。PostgreSQL 通过 synchronous_commit 参数,将这一选择权交给了 DBA,使其可以根据业务场景在二者间精细权衡。

参数值 事务提交成功条件 数据安全性 性能影响
off WAL 写入主库 WAL Buffer (不等刷盘) 最低,主库崩溃可能丢失已提交事务 最低
local WAL 刷盘到主库本地磁盘 (fsync) 中,主库崩溃不会丢失,主库服务器故障会丢失
remote_write WAL 已发送到备库 OS 缓冲区,但未刷盘 较高,备库 OS 崩溃会丢失
on WAL 已刷盘到一台同步备库的磁盘 高,备库实例崩溃不丢失
remote_apply 备库已应用该事务,数据在其上可见 最高,保证主备强一致读 最高

1.2.1 异步复制

异步复制(synchronous_commit = offlocal)是默认的行为,也常是追求极致性能时的选择。主库提交事务时,完全不等待备库的任何确认。

  • 内部实现 :事务提交时,WAL Writer 只管写入(本地 WAL Buffer/磁盘),WAL Sender 是异步向备库传输。主库与备库之间始终存在一个与负载相关的延迟窗口。
  • 风险:如果主库发生不可恢复的硬件故障,在那个延迟窗口内已提交但未传输到备库的事务,将永久丢失。这在金融等对数据一致性要求极高的场景中是不可接受的。
  • 适用场景:报表系统、非关键业务的有状态应用、异地灾备(容忍分钟级数据丢失)。

1.2.2 同步复制

同步复制保证了在任何时刻,至少有一个备库与主库在事务提交上保持一致。

  • 核心配置 synchronous_standby_names :此参数定义了哪些备库可以作为同步备库,并采用何种策略。其语法非常灵活:

    ini 复制代码
    # FIRST num (num_sync):等待列表中前num个备库的响应
    # ANY num (num_sync):等待列表中任意num个备库的响应
    # 例如:
    synchronous_standby_names = 'FIRST 2 (sby1, sby2, sby3)'
    # 含义: 必须等待 sby1 和 sby2 这两个优先级最高的备库确认。
    
    synchronous_standby_names = 'ANY 1 (sby1, sby2, sby3)'
    # 含义: 只需等待 sby1、sby2、sby3 中任意一个确认即可。
  • 内部实现 :当一个设置了 synchronous_commit = on 的事务提交时,WAL Sender 会立即将该次提交的WAL记录发送给所有同步备库。主库的 WAL Sender 进程会阻塞等待 ,直到收到满足 synchronous_standby_names 策略的备库们发回的确认(ACK)。只有收到足够的 ACK 后,WAL Sender 才会通知主库后端进程事务提交成功,用户客户端才会收到响应。

  • remote_apply 的强一致性 :这是同步复制的最高级别。on 只保证备库已刷盘,但若此时在备库查询,可能依然看不到新提交的数据。remote_apply 则要求备库不仅要刷盘,还要应用该事务。这保证了主库提交成功后,用户可以立刻在同步备库上读到这条新数据,实现了真正的读写强一致性。

1.3 物理复制槽:防止 WAL 过早清理的守护者

在高可用架构中,最令人头疼的场景之一便是:一个备库因维护、网络抖动或故障离线了一段时间,当它重新上线时,发现所需的主库 WAL 日志已经被清理回收了。此时,唯一的办法就是对该备库进行全量重建,耗时巨大。物理复制槽(Physical Replication Slot) 正是为此而生。

  • 原理 :复制槽本质上是在主库上为每个下游消费者(如备库)维护的一个"水位线"。它以 restart_lsn 指针的形式存在,告诉主库:"这个位置之后的 WAL,我一个都不能丢,不要清理!"

  • 内部机制

    1. 创建:在主库执行 SELECT pg_create_physical_replication_slot('slot_for_sby1');。这会在 pg_replslot/slot_for_sby1/ 目录下创建元数据,并在共享内存中注册。
    2. 使用:备库在 primary_conninforecovery.conf (PostgreSQL 12之前版本)中配置 primary_slot_name = 'slot_for_sby1'。连接时,主库会将该备库的进程与这个槽位关联。
    3. 向前推进:每当备库接收并确认(flush)了某个位置的 WAL 后,它会向主库报告其最新的 flush_lsn。主库收到报告后,会将复制槽的 restart_lsn 推进到备库确认的安全位置。
    4. 断连保护:当备库离线时,报告中断,复制槽的 restart_lsn 就停在了它离线前的位置。主库的检查点(Checkpointer)进程在执行 WAL 清理时,会遍历所有活跃的复制槽,并永远不会清理 小于任何 restart_lsn 的 WAL 文件。
  • 监控与风险

    sql 复制代码
    SELECT slot_name, slot_type, active, restart_lsn, 
           pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
    FROM pg_replication_slots;
    • 关键风险-主库磁盘被撑爆:如果备库长时间离线,主库的 WAL 会不断堆积,直至磁盘空间耗尽,导致主库也陷入瘫痪。这是工程实践中必须严防的底线。
    • 最佳实践
      • 必须对所有同步组件设置磁盘使用率的监控与告警。
      • 为复制槽设置 max_slot_wal_keep_size(来自 PG 13+),限制一个槽位最多能保留的 WAL 大小。当超过此限制,主库会失效并删除该复制槽,避免自身宕机。备库再次连接时会发现槽不存在,触发重建。

1.4 基础流复制环境搭建与计划内切换

1.4.1 环境搭建

主库 (postgresql.conf) 核心配置段解读:

ini 复制代码
# 1. 基础WAL设置 (详见系列第3篇)
wal_level = replica          # 必须至少为 'replica',以生成足够信息
max_wal_senders = 10         # 允许的最大并发 WAL Sender 连接数,需 >= 备库数+预留

# 2. 复制槽设置
max_replication_slots = 10   # 允许的最大复制槽数量

# 3. 同步复制设置 (此处以异步为例,注掉同步参数)
# synchronous_standby_names = 'ANY 1 (sby1, sby2)'

# 4. 连接与认证
# pg_hba.conf 需要添加 replication 条目:
# host    replication     repuser         192.168.1.0/24      scram-sha-256

备库搭建脚本解读 (pg_basebackup 方式):

bash 复制代码
#!/bin/bash
# 清理备库数据目录 (危险操作,生产环境请三思)
rm -rf /var/lib/postgresql/16/main/*

# 使用 pg_basebackup 从主库拉取基础备份
# -h: 主库地址
# -D: 备库数据目录
# -U: 复制账号
# -P: 显示进度
# -R: 自动生成 standby.signal 文件和 primary_conninfo 配置到 postgresql.auto.conf
pg_basebackup -h primary_host -D /var/lib/postgresql/16/main -U repuser -P -R

# pg_basebackup -R 做完后,会在 $PGDATA 下生成 standby.signal 标识文件,
# 并在 postgresql.auto.conf 中写入如下类似配置:
echo "primary_conninfo = 'host=primary_host port=5432 user=repuser password=rep_pass'" >> /var/lib/postgresql/16/main/postgresql.auto.conf
# 可选:指定使用的复制槽
echo "primary_slot_name = 'slot_for_sby1'" >> /var/lib/postgresql/16/main/postgresql.auto.conf

# 启动备库
pg_ctl -D /var/lib/postgresql/16/main start
  • standby.signal vs recovery.signal :在 PG 12+ 版本中,这两个文件是进入恢复模式的标识。
    • recovery.signal :指示 PostgreSQL 进入归档恢复 模式。它会应用 archive_command 拉取的归档 WAL,或者在 recovery_target_* 指定的时间点/事务点停止。这是为 PITR(Point-In-Time Recovery,即时间点恢复)准备的。
    • standby.signal :指示 PostgreSQL 进入备库模式。它会忽略任何恢复目标,循环不断地应用来自归档目录和流复制的 WAL,永远跟随主库。
  • pg_basebackup -R 会自动创建 standby.signal,因为我们是为了建立一个流复制备库,需要它永远跟随。

1.4.2 计划内主备切换(Switchover)

计划内切换是数据库运维的必备技能,用于主库硬件升级、内核升级等场景。它允许零数据丢失的优雅切换。

步骤与原理:

  1. 旧主库优雅关闭(pg_ctl -m fast stop

    • -m fast: 终止所有活跃连接,回滚未完成的事务,执行一次 Checkpoint后关闭。这确保了数据文件的完整性,且在关闭前所有的 WAL 都已刷盘。
  2. 检查备库同步状态

    • 在切换前,必须在旧主库上反复确认备库已追上。
    sql 复制代码
    SELECT application_name, sync_state, pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS sent_lag,
           pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS flush_lag
    FROM pg_stat_replication;
    -- 确保 flush_lag 为 0,表示所有数据已同步,可以做到零数据丢失。
  3. 将选定的备库提升为新主库(pg_ctl promote

    • 这是最关键的一步。在选定的备库上执行 pg_ctl -D /var/lib/postgresql/16/main promote
    • 内部原理pg_ctl promote 并非执行新的命令,而是在备库的数据目录创建一个 promote.signal 文件,然后向备库的启动进程发送 SIGUSR1 信号。启动进程收到信号并检测到文件后,会完成当前WAL段的应用,然后立刻切换角色:将 standby.signal 标记为已失效,关闭恢复模式,启动写事务的能力,并时间线(Timeline)增加 1
  4. 将旧主库配置为新备库

    • 在旧主库上,创建 standby.signal 文件,并将 primary_conninfo 指向新主库的地址。启动旧主库,它将作为新主库的一个备库重新加入集群。

2. repmgr 自动故障转移:守护进程、选举与 Witness Server

在完成了手工搭建和切换流复制环境后,我们将进入自动化的世界。repmgr 是一款 PostgeSQL 原生社区中最流行的轻量级集群管理工具,它通过守护进程和命令行,将复杂的手动 Failover 流程自动化。

2.1 repmgr 整体架构与 repmgrd 守护进程

repmgr 的架构由两大组件构成:

  • repmgr 命令行工具:用于管理员的配置、注册、监控和手动切换操作。
  • repmgrd 守护进程:在每个 PG 节点上运行,负责持续监控、自动故障检测和故障转移执行。

repmgr.conf 核心配置解读:

ini 复制代码
# 每个节点的唯一标识,集群内不可重复
node_id=1
# 此节点的名称,用于日志和显示
node_name='pg-node-1'
# 该节点的 PostgreSQL 连接信息,用于 repmgr 元数据操作
conninfo='host=192.168.1.1 port=5432 user=repmgr dbname=repmgr'
# 该节点的 PG 数据目录
data_directory='/var/lib/postgresql/16/main'
# 指示 repmgrd 在启动时自动跟随新主库(常用于执行failover后)
promote_command='pg_ctl -D /var/lib/postgresql/16/main promote'
follow_command='pg_ctl -D /var/lib/postgresql/16/main -w start'
# 监控主库的连接信息,repmgrd 通过此连接进行健康检查
primary_conninfo='host=192.168.1.1 port=5432 user=repmgr dbname=repmgr'
# 使用物理复制槽来防止WAL被过早清理
use_replication_slots=1
# 故障转移阈值(秒):主库在这段时间内无响应,触发自动Failover
reconnect_interval=10
reconnect_attempts=6

repmgrd 的工作原理repmgrd 是一个运行在每个 PG 节点上的守护进程,其核心工作逻辑是一个事件循环:

  1. 本地监控:它通过本地 Unix Socket 定期轮询本地的 PostgreSQL 进程状态。
  2. 主库心跳 :它维护一条到当前记录的主库节点的连接,并执行轻量级查询(如 SELECT 1)作为心跳检查。
  3. 状态上报 :它将本地节点的健康状态、WAL 位置等信息写入到每个节点的本地 repmgr 元数据库中(该库通过流复制同步)。
  4. 故障检测 :当备库上的 repmgrd 检测到与主库的心跳连接连续失败(超过 reconnect_attempts * reconnect_interval)时,它会与其他备库节点通信,发起故障转移。

2.2 自动 Failover 流程

sequenceDiagram participant D1 as repmgrd on Standby 1 (S1) participant D2 as repmgrd on Standby 2 (S2) participant W as repmgrd on Witness participant S1_PG as PostgreSQL on S1 participant S2_PG as PostgreSQL on S2 participant META as Shared repmgr Meta DB Note over D1, META: 故障检测与选举阶段 loop 持续监控 D1->>META: 心跳连接主库 (Standby?) W->>META: 心跳连接主库 (Witness?) end META--xD1: 心跳超时! META--xW: 心跳超时! D1->>D2: 1. 广播"主库疑似故障",获取其他节点确认 ("投票") D2->>META: 2. D2 也尝试连接主库 META--xD2: 确认主库故障 D2->>D1: 3. 发送确认信息 W->>D1: 4. Witness 节点也投下关键一票 D1->>D1: 5. 计算选举:S1 的 priority=100, LSN 最新 -> S1 获胜 Note over D1, S2_PG: 故障转移与跟随阶段 D1->>S1_PG: 6. 执行 promote_command (pg_ctl promote) S1_PG-->>S1_PG: 角色变为 Primary D1->>D1: 7. 更新本地元数据,S1 为新主库 D1->>D2: 8. 通知:S1 已成为新主库 D2->>S2_PG: 9. 执行 follow_command,更改 primary_conninfo S2_PG-->>S2_PG: 重启并跟随新主库S1

图 2:repmgr 自动 Failover 序列图

  • 图表主旨概括:此序列图清晰地展示了从主库故障被检测到,到选举出胜出者并完成集群重建的全过程。
  • 逐层/逐元素分解
    • 故障检测 :依赖 repmgrd 之间的通讯来确认故障,避免单点误判(网络抖动)。
    • 选举算法 :规则简单明确------priority(节点配置的优先级,手动设置,越小越优先)最高,若相同则 LSN 最新(即数据最新的节点)胜出。
    • Witness Server :它不存储数据,只运行 repmgrd 服务并拥有投票权。在只有两个数据节点的集群中至关重要,可以打破平局,防止脑裂。
    • 跟随机制 :Failover 成功后,repmgrd 通过 follow_command 自动将其他备库重定向到新主库。
  • 设计原理映射 :此流程是一个典型的Quorum(法定人数)选举算法repmgrd 充当集群成员的Observer,采用"询问-确认"模式,只有在多数派达成共识后才执行操作,以避免出现多个主库。
  • 工程联系与关键结论Witness Server 自身不存储数据,是"无状态"的投票者,这使其部署极为轻量。在生产环境中,强烈建议在类似于监控服务器的地方部署一个 Witness 实例,对于双节点的集群,它是防止脑裂的关键一环。

2.3 repmgr cluster show 输出解读

bash 复制代码
$ repmgr -f /etc/repmgr.conf cluster show
 ID | Name     | Role    | Status    | Upstream | Location | Priority | Replication lag
----+----------+---------+-----------+----------+----------+----------+----------------
 1  | pg-node-1 | primary | * running |          | default  | 100      | 
 2  | pg-node-2 | standby |   running | pg-node-1| default  | 80       | 0 bytes
 3  | pg-node-w | witness | * running |          | default  | 50       | 
  • ID/Name: 节点的唯一标识和别名。
  • Role : primary 代表主库,standby 代表备库,witness 代表见证服务器。
  • Status : * running 表示 repmgrd 守护进程正在运行且通信正常。
  • Upstream: 对于备库,这个字段指向上游主库的名称。
  • Priority : 这决定了在自动 Failover 选举中的顺序。数值越小,优先级越高
  • Replication lag : 备库与上游主库的 WAL 复制延迟,以字节为单位。0 bytes 表示实时同步。

2.4 手动 Switchover 步骤

repmgr 的手动切换极大简化了之前的手工流程。

bash 复制代码
# 在任意一个健康的备库上执行,通常指定要提升的备库名称
$ repmgr -f /etc/repmgr.conf standby switchover --siblings-follow

# 执行过程会进行一系列的预检:
#   1. 检查所有节点的连通性。
#   2. 确保指定备库与旧主库完全同步 (lag=0)。
#   3. 停止旧主库上的所有连接。
#   4. 提升备库为新主库。
#   5. 将旧主库降级并配置为跟随新主库。
#   6. --siblings-follow 使其他所有备库自动跟随新主库。

整个过程脚本化、标准化,避免了手工误操作的风险。


3. Patroni + etcd:分布式协调与云原生高可用

当业务上云,集群规模扩大,我们需要一个更自动化、更强一致、更易于集成的方案。Patroni 结合 etcd 正是为满足这种"云原生"需求而生的黄金组合。

3.1 Patroni 架构全景

Patroni 本身不是一个分布式系统,而是一个智能的单机 Agent。多个 Patroni 实例通过与 etcd 集群交互,构成了一个强大的分布式高可用系统。

flowchart TB subgraph PG_Node1 ["PG 节点 1 (初始主库)"] P1["Patroni Agent 1"] PG1["PostgreSQL 16
Role: Primary"] end subgraph PG_Node2 ["PG 节点 2 (备库)"] P2["Patroni Agent 2"] PG2["PostgreSQL 16
Role: Standby"] end subgraph etcd_Cluster ["etcd 集群 (强一致性KV存储)"] E1["etcd Node 1"] E2["etcd Node 2"] E3["etcd Node 3"] E1 <--> E2 <--> E3 end subgraph External_Sys ["外部系统"] HAProxy["负载均衡 (如 HAProxy)"] Apps["应用"] end P1 -- "1. 持续续约 Leader Key (TTL)" --> E1 P2 -- "2. 监控 Leader Key,等待过期" --> E1 P1 -- "3a. 读写 API,管理 PG 生命周期" --> PG1 P2 -- "3b. 跟随主库,管理 PG 生命周期" --> PG2 P1 -- "4. 提供健康检查 REST API" --> HAProxy P2 -- "4. 提供健康检查 REST API" --> HAProxy HAProxy -- "5a. 写请求" --> PG1 HAProxy -- "5b. 读请求" --> PG2 Apps --> HAProxy

图 3:Patroni + etcd 分布式协调架构图

  • 图表主旨概括:此架构图展示了 Patroni Agent、PostgreSQL 实例和 etcd 集群三者之间的静态部署与动态交互关系。
  • 逐层/逐元素分解
    • etcd 作为"真实来源":集群的当前状态(谁是主库,系统配置)不再属于任何一个 PG 节点,而是存储在 etcd 集群中。这是一个分布式的、强一致的、高可用的外部真理。
    • Patroni Agent 的角色:它是 PG 实例的"监护人",负责启动、停止、监控和角色切换。它通过 etcd 的 Leader Key 机制与其他监护人竞争,从而决定本地的 PG 应该是主库还是备库。
    • REST API 的集成作用 :Patroni 在每个节点上提供了一个轻量级的 HTTP REST API(默认 8008 端口)。HAProxyPgpool-II 等中间件可以通过查询 /primary/replica 端点来动态发现集群拓扑,实现自动路由。
  • 设计原理映射 :这是典型的 Leader Election(领导者选举) 模式。利用 etcd 的原子操作(Compare-And-Swap)和租约(Lease)机制,实现了分布式环境下的安全选主。
  • 工程联系与关键结论这种架构彻底解决了脑裂问题。因为主库的身份不与主机绑定,而是与 etcd 中的一个 Key 绑定。任何时刻,etcd 集群的多数派同意的 Leader 只有一个。

3.2 etcd Leader Key 的 TTL 竞争机制与 Raft 共识

这是 Patroni 实现自动故障转移的核心所在。

  • Leader Key :这是一个存储在 etcd 中的特殊键值,其路径通常为 /service/patroni_cluster_name/leader。它的 Value 包含了当前主库的标识信息(如名称、地址)。此 Key 被设置了 Time To Live (TTL) ,通常由 ttl 参数配置(如 30 秒)。
  • 保活(Keep-Alive):当前主库的 Patroni Agent 会启动一个后台线程,以远小于 TTL 的频率(如每 TTL/2 秒)向 etcd 发送请求,续约 Leader Key 的 TTL。只要续约成功,这个 Key 就始终存在。
  • 故障转移触发器 :当主库节点发生故障(如宕机、网络中断)时,续约中断。当 Leader Key 的 TTL 过期后,etcd 会自动删除该 Key。所有备库的 Patroni Agent 都在通过 etcd 的 Watch API 监听这个 Key。一旦 Key 被删除,它们会立刻收到事件通知。
  • 选主竞赛 :收到通知后,所有健康的备库会立即向 etcd 发起请求,尝试"创建"同一个 Leader Key。但这里的创建不是简单的 PUT,而是使用了 etcd 的事务或 CAS(Compare-And-Swap,即比较并交换)原子操作,条件是该 Key 必须不存在。
    • 只有一个节点能成功创建 Key,它获得"锁",成为新的领导者。
    • 其他节点的创建请求会因为 Key 已存在而失败,它们继续保持备库角色。
  • Raft 共识:上述的"谁能写入成功"由 etcd 内部的 Raft 算法决定。Raft 确保了在 etcd 集群多数派(N/2+1)存活的情况下,整个系统对外提供强一致的、唯一的视图。这就是 Patroni 理论上能防止脑裂的底层数学基础。

3.3 patroni.yml 核心配置解读

yaml 复制代码
# 每个 Patroni 节点的名字,集群内唯一
scope: my_pg_cluster
name: pg-node-1

restapi:
  listen: 0.0.0.0:8008
  connect_address: 192.168.1.1:8008 # 其他节点和中间件可达的地址

# etcd 集群连接信息
etcd:
  hosts: 192.168.1.10:2379,192.168.1.11:2379,192.168.1.12:2379
  # ttl of the leader key
  ttl: 30
  # etcd key prefix
  namespace: /service/

# Patroni 管理的 PostgreSQL 数据库
postgresql:
  listen: 0.0.0.0:5432
  connect_address: 192.168.1.1:5432
  data_dir: /var/lib/postgresql/16/main
  # pg_hba.conf 配置,确保复制和管理用户能连通
  pg_hba:
    - host all all 0.0.0.0/0 md5
    - host replication replicator all md5
  # 配置 Patroni 使用的复制和超级用户账号
  authentication:
    replication:
      username: replicator
      password: rep_password
    superuser:
      username: postgres
      password: supervisor_password
  parameters:
    # 至关重要的配置: 同步复制模式
    # 这确保了在故障转移时,被提升的新主库拥有最新的数据
    synchronous_mode: on
    synchronous_commit: "remote_apply"
    synchronous_standby_names: "*"
    # 开启复制槽,防止 WAL 被误清理
    max_replication_slots: 10
    max_wal_senders: 15
    wal_level: replica
  • scopename : scope 是集群的名字,name 是节点自身在 scope 下的唯一 ID。它们在 etcd 中构建出一个命名空间。
  • synchronous_mode: on : 这是 Patroni 最具特色的功能之一。 开启后,Patroni 会智能地管理 synchronous_standby_names。它确保只有与主库实时同步的备库才能被包括在同步备库列表中,且至少有一个健在。当主库故障,它能保证选出的新主库是数据最新的那个,从而实现零数据丢失的故障转移。

3.4 patronictl 命令实战

bash 复制代码
# 1. 查看集群状态
$ patronictl -c /etc/patroni.yml list
+ Cluster: my_pg_cluster (717493620073884558) ----+----+-----------+-----------------+
| Member     | Host         | Role    | State   | TL | Lag in MB | Pending restart |
+------------+--------------+---------+---------+----+-----------+-----------------+
| pg-node-1  | 192.168.1.1  | Leader  | running |  3 |       0.0 |                 |
| pg-node-2  | 192.168.1.2  | Sync    | running |  3 |       0.0 |                 |
| pg-node-3  | 192.168.1.3  | Replica | running |  3 |       0.0 |                 |
+------------+--------------+---------+---------+----+-----------+-----------------+
# 解读:
# TL (Timeline): 当前集群的统一时间线,用于标识不同版本的 WAL 历史。
# Lag in MB: 备库落后主库的WAL量。Sync 角色的备库滞后为 0。
# Role: Leader(主库), Sync(同步备库), Replica(异步备库)

# 2. 计划内切换 (Switchover)
$ patronictl -c /etc/patroni.yml switchover
# Master [pg-node-1]:  [Enter to keep]
# Candidate ['pg-node-2', 'pg-node-3']: pg-node-3
# When should the switchover take place [now]: now
# 交互式地选择要切换的目标节,Patroni 会优雅地执行切换流程。

# 3. 强制故障转移 (Failover)
$ patronictl -c /etc/patroni.yml failover
# 这个命令在旧主库明确宕机/异常时使用,会立刻在新节点上执行 promote。

# 4. 重建备库 (Reinit)
$ patronictl -c /etc/patroni.yml reinit my_pg_cluster pg-node-3
# 当一个备库因 WAL 断开太久需要重建时,此命令会清空其数据并从当前主库重新拉取。

3.5 Patroni vs repmgr 对比选型

特性 Patroni + etcd repmgr
分布式一致性 ,依赖 etcd Raft 协议,杜绝脑裂 ,依赖自身 Witness Server,逻辑相对简单
外部依赖 ,需要独立部署和维护 etcd/DCS 集群 ,无外部依赖
自动化程度 极高,全自动运行,提供REST API ,主要依赖 repmgrd 守护进程
云原生友好度 极优,K8s Sidecar 部署、REST API易集成 一般,传统部署模式为主
学习与运维成本 ,需掌握 etcd 和 Patroni 两个系统 ,专注于 PostgreSQL 运维
适用场景 大中型核心业务,云原生环境,对一致性要求极高 中小企业,独立部署,追求简单可靠

4. Pgpool-II 读写分离与连接池实战

高可用解决了"活着"的问题,而 Pgpool-II 则解决"活得好"的问题。它是一种位于应用与数据库之间的透明中间件。

4.1 中间件架构与 SQL 路由规则

Pgpool-II 作为一个反向代理,接管了所有应用的数据库连接。

flowchart LR subgraph Applications ["Applications"] App1["App 1"] App2["App 2"] end subgraph Pgpool ["Pgpool-II 中间件集群"] Pool1["Pgpool-II 1"] Pool2["Pgpool-II 2"] Pool1 <-- "Watchdog" --> Pool2 end subgraph PG_HA ["PostgreSQL 高可用集群 (Patroni管理)"] Primary[("主库 (Read/Write)")] Standby1[("备库 1 (Read-Only)")] Standby2[("备库 2 (Read-Only)")] end App1 --> Pool1 App2 --> Pool2 Pool1 -- "写请求 (INSERT/UPDATE/DELETE)" --> Primary Pool1 -- "读请求 (SELECT)" --> Standby1 Pool1 -. "负载均衡" .-> Standby2 Pool2 -- "写请求" --> Primary Pool2 -- "读请求" --> Standby2 Pool2 -. "负载均衡" .-> Standby1

图 4:Pgpool-II 读写分离与负载均衡路由图

  • 图表主旨概括:此图展示了 Pgpool-II 作为中间件如何接收所有上游流量,并根据 SQL 类型将其智能分发到下游 PostgreSQL 主备集群的不同节点上。
  • 逐层/逐元素分解
    • Watchdog:Pgpool-II 自身的高可用方案。多个 Pgpool-II 节点通过心跳组成集群,当主 Pgpool-II 故障时,备 Pgpool-II 接管 VIP(虚拟 IP),保证接入层单点无故障。
    • 读写分离 :核心逻辑是 SQL 解析器。Pgpool-II 会识别 SQL 文本,将 SELECT 请求(默认)发送到加权轮询选择的备库节点,将事务和写请求(INSERT/UPDATE/DELETE /DDL)发送到主库。对于读写事务(如 SELECT ... FOR UPDATE),会统一路由到主库。
    • 连接池:Pgpool-II 作为一个进程模型的服务,维护着与后端的 PostgreSQL 实例的预连接池。用户连接快速映射到空闲的后端连接,极大地减少了数据库花在新建连接上的开销。
  • 设计原理映射 :这是典型的代理模式池化模式的结合。Pgpool-II 切断了应用与物理数据库的直接耦合,为上层提供了一个逻辑上的单一数据库视图。
  • 工程联系与关键结论读写分离有效提升了集群的整体吞吐量。但必须注意主备延迟。一个刚从主库写入的数据,立刻在备库查询可能因同步延迟而读不到。

4.2 pgpool.conf 核心参数与 Patroni 集成

pgpool.conf 核心参数解读:

ini 复制代码
# 监听和watchdog配置
listen_addresses = '*'
port = 9999

# 进程池配置
num_init_children = 32        # 预fork的子进程数,即最大并发连接数

# 后端节点配置 (与 Patroni 集成)
# 方案: 写主库,读备库
backend_hostname0 = '192.168.1.1'  # 主库
backend_port0 = 5432
backend_weight0 = 0          # 读写分离模式中,写操作的权重。对主库此值应为0,表示不参与读负载均衡。
backend_flag0 = 'ALLOW_TO_FAILOVER'

backend_hostname1 = '192.168.1.2'  # 备库1
backend_port1 = 5432
backend_weight1 = 1          # 权重为 1,参与 SELECT 负载均衡
backend_flag1 = 'ALLOW_TO_FAILOVER'

backend_hostname2 = '192.168.1.3'  # 备库2
backend_port2 = 5432
backend_weight2 = 1
backend_flag2 = 'ALLOW_TO_FAILOVER'

# 读写分离配置
load_balance_mode = on
master_slave_mode = on

# 主备状态自动检测 (与 Patroni 集成核心)
sr_check_period = 10         # 流复制检查间隔
sr_check_user = 'repmgr'     # 用于检查的用户。也可以使用 Patroni 用户。
sr_check_password = 'password'
# 关键:使用 Patroni REST API 作为状态源
backend_application_name = 'pgpool'
health_check_user = 'postgres'

# 或者,直接集成 Patroni REST API (新版本推荐方式)
use_watchdog = on
# 指定 Patroni REST API 地址,Pgpool-II 会通过 HTTP 请求实时获取主备角色
backend_application_name = 'pgpool'
# 这种方式更加精准,绕过了需要对 PG 实例执行查询的探测方式。
  • 与 Patroni 集成的核心 :Pgpool-II 需要知道谁是主库。传统方式通过 sr_check_period 定时查询 PG 状态,但在 Patroni 管理下,主库角色切换极快。更现代和可靠的方式是通过 wd_lifecheck 模块或是通过配置自定义脚本来 查询 Patroni REST API ,直接获取 role: master 的节点,实现路由表的准实时更新。

4.3 pcp 命令管理

bash 复制代码
# 查看节点信息
$ pcp_node_info -h localhost -p 9898 -U pgpool
Password:
Hostname: 192.168.1.1
Port: 5432
Status: 2         # 0: 未定义,1: 等待中, 2: 存活, 3: 下线
Weight: 1
Role: primary

# 手动将一个节点从负载均衡中摘除/恢复
$ pcp_detach_node -h localhost -p 9898 -U pgpool -n 0  # 摘除节点0
$ pcp_attach_node -h localhost -p 9898 -U pgpool -n 0  # 重新加入节点0

5. 三种高可用方案对比与选型建议

单一的工具无法覆盖所有场景,理解其差异是做出合理架构决策的前提。

flowchart TB subgraph Patroni ["Patroni + etcd 方案"] PA["全自动故障转移"] PB["极强一致性(Raft)"] PC["云原生/声明式配置"] PD["需独立维护 etcd"] PE["REST API 标准化集成"] end subgraph Repmgr ["repmgr方案"] RA["自动故障转移"] RB["保障一致性(Witness)"] RC["传统独立部署"] RD["无外部依赖"] RE["命令行/事件通知"] end subgraph Pgpool ["Pgpool方案"] P_A["无故障转移,依赖其他方案"] P_B["不解决一致性问题"] P_C["独立部署/云原生均可"] P_D["需处理自身 HA"] P_E["读写分离/连接池/负载均衡"] end Patroni -- "黄金搭档" --> Pgpool Repmgr -- "轻量组合" --> Pgpool classDef pat fill:#e3f2fd,stroke:#1e88e5; classDef rep fill:#e8f5e9,stroke:#43a047; classDef pgp fill:#fff3e0,stroke:#fb8c00; class Patroni pat; class Repmgr rep; class Pgpool pgp;

图 5:三种高可用方案对比矩阵图

  • 图表主旨概括:此图旨在通过多维度的对比,帮助读者快速定位 Patroni、repmgr 和 Pgpool-II 的各自定位和组合关系。
  • 逐层/逐元素分解
    • 功能定位Patronirepmgr故障转移(Failover) 工具,答案是"谁是新主库?"。Pgpool-II连接和负载管理工具,答案是"流量该发给谁?"。
    • 一致性保障Patroni 借助 etcd 获得强一致性repmgr 通过 Witness 提供最终一致性 保障;Pgpool-II 不参与此过程。
    • 依赖性repmgr 零外部依赖 是其最大亮点。Patroni 虽然功能强大,但引入了 etcd 集群的运维开销。
  • 设计原理映射Patroni 架构遵循了"状态与逻辑分离"的设计哲学,etcd 负责分布式状态存储,Patroni 负责本地操作逻辑。Pgpool-II 则是经典的中间件模式,在应用和数据层之间引入一个解耦层。
  • 工程联系与关键结论三者并非完全互斥。最佳实践往往是 Patroni + Pgpool-II 的组合。Patroni 负责集群的高可用和自愈,Pgpool-II 负责提供稳定的读写分离接入点。repmgr 则是希望避免维护 etcd 的团队的绝佳选择。

生产环境选型建议

  • 小型集群(2-3个PG节点,对可用性要求一般)repmgr + Pgpool-II。简单可靠,运维成本低。Witness Server 可部署在 Pgpool-II 节点上。
  • 中型核心业务(追求自动化与数据零丢失)Patroni + etcd。再上层使用 HAProxy 或直接使用K8s Service 做流量的 noport 转发。
  • 大型云原生/混合部署Patroni + etcd + Pgpool-II。Patroni 负责PG生命周期,Pgpool-II 负责读写分离、连接池和SQL级负载均衡。这是功能最完整的组合。

6. 故障模拟与演练

理论终究需要实践的检验。以下故障演练将在 Patroni + etcd + Pgpool-II 的组合环境中进行。

6.1 故障一:主库宕机 → Patroni 自动 Failover

操作步骤:

  1. 确认当前状态patronictl list,确认 pg-node-1 为 Leader。
  2. 模拟主库宕机 :在 pg-node-1 上执行 sudo systemctl stop patronikill -9 Patroni 和 PostgreSQL 进程。
  3. 观察 etcd 行为 :在一个 etcd 节点上执行 etcdctl watch /service/my_pg_cluster/leader
  4. 观察备库行为 :查看 pg-node-2 的 Patroni 日志 tail -f /var/log/patroni.log
  5. 检查新主库 :执行 patronictl list 查看集群最终状态。

预期现象与输出解读:

bash 复制代码
$ patronictl list
+ Cluster: my_pg_cluster (717493620073884558) ----+----+-----------+
| Member     | Host         | Role    | State   | TL | Lag in MB |
+------------+--------------+---------+---------+----+-----------+
| pg-node-1  | 192.168.1.1  |         | stopped |    |   unknown |
| pg-node-2  | 192.168.1.2  | Leader  | running |  4 |       0.0 | # <-- pg-node-2 晋升为新主库
| pg-node-3  | 192.168.1.3  | Sync    | running |  4 |       0.0 | # <-- pg-node-3 自动成为同步备库
+------------+--------------+---------+---------+----+-----------+
  • 现象分析

    • pg-node-1 状态为 stopped
    • pg-node-2Role 变为 Leader,且 Timeline 从 3 变为 4,表示发生了一次 promote 操作。
    • pg-node-3 的角色变为 Sync,并紧跟随新的 Timeline 4。
  • 日志解读 (pg-node-2patroni.log) :

    vbnet 复制代码
    INFO: Got response from etcd /service/my_pg_cluster/leader: {'host': '192.168.1.1', ...}
    ...
    INFO: failed to reach the leader
    ...
    INFO: Got release of the leader lock from etcd, the session expired
    INFO: Trying to acquire the leader lock...
    INFO: Successfully acquired the leader lock
    INFO: Starting promote of 'pg-node-2'
    ...

6.2 故障二:脑裂场景与 etcd 仲裁

操作步骤(模拟网络分区):

  1. 环境:一个3节点的 Patroni 集群,etcd 也在3个节点上。 pg-node-1 是 Leader。

  2. 制造分区 :在 pg-node-1 上使用 iptables 阻断它与 pg-node-2etcd 集群多数派的连接。

    bash 复制代码
    sudo iptables -A INPUT -s 192.168.1.2 -j DROP
    sudo iptables -A OUTPUT -d 192.168.1.2 -j DROP
    # 假设 etcd 集群是 1,2,3,必须同时阻断与大多数(如2和3)的通信

预期现象与输出解读:

  1. pg-node-1 视角:它与 etcd 集群多数派失去联系,无法完成 Leader Key 续约。
  2. pg-node-1 日志 :出现大量与 etcd 连接超时的错误。在 TTL 时间后,它会自动将自己降级为只读的备库(read-only) ,以防止脑裂。 patroni.log 会显示 "Demoting self because of DCS is not accessible and I am a leader"
  3. etcd 集群视角pg-node-1 的 Leader Key 过期被删除。
  4. pg-node-2pg-node-3 视角 :它们构成 etcd 的多数派,Watch 到 Key 删除后,发起新一轮选举,其中一个(如 pg-node-2)成功当选为新 Leader。
  5. 最终结果patronictl list 会显示 pg-node-1 处于非健康状态或降级备库,而 pg-node-2 是新 Leader。完全杜绝了双主现象。

6.3 故障三:Pgpool-II 感知主备切换

操作步骤:

  1. Pgpool-II 已配置通过 wd_lifecheck 或自定义脚本查询 Patroni REST API。
  2. 在 Patroni 集群上执行一次 Switchover:patronictl switchover
  3. 观察 Pgpool-II 日志和通过 SHOW POOL_NODES 命令的输出。

预期现象与输出解读:

sql 复制代码
-- 在 Pgpool-II 上执行查询
pgpool=# SHOW POOL_NODES;
 node_id |  hostname   | port | status | lb_weight |  role   | ... 
---------+-------------+------+--------+-----------+---------+-----
 0       | 192.168.1.1 | 5432 | up     | 0.000000  | standby | ...  <-- role 从 primary 变为 standby
 1       | 192.168.1.2 | 5432 | up     | 1.000000  | primary | ...  <-- role 从 standby 变为 primary
 2       | 192.168.1.3 | 5432 | up     | 1.000000  | standby | ... 
(3 rows)
  • 现象分析 :Pgpool-II 的 SHOW POOL_NODES 输出准确反映了新的角色拓扑。这个过程对应用是完全透明的,应用可以继续通过 Pgpool-II 的同一端口进行无感知的读写操作。
  • Pgpool-II 日志 :会显示类似 "Detected switchover. Changing backend status for node 0 to standby.""... node 1 to primary." 的信息。
sequenceDiagram participant OP as 运维人员 participant PG1 as pg-node-1 (原主库) participant Patroni1 as Patroni on pg-node-1 participant etcd as etcd 集群 participant Patroni2 as Patroni on pg-node-2 participant PG2 as pg-node-2 (新主库) participant Pgpool as Pgpool-II participant HAProxy as 负载均衡器 OP->>Patroni1: patronictl switchover Patroni1->>etcd: 1. 写入 Switchover LSN 目标 Patroni1->>PG2: 2. 等待 pg-node-2 追赶到指定LSN PG2-->>Patroni1: 3. 追赶完成 Patroni1->>etcd: 4. 将 Leader Key 更新为 pg-node-2 Note over Patroni1, PG1: 旧主库: Patroni 将其降级并重启为备库 Patroni2->>etcd: 5. Watch 到 Leader Key 更新 Patroni2->>PG2: 6. 执行 pg_ctl promote,提升为新主库 Par 拓扑感知与更新 Pgpool->>Patroni2: 7. wd_lifecheck 查询 REST API /primary Patroni2-->>Pgpool: 8. 返回新主库位置 Pgpool->>Pgpool: 9. 内部路由表: pg-node-1=standby, pg-node-2=primary HAProxy->>Patroni2: 7a. 健康检查请求 Patroni2-->>HAProxy: 8a. 返回 200 OK for Primary HAProxy->>HAProxy: 9a. 更新后端主库池 end OP->>Pgpool: SHOW POOL_NODES (验证)

图 6:故障模拟全链路观测序列图

  • 图表主旨概括:此序列图完整展示了一次计划内主备切换从发起到各组件完成自愈的全过程,体现了系统的自动化程度。
  • 逐层/逐元素分解
    • Patroni 的精准控制:步骤1-3显示,Patroni 在切换前会确保目标节点数据完全同步,这是零数据丢失的关键。
    • etcd 的中心协调作用:步骤4-6展示了所有状态变更都通过 etcd 作为原子操作,这是多节点间协作的基础。
    • 中间件自愈能力:步骤7-9展示了以 Pgpool-II 和 HAProxy 为代表的中间件,是如何通过主动探测(REST API/健康检查)来动态适应数据库拓扑变化的。
  • 设计原理映射 :这是观察者模式在分布式系统中的宏大应用。Patroni 是事件产生者,etcd 是事件总线,Pgpool-II 和 HAProxy 是事件消费者。
  • 工程联系与关键结论此演练证明,一个成熟的 Patroni + Pgpool-II 方案能够在秒级完成自动故障恢复和对上层应用的透明切换,其核心在于各个组件围绕 etcd 这个"真理之源"的松耦合协作。

7. 面试高频专题

本节从面试官视角出发,精选13道(含一道系统设计题)高频题目,并提供系统化答案。

1. PostgreSQL 的物理流复制是如何工作的?同步复制与异步复制有什么区别?

  • 一句话回答 :物理流复制是主库通过 WAL Sender 进程将 WAL 日志流式发送给备库的 WAL Receiver 进程,备库再应用这些日志来实现数据同步。异步复制不等待备库确认,性能高但可能丢数据;同步复制则需等待备库确认,数据不丢失但影响性能。
  • 详细解释
    • 内部实现 :启动时,备库通过 primary_conninfo 连接到主库请求复制。主库 fork 出一个 WAL Sender 进程,从请求的 LSN 处开始持续发送 WAL 记录。备库 WAL Receiver 接收后先写盘,再通知 Startup Process 应用。
    • 同步与异步差异 :差异核心在于 synchronous_commit 参数。异步(local/off)在 WAL 写入主库本地后就返回客户端。同步(on/remote_apply)则须等待备库的 ACK。remote_apply 保证主备强一致读,on 仅保证备库持久化。可以通过 synchronous_standby_names 灵活配置 FIRST/ANY 策略来决定需要等待哪些备库的确认以及数量。
    • 运维实践 :对于金融等关键业务,必须使用 remote_apply 同步复制。但必须确保同步备库数量(synchronous_standby_names),如一份数据至少三副本(一主一同步备一异步备),防止同步备库故障导致主库写操作全部挂起。
  • 多角度追问
    1. 架构追问:如果网络抖动导致同步备库短暂失联再迅速恢复,会发生什么?主库上的事务会被阻塞直到备库重连或超时/被手动从同步列表中移除。
    2. 性能追问 :主备之间的网络延迟对 remote_apply 的性能影响有多大?影响非常大,因为每个事务提交都多了一次网络往返(RTT),对于高频小事务型场景可能是灾难性的。
    3. 安全追问 :流复制的网络连接是加密的吗?不默认加密,应通过 pg_hba.conf 强制使用 scram-sha-256 认证,并强烈建议在网络上使用 SSH TunnelSSL/TLS 连接模式(sslmode=verify-full)。
  • 加分回答 :在 PostgreSQL 16 中,支持逻辑复制与物理复制混合使用,两者使用不同的 walsender 进程,相互独立。物理复制槽 pg_replication_slots 监控对于生产至关重要,需监控其 active 状态和 WAL 延迟,防止磁盘被撑爆。一个优秀的 DBA 会设置 max_slot_wal_keep_size 作为最后的安全阀。

2. Patroni 是如何利用 etcd 实现自动故障转移的?Leader Key 的机制是怎样的?

  • 一句话回答:Patroni 利用 etcd 支持 TTL 的 Key 和 Watch 机制实现领导者选举。主库 Patroni 持有带 TTL 的 Leader Key 并不断续约,一旦主库故障导致续约失败,Key 过期删除,其他备库通过 Watch 机制感知后竞争创建新的 Leader Key,成功者晋升为新主库。
  • 详细解释
    • 内部实现
      1. 注册与续约:集群启动时,第一个成功向 etcd 写入 Leader Key 的节点成为主库。此后,主库的 Patroni 会周期性(如每 TTL/2 秒)向 etcd 发送续约请求,刷新 Key 的 TTL。
      2. 故障检测与选主 :所有备库都在 Watch 这个 Key。一旦主库宕机,续约停止,Key 在 TTL 过期后被 etcd 删除。所有 Watch 的备库几乎同时收到删除事件,并立即尝试通过一个原子性的 CAS(Compare-And-Swap,即比较并交换)操作("如果 Key 不存在,则创建")去创建 Leader Key。仅有一个节点能成功,它即成为新 Leader。
  • 多角度追问
    1. 架构追问:如果 etcd 集群自身发生分区,只剩下少数派(如3节点中的1个),Patroni 集群还能工作吗?不能。etcd 的 Raft 协议要求多数派存活才能提供服务。因此,故障转移也会失败,所有 Patroni 节点都将进入只读状态以防止脑裂。
    2. 安全追问 :如何防止一个被错误恢复的旧主库("僵尸"主库)再次写入,造成脑裂?当它恢复后,其 Patroni 会发现自己不是 Leader,因为 etcd 中的 Leader Key 已不属于它。它会立即执行降级(demote)操作,将自己变成一个跟随新主库的备库,完美解决了脑裂。
    3. 运维追问 :TTL 时间(etcd.ttl)应该如何设置?过大(如几分钟)会导致故障转移时间(RTO)过长。过小(如几秒)可能因网络抖动导致不必要的误切换。通常设置为 30 秒,是一个在灵敏度和稳定性之间的合理折衷。
  • 加分回答 :Patroni 的 synchronous_mode 通过动态管理 synchronous_standby_names 参数,确保只有在数据上最同步的备库才有资格在下一轮竞选中成为 Leader,这是实现 RPO(Recovery Point Objective,即恢复点目标)为零的关键机制。DCS(Distributed Configuration Store,即分布式配置存储)不仅是 etcd,还可以是 ZooKeeper、Consul 等,体现了良好的设计解耦。

3. repmgr 和 Patroni 有什么区别?各自适用什么场景?

  • 一句话回答:repmgr 是一个无需外部依赖的轻量级 PG 高可用工具,通过 Witness Server 解决脑裂;Patroni 则是一个云原生风格的方案,依赖 etcd/DCS 实现更强的分布式一致性,并提供了丰富的 REST API 用于集成。
  • 详细解释
    • 依赖与复杂度:这是两者最根本的区别。repmgr 与 PG 紧密集成,通过元数据表和守护进程工作,零额外组件。Patroni 引入了 DCS (如 etcd) 作为外部真理之源,带来了极高的可靠性,但也引入了部署和运维 etcd 集群的额外成本。
    • 一致性与防脑裂:repmgr 通过 Witness Server 投票来防止双节点集群的脑裂。Patroni 则依赖 etcd 集群自身的 Raft 共识协议,提供了理论上更强的、多数派性质的脑裂防护。
    • 功能与集成 :Patroni 提供了 REST API,使得 HAProxy、Pgpool-II 等中间件能动态发现主库。其 synchronous_mode 配置也能更智能地实现零数据丢失切换。repmgr 功能相对单一,专注于节点管理和切换。
  • 多角度追问
    1. 选型追问 :如果公司只有2台物理服务器跑 PG,该选谁?这种情况下部署完整 etcd 集群不现实。repmgr + Witness 是绝佳选择,Witness 可以轻松地放在一台管理服务器或虚拟机上。
    2. 架构追问:在 Kubernetes 上部署 PG 高可用,两者如何处理?K8s 本身就是一套类似 DCS 的系统。Patroni 可以很好地利用 K8s API 代替 etcd 来存储状态和进行选主,是 K8s 环境的事实标准。repmgr 的部署模式在K8s上则显得水土不服。
    3. 体验追问:从一个初级 DBA 的角度,学习哪个更容易?毫无疑问是 repmgr。它的概念少,配置直白,排错时只需关注 PG 和其自身日志。Patroni 则要求 DBA 同时掌握 PostgreSQL 和 etcd/DCS 两个领域的知识。
  • 加分回答:在代码设计和哲学上,repmgr 采用"谁有数据谁做主",选举基于 LSN。Patroni 则是"分布式共识做主",选举基于对 Leader Key 的抢占。这是两个完全不同的世界观。

4. Pgpool-II 是如何实现读写分离的?它如何感知主备切换?

  • 一句话回答 :Pgpool-II 作为中间件,通过解析客户端发来的 SQL 语句类型,将读请求(SELECT)分发给备库,将写请求分发给主库。它通过查询 Patroni REST API 或定时执行 SQL 探测的方式,动态感知后端数据库集群的角色变化。
  • 详细解释
    • 读写分离实现 :Pgpool-II 的 SimpleQuery / ExtendedQuery 解析器会分析 SQL,识别出 SELECTINSERT/UPDATE/DELETE 等。通过 load_balance_mode=on 配合 black_function_list/white_function_list 来控制函数的路由。对于显式事务(BEGIN...COMMIT),如果在事务中出现写操作,Pgpool-II 会将整个事务内的所有后续语句(包括读)都发往主库,以保证数据一致性。
    • 感知切换 :集成方案有多种:
      1. 脚本方式 :最灵活。配置 failover_commandfollow_primary_command,脚本内调用 curl 查询 Patroni REST API 获取新拓扑,然后用 pcp_* 命令更新 Pgpool-II 内部状态表。
      2. wd_lifecheck 方式:直接配置 Pgpool-II 去 ping Patroni 节点的 API。这是与 Patroni 集成的最佳实践。
      3. SQL 探测方式 :传统方式,通过定时执行 SELECT pg_is_in_recovery() 来判断节点角色,延迟相对较高。
  • 多角度追问
    1. 一致性问题 :读写分离带来的"复制延迟"问题,Pgpool-II 如何解决?没有完美方案。可以通过 delay_threshold 参数让 Pgpool-II 自动摘除延迟过大的备库,但这是一种粗粒度的逃避。根本解决还是在应用层面容忍一定延迟,或对实时性要求高的查询指定发往主库。
    2. 连接池追问 :Pgpool-II 的连接池和数据库直连的性能差异?连接池在短连接、高并发的场景下优势巨大,可以显著减少 PG 的 fork 开销和连接数。但在长连接、连接数少的场景下,其自身作为一个代理可能会增加几毫秒的网络延迟。
    3. 高可用追问:如果 Pgpool-II 自己挂了怎么办?使用 Pgpool-II 自身的 Watchdog 功能部署多个 Pgpool-II 节点,并通过 VIP(虚拟IP)实现自身的高可用。
  • 加分回答 :Pgpool-II 4.x 的查询缓存(query_cache)功能也能显著提升读性能。当 SELECT 结果被缓存后,可以完全绕过数据库,直接从 Pgpool-II 内存返回,但必须严格配置缓存失效策略,避免返回脏数据。

5. 物理复制槽(Replication Slot)的作用是什么?为什么生产环境建议开启?

  • 一句话回答 :物理复制槽在主库上为每个备库保留一个 WAL 消费的"水位线",防止主库在备库离线时过早地清理掉备库尚未接收的 WAL 文件,避免备库因 requested WAL segment has already been removed 而需要进行全量重建。
  • 详细解释
    • 内部实现 :它是一个持久化的指针 restart_lsn。创建后,主库的 Checkpointer 和 WAL Writer 进程在回收旧的 WAL 段文件时,会检查所有活动槽的 restart_lsn,确保被清理的位置都小于这个指针。
    • 风险与最佳实践 :不加监控地开启复制槽是高危操作。一旦备库长时间故障,WAL 在主库无限堆积,最终导致主库磁盘写满并宕机,引发雪崩。必须做到:1)监控 pg_replication_slots 的 WAL lag;2) 设置 max_slot_wal_keep_size,允许主库在空间不足时自动"抛弃"落后的备库以保护自身。
  • 多角度追问
    1. 架构追问 :物理复制槽和逻辑复制槽有什么区别?物理复制槽同步的是二进制 WAL 字节流,主备数据块完全一致。逻辑复制槽则对 WAL 进行解码(Decode),输出的是 INSERTUPDATE 等逻辑变更,用于跨版本、跨平台或部分表同步。
    2. 运维追问 :如何清理一个不再使用的僵尸复制槽?使用 SELECT pg_drop_replication_slot('slot_name');。在删除前,务必确认没有下游在使用它,否则会导致备库同步中断。
    3. 故障追问:如果备库因故障丢失了复制槽,会怎样?备库启动时会报告找不到槽,无法同步。处理办法是:在主库为它重建一个新槽,然后在备库执行全量重建流程。
  • 加分回答 :在 PG 16 中,pg_replication_slot_advance 函数可以在不实际发送数据的情况下,手动推进槽的位置。这在高级的运维场景下(比如跳过一段有问题的 WAL)可能有用,但极度危险,操作不当会导致备库数据不一致。

6. 如何防止 PostgreSQL 高可用集群出现脑裂(Split-Brain)?

  • 一句话回答:防止脑裂的核心在于,确保在任何时刻集群的多数派(Quorum)节点能够就"谁是主库"达成共识,并通过强制措施(如电源门控/服务自杀)隔离少数派。
  • 详细解释
    • 方案一:Patroni + etcd (依赖分布式共识) :etcd 通过 Raft 协议保证了其内部状态的强一致性。只有获得 etcd Leader Key 的节点才能作为主库。当旧主库处于网络分区时,它无法联系到 etcd 多数派,导致其 Leader Key 过期。Patroni 会检测到这一点并立刻执行自我降级(demote),停止接受写操作,从而杜绝脑裂。
    • 方案二:repmgr + Witness Server (依赖投票):在只有两个数据节点的集群中,当网络分区时,双方都认为对方宕机。Witness Server 作为第三个投票者,确保只有与 Witness 同处一个分区的备库才能成功晋升,旧主库会因连不上 Witness 而无法完成投票流程。
  • 多角度追问
    1. 极端场景:如果 Patroni 的自我降级代码有 Bug 并没能执行成功,该怎么办?这属于极端灾难。某些极高级别安全系统会引入 STONITH(Shoot The Other Node In The Head,即爆头)机制。由集群仲裁程序通过网络控制的 PDU(电源分配单元)直接切断旧主库服务器的电源,在物理上消灭它。
    2. repmgr 细节:repmgr 的 Witness 故障了,集群还有脑裂保护吗?没有。在两个数据节点的集群中,Witness 单点故障将导致集群无法进行自动故障转移,因为任何一侧的备库都无法获得绝对多数(2/3)的投票。
    3. 数据层面 :除了服务脑裂,会不会出现数据脑裂(一个集群出现两条不同历史的分叉)?这正是 Timeline 机制所防止的。一旦发生过 promote,时间线就增加。旧主库 WAL 和备库 WAL 不再兼容,即使旧主库被强行拉起,也无法跟上新主库的流复制,需要全量重建。
  • 加分回答 :脑裂的本质是CAP定理(一致性C、可用性A、分区容错性P)中,在网络分区(P)发生时,必须为了一致性(C)而牺牲可用性(A)。Patroni 的自我降级机制,就是在 P 发生时,选择 C 而放弃 A 的具体工程实现。

7. Pgpool-II 与 Patroni 是如何集成工作的?

  • 一句话回答:Patroni 负责管理 PostgreSQL 集群的高可用和主备切换;Pgpool-II 位于应用层和数据库层之间,通过 Patroni 提供的 REST API 动态发现集群拓扑,实现透明的读写分离和故障转移后的路由更新。
  • 详细解释
    • 劳动分工 :Patroni 确保"数据库永远有一个正确的主库",这是集群的控制平面 。Pgpool-II 负责"将业务流量高效且不间断地路由到正确的节点",这是集群的数据平面
    • 集成管道 :Pgpool-II 通过一个薄薄的"胶水层"与 Patroni 通信。最优雅的方法是配置 Pgpool-II 的 failover_command 为一个脚本,该脚本使用 curl 请求 Patroni 节点的 /patroni API。
      • 正常时,脚本可以通过查询 /cluster 端点列出所有节点及其角色。
      • 故障转移时,Pgpool-II 检测到后端某个节点挂了,调用 failover_command 脚本。脚本通过 API 触发 Patroni 的自动恢复或感知到新主库,然后用 pcp_attach_node/pcp_promote_node 等命令更新 Pgpool-II 的路由表。
  • 多角度追问
    1. 解耦追问 :如果不想让 Pgpool-II 和 Patroni 有这种直接集成,有其他方案吗?完全可以。在它们中间加一层 HAProxy。HAProxy 通过 Patroni REST API /primary/replica 来做健康检查。应用连 HAProxy 的读写端口做写操作,连其只读端口做读操作。Pgpool-II 只作为连接池连在中间,这个架构的解耦程度更高。
    2. 顺序追问 :Pgpool-II 的 failover_command 如果比 Patroni 更早检测到故障并执行,会不会乱套?这是一个必须规避的竞态条件(race condition)。最佳实践是让 failover_command 脚本保持幂等,并且主要是探测和等待,而不是主动去触发 pg_ctl promote。Promote 动作必须由 Patroni 集中决策。
    3. 性能追问:Pgpool-II 定期查询 API 会带来多大的开销?极低。通常只是一个简单的 HTTP GET 请求查询健康状态,响应体很小。即使每秒查询一次,对 Patroni 进程和网络的影响也微乎其微。
  • 加分回答 :一个典型的 failover.sh 脚本的核心逻辑是一个循环:while true; do sleep 2; curl -s PatroniAPI | ... ; if [new_primary_found]; then pcp_command...; break; fi; done,优雅地处理了等待和幂等性。

8. 计划内的主备切换(Switchover)应该如何操作?有哪些注意事项?

  • 一句话回答 :计划内切换推荐使用 patronictl switchoverrepmgr standby switchover,工具会自动执行预检、同步待命、切换角色、更新拓扑等一系列步骤,比手工操作安全得多。
  • 详细解释
    • 手工步骤 :停止旧主库 -> 确保备库完全同步(flush_lag = 0) -> 在目标备库上执行 pg_ctl promote -> 将旧主库改为备库并启动跟随。
    • 注意事项
      1. 应用停写:在切换窗口,务必先暂停业务写入或确认没有长事务,否则等待同步可能需要非常久。
      2. VIP/中间件切换 :手工切换最容易忘记的是 DNS/VIP 或负载均衡配置的更改。Patroni + Pgpool-II/HAProxy 的组合则完美规避了这个问题。
      3. 回滚 :切换前想好回滚方案。在 Patroni 中,回滚就是再执行一次到原节点的 switchover
  • 多角度追问
    1. 内核追问pg_ctl promote 在数据库内部到底做了什么?它创建 promote.signal 文件,然后向 Startup Process 发送 SIGUSR1 信号。Startup Process 收到信号后,完成当前 WAL 应用,然后将 standby.signal 标记为失效,关闭恢复模式,开始允许写操作,并将时间线 +1。
    2. 故障追问 :切换过程中,如果旧主库关闭后,新主库提升失败(如硬件故障),怎么办?这是最糟糕的情况。此时必须立刻放弃旧主库,尝试在原备库中另选一台进行提升。Patroni 可以在 switchover 命令失败后,用 failover 命令来执行强制切换。
    3. 版本追问 :切换可以用来进行大版本升级吗(比如 PG 15 -> 16)?不可以。流复制备库提升后依然是同版本。大版本升级必须使用 pg_upgrade 工具或逻辑复制。
  • 加分回答 :Patroni 的 synchronous_mode=on 确保了在 switchover 时,被提升的候选者一定是数据最新的同步备库,RPO 为零。而且整个过程 Patroni 都会通过分布式锁保护,防止多个管理员同时进行错误操作。

9. 如何验证一个高可用集群的故障转移时间(RTO)和数据丢失量(RPO)?

  • 一句话回答:RTO 可以通过编写脚本持续写入并计时,同时模拟主库宕机,测量从写入失败到在新主库上写入成功的时长来验证;RPO 可以通过比较切换前后主备库自增列或特定写入埋点的 Max ID 来估算数据丢失量。
  • 详细解释
    • RTO 测量 :使用一个简单的脚本循环 INSERT 数据,记录每条的时间戳。在主库上 kill -9 PG 进程,观察脚本报错的时间。脚本需实现重连和重试机制。当在新主库上写入成功后,记录时间。时间差即为近似 RTO。
    • RPO 测量 :在主库上用一个有自增主键的表,以极高频率写入。宕机后,在主库(如果不丢盘)和备库分别执行 SELECT max(id)
      • 异步复制下,通常备库的值会略小于原主库,差值即为可能丢失的事务数。
      • Patroni 同步复制下,被提升的新主库 max(id) 应等于原主库,实现了 RPO = 0。
  • 多角度追问
    1. 工具追问 :有开箱即用的 RPO/RTO 测试工具吗?可以使用 Percona 的 sysbench 或专为 Postgres 设计的 pgbench 做持续性压力测试,并配合故障模拟脚本来观察其自带的统计信息。
    2. 精确性追问max(id) 方法测量 RPO 为什么是近似的?因为自增序列的缓存(cache)值可能丢失,导致计算出的差距比实际差距要大,造成误判。更精确的方法是使用 pg_wal_lsn_diff() 对比断连前的 LSN 和新主库的最后应用 LSN。
    3. 基线追问 :业界对 PG 高可用 RTO 和 RPO 的常见目标是什么?一个成熟的 Patroni 集群,RTO 通常在 30-60 秒 (取决于 etcd.ttl 和检测时间),同步模式下 RPO 为 。异步模式下 RPO 为毫秒级到秒级的数据量。
  • 加分回答:真正严谨的 RTO/RPO 测试需要考虑各种场景,包括网络分区、节点掉电、磁盘慢 IO 等。Google 的《Site Reliability Engineering》一书强调了这类混沌工程的必要性。对于核心金融服务,通常要求 RPO=0, RTO<1min。

10. 流复制环境下,如何在线添加一个新的备库而不影响主库?

  • 一句话回答 :使用 pg_basebackup 从主库或一个现有的、健康的备库上拉取完整基础备份,并使用复制槽来确保备份期间所需的 WAL 不被主库清理。
  • 详细解释
    • pg_basebackup 参数
      • -D <dir>:新备库数据目录。
      • -R:自动生成 standby.signalprimary_conninfo
      • -S <slot_name>:在主库上创建一个临时的物理复制槽,pg_basebackup 结束时不会自动删除(PG 16行为),必须手动清理。
      • -Fp:plain格式,直接拷贝文件,更适合在添加备库时使用。
      • -Xs:在备份期间使用流复制方式获取 WAL,而不是等待 WAL 归档文件。
    • 为何不影响主库pg_basebackup 本质上是一个长连接的长查询/复制操作。它会对磁盘和网络有IO负载,但不会阻塞其他事务进行。关键在备份期间,主库会因为复制槽的存在而保留 WAL,因此必须监控 pg_replication_slots 并监控主库磁盘,确保不会因为备份时间过长导致磁盘写满。
  • 多角度追问
    1. 架构追问 :为什么推荐从备库而不是主库拉取 pg_basebackup?这能显著降低主库的IO和网络开销,尤其是在主库负载极高的时候。这对于超大型数据库的扩容至关重要。
    2. 运维追问pg_basabcup -R 自动生成的备库配置,还需要手动修改什么?通常需要核对 primary_conninfo 的连接信息,并根据需要添加 primary_slot_name,并确保 pg_hba.conf 允许该新备库的连接。
    3. 网络追问 :如果跨机房拉取,网络很慢怎么办?可以在源端(主/备库)使用 pg_basebackup 先备份到压缩文件,传输到目标端再解压到数据目录。这种方式不适用于在线添加(因为不是实时的),更适合初始化离线的"冷备"。
  • 加分回答 :在云原生环境中,比如 K8s 的 Zalando Postgres Operator,添加一个备库(Replica)只需要修改 StatefulSet 的 replicas 数值。Operator 会自动通过 pg_basebackup 初始化新 Pod,并注册到 Patroni 和 etcd 中,实现了声明式的运维。

11. Patroni 的 synchronous_mode 配置有什么作用?

  • 一句话回答 :Patroni 的 synchronous_mode: on 启用了同步复制模式。更重要的是,它会动态、智能地管理 synchronous_standby_names 参数,确保在发生故障转移时,能够选举出一个数据实时同步的备库作为新主库,从而实现 RPO 为零。
  • 详细解释
    • 静态配置的缺陷 :如果手动配置 synchronous_standby_names = 'ANY 1 (sby1, sby2)',当 sby1 与主库同步但宕机时,sby2 (复制有延迟)将被用于确认同步提交。如果此时主库宕机,sby1sby2 都无法提供全量数据,RPO > 0。
    • Patroni 的动态智能管理 :开启 synchronous_mode 后,Patroni 会做两件关键的事:
      1. 严格筛选 :它只将那些 flush_lsn 完全等于主库 pg_current_wal_lsn() 的备库,动态地添加到 synchronous_standby_names 列表中。如果没有任何备库是完全同步的,它会保持此列表为空,此时主库上的同步事务提交将会被阻塞,直到有一个备库追上。
      2. 安全选举:在执行 Failover 候选检查时,它会额外要求候选者必须是名单中的"同步备库",保证当选者数据绝对最新。
  • 多角度追问
    1. 可用性追问:如果所有备库都突然出现延迟,主库是不是就宕了?是的,如果业务全是同步事务,那么所有写操作都会被阻塞。这虽然在数据安全上做到了极致,但牺牲了可用性。因此,生产环境通常采用"一同步、多异步"的部署架构来平衡。
    2. 架构追问synchronous_mode_strict 又是什么?这是更严格(也更危险)的模式。当它开启时,如果 Patroni 探测不到任何一个健康的同步备库,它会直接关闭主库,以避免主库在没有同步守护的情况下裸奔。除非有像金融核心账务库这样极端的安全要求,否则生产环境不建议开启。
    3. 配置追问synchronous_mode 可以和异步备库共存吗?当然可以。它只管理 synchronous_standby_names 这个列表,列表外的备库依然是异步备库,其行为模式不变。你可以有一主库、一同步备库、一异步备库的组合架构。
  • 加分回答:这个特性是 Patroni 超越 repmgr 和其他脚本式方案的核心竞争力之一。它把 DBA 手工检查和保障 RPO=0 的经验,自动化、标准化到了软件里,极大地降低了人工出错的概率。

12. (系统设计题)设计一个支持多数据中心部署的 PostgreSQL 高可用架构

要求

  • 每个数据中心有一个主库和至少一个备库。

  • 主数据中心故障时,备数据中心能自动切换并接管业务。

  • 读写分离,主库承担写入,备库承担读查询。

  • 请结合 Patroni、etcd 和 Pgpool-II 给出核心的架构设计、组件部署方案和故障切换流程。

  • 一句话回答:采用"两地三中心"或"三地五中心"的架构,通过在在三个数据中心部署 etcd 集群来提供跨地域的分布式共识,每个数据中心由独立的 Patroni 管理自己的 PG 实例,主数据中心故障后,etcd 多数派选举出备用数据中心的新主库,并由该中心的 Pgpool-II 接管读写流量,核心在于 etcd 节点和 Patroni 节点的部署策略。

  • 详细解释

    • 架构设计
      • 数据中心 Layer:主中心(DC1)部署一主库一同步备库,备中心(DC2)部署一异步备库。备库数量可根据读负载扩展。
      • 共识层 (etcd) Layer :这是多数据中心设计的难点。etcd 对网络延迟(RTT)极其敏感。因此不能简单地将 etcd 节点跨在延迟 > 10ms 的数据中心上。推荐做法是:
        • 方案 A (优选):在第三个低延迟的中立站点(如云上的一个 region 的不同 AZ,或一个专用的 DMZ 区域)部署第三个 etcd 节点。这样 DC1、DC2 和 Site3 构成 etcd 三节点集群。
        • 方案 B:如果只有两个高延迟 DC(> 10ms),不推荐跨 DC 部署 etcd。此时放弃跨 DC 的自动 Failover,选择在同城双活或灾备演练时进行半自动/手动切换。
      • 接入层 (Pgpool-II) Layer:在 DC1 和 DC2 各部署一套 Pgpool-II 集群。它们都配置为探活 etcd 或本地的 Patroni REST API。
    • 故障切换流程 (DC1 整体故障)
      1. etcd 层面:DC1 中的 etcd 节点失联。由于 DC2 和 Site3 构成多数派,etcd 集群继续正常服务。
      2. Patroni 层面:DC1 主库的 Leader Key 因无法续约而过期。DC2 中的异步备库 Patroni 检测到 Key 消失,参与竞选。
      3. 数据一致性权衡 :这是关键点。由于是异步备库,直接晋升会有数据丢失。设计时需要 DC2 的 Patroni 配置 synchronous_mode: off,但需要写脚本确认数据丢失量(LSN diff)在可接受范围内,或者直接选择全自动 Failover 接受可能的数据丢失,或者半自动 Failover 人工确认。
      4. 流量接管:DC2 的 Patroni 晋升成功后,其 Pgpool-II 通过 Watchdog 或脚本自动将写路由指向新主库。DNS 或全局负载均衡(GSLB)将应用流量从 DC1 切换到 DC2。
      5. 事后重建:DC1 恢复后,其所有 PG 实例都以备库身份重新加入集群,数据将被 DC2 的新主库重写。
  • 多角度追问

    1. 网络追问:DC1 和 DC2 之间的网络延迟假设 15ms,PG 流复制能力没问题。那 etcd 为什么不行?Raft 协议要求 Leader 将日志复制到多数派节点并得到确认后才能提交。跨 15ms 延迟会使 etcd 的写操作 TPS 急剧下降(可能不足 100),这会导致 Leader Key 续约瓶颈、超时,最终引发 etcd 集群不稳定和 Patroni 集群误判。
    2. 仲裁追问 :有没有办法让跨 DC 的 etcd 更稳定?使用 etcd 的 Learner 模式。Learner 是只读同步Raft日志但不参与投票的节点。可以将 DC2 的 etd 节点设为 Learner,这样在只有两个高延迟 DC 时,由 DC1 的两个 etcd 成员和 DC2 的一个 Learner 组成,Learner 不增加投票延迟,但在 Leader 切换时能快速追赶日志成为正式成员。
    3. 成本追问:有没有更简单的多数据中心方案?不使用分布式共识,而是采用"主备集群 + 异步流复制 + 脚本化灾备切换"的架构。平时只对灾备中心进行演练,真实切换时由 SOP 和脚本驱动,牺牲了 RTO,但消除了跨 DC 部署 etcd 的脆弱性。这是很多传统企业的选择。
  • 加分回答:真正意义上的多活(Active-Active)架构在该方案中并未实现。让两个 DC 的主库都能写入,属于分库分表、双向逻辑复制或像 Google Spanner 那样基于原子钟的分布式数据库的范畴,已超出 PostgreSQL 单体集群高可用的范畴。


PG 高可用组件速查表

组件 功能 核心配置/命令 适用场景 注意事项
流复制 实时数据同步 wal_level=replica, primary_conninfo, standby.signal 所有 HA 方案的基础 区别同步/异步,使用复制槽 (pg_replication_slots)
repmgr 轻量级自动故障转移 repmgr.conf, repmgrd, repmgr cluster show 中小企业、无额外 DCS 的简单环境 务必部署 Witness Server 防止双节点脑裂
Patroni 云原生自动故障转移与管理 patroni.yml, patronictl, REST API 大中型核心业务、云原生环境 (K8s) 需维护 etcd/DCS 集群;synchronous_mode 是实现 RPO=0 的关键
etcd 分布式键值存储,提供共识 etcdctl, Raft 协议 配合 Patroni 或任何需要选主的分布式系统 部署奇数节点 (3/5/7),对网络延迟高度敏感
Pgpool-II 读写分离、连接池、负载均衡 pgpool.conf, pcp 命令, Watchdog 需要透明读写分离和减少连接数的场景 自身需通过 Watchdog 实现高可用;注意与 Patroni 的集成方式
Witness repmgr 的第三方仲裁角色 repmgr.conf,配置为 witness repmgr 双节点集群防脑裂 极轻量,不存数据,可部署在任何一台闲置服务器或虚拟机上

延伸阅读

附录:Demo 代码与配置示例

A. Docker Compose 编排文件 (Patroni + etcd + Pgpool-II 集群)

以下是 docker-compose.yml 的核心片段,展示了如何在一台开发机上启动一个3节点的 PG 高可用集群。

yaml 复制代码
version: '3.8'
services:
  etcd1: &etcd
    image: quay.io/coreos/etcd:v3.5.9
    command: etcd --name etcd1 --initial-advertise-peer-urls http://etcd1:2380 --listen-peer-urls http://0.0.0.0:2380 --advertise-client-urls http://etcd1:2379 --listen-client-urls http://0.0.0.0:2379 --initial-cluster etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 --initial-cluster-state new
    ports: ["2379:2379"]
  etcd2:
    <<: *etcd
    command: etcd --name etcd2 ...
  etcd3:
    <<: *etcd
    command: etcd --name etcd3 ...

  patroni1: &pg_node
    image: patroni:latest # 需要事先构建或拉取
    hostname: pg-node-1
    environment:
      PATRONI_ETCD3_HOSTS: "etcd1:2379,etcd2:2379,etcd3:2379"
      PATRONI_SCOPE: "my_pg_cluster"
      PATRONI_NAME: "pg-node-1"
      PATRONI_RESTAPI_CONNECT_ADDRESS: "pg-node-1:8008"
      # ... 其他环境变量配置超级用户、复制用户密码 ..
    ports:
      - "8001:8008" # REST API
      - "5431:5432" # PG 端口
    depends_on:
      - etcd1
      - etcd2
      - etcd3

  patroni2:
    <<: *pg_node
    hostname: pg-node-2
    environment:
      PATRONI_NAME: "pg-node-2"
      PATRONI_RESTAPI_CONNECT_ADDRESS: "pg-node-2:8008"
    ports:
      - "8002:8008"
      - "5432:5432" # 映射到宿主机 5432 作为主库入口

  patroni3:
    <<: *pg_node
    hostname: pg-node-3
    environment:
      PATRONI_NAME: "pg-node-3"
      PATRONI_RESTAPI_CONNECT_ADDRESS: "pg-node-3:8008"
    ports:
      - "8003:8008"
      - "5433:5432"

  pgpool:
    image: pgpool/pgpool:4.4
    # ... 详细配置卷挂载 ...
    environment:
      PGPOOL_BACKEND_NODES: "0:pg-node-1:5432,1:pg-node-2:5432,2:pg-node-3:5432"
      PGPOOL_SR_CHECK_USER: "repmgr"
      # ... 关键配置使用环境变量注入 ...
    ports:
      - "9999:9999"

B. 流复制环境搭建 Shell 脚本

bash 复制代码
#!/bin/bash
# 文件名: setup-replication.sh
# 描述: 在主库上运行,为备库生成完整的搭建脚本
# 用法: sudo bash setup-replication.sh <主库IP> <应用名>

MASTER_IP=$1
APP_NAME=$2
REPL_USER="replicator"
REPL_PASS="strong_pass" # 生产环境应从密钥管理服务获取

# 1. 在主库上创建复制用户
psql -U postgres -c "CREATE USER ${REPL_USER} WITH REPLICATION LOGIN ENCRYPTED PASSWORD '${REPL_PASS}';"

# 2. 添加 pg_hba.conf 条目
echo "host    replication     ${REPL_USER}     all     scram-sha-256" >> /var/lib/postgresql/16/main/pg_hba.conf

# 3. 生成备库搭建脚本
cat > /tmp/init-standby.sh << EOF
#!/bin/bash
# 停止 PG 并清空数据目录
pg_ctl -D /var/lib/postgresql/16/main stop
rm -rf /var/lib/postgresql/16/main/*

# 从主库拉取基础备份
pg_basebackup -h ${MASTER_IP} -D /var/lib/postgresql/16/main -U ${REPL_USER} -P -R -S ${APP_NAME}_slot

# -R 选项已自动生成 standby.signal 和 primary_conninfo
# 手动插入复制槽名称 (pg_basebackup -S 不会自动将其写入 auto.conf)
echo "primary_slot_name = '${APP_NAME}_slot'" >> /var/lib/postgresql/16/main/postgresql.auto.conf

# 启动备库
pg_ctl -D /var/lib/postgresql/16/main start
EOF

chmod +x /tmp/init-standby.sh
echo "备库初始化脚本 /tmp/init-standby.sh 已生成,请在备库服务器上执行。"

C. repmgr 配置文件 (/etc/repmgr.conf)

ini 复制代码
# 节点基本信息
node_id=2
node_name='pg-node-2'
data_directory='/var/lib/postgresql/16/main'

# 监控与连接
conninfo='host=192.168.1.2 port=5432 user=repmgr dbname=repmgr'
primary_conninfo='host=192.168.1.1 port=5432 user=repmgr dbname=repmgr'

# 功能开关
use_replication_slots=1
promote_command='pg_ctl -D /var/lib/postgresql/16/main promote'
follow_command='/usr/bin/pg_ctl -D /var/lib/postgresql/16/main -w start'

# 故障转移参数
reconnect_interval=5
reconnect_attempts=6

# 日志
log_file='/var/log/repmgr/repmgrd.log'
log_level=INFO

# 服务控制
service_start_command='sudo systemctl start postgresql@16-main'
service_stop_command='sudo systemctl stop postgresql@16-main'
service_restart_command='sudo systemctl restart postgresql@16-main'
service_reload_command='sudo systemctl reload postgresql@16-main'

D. 故障模拟全链路脚本

bash 复制代码
#!/bin/bash
# 文件名: simulate-failover.sh
# 描述: 模拟pg-node-2宕机,观察并验证集群自愈能力

echo "[1/4] 当前集群状态:"
patronictl list my_pg_cluster

echo -e "\n[2/4] 注入故障:杀死 pg-node-2 的主进程..."
# 假设 pg-node-2 的 patroni 由 supervisor 管理,直接停掉 patroni
docker-compose stop patroni2

echo -e "\n[3/4] 等待 60 秒,观察故障转移过程..."
sleep 60
echo "故障转移过程中的 Patroni 日志 (来自 pg-node-3 备库):"
docker-compose logs --tail=20 patroni3

echo -e "\n[4/4] 最终集群状态与中间件验证:"
patronictl list my_pg_cluster
echo -e "\n Pgpool-II 内部路由表:"
docker-compose exec pgpool psql -U pgpool -c "SHOW POOL_NODES;"

echo -e "\n 结论: pg-node-3 已提升为 Leader,Pgpool-II 已更新路由表,pg-node-2 处于 down 状态。"
echo "故障模拟完成。RTO约等于 Patroni 检测时间 + TTL + PG 启动时间。"
相关推荐
敖正炀7 小时前
PostgreSQL 架构核心:进程模型、共享内存与 WAL
postgresql
敖正炀7 小时前
PostgreSQL 数据类型深度及存储原理
postgresql
敖正炀8 小时前
PostgreSQL 环境搭建与核心命令行实战
postgresql
曲幽9 小时前
让FastAPI Agent真正记住你:聊聊会话记忆与持久化存储的落地实践
redis·python·postgresql·fastapi·web·chat·async·session·ai agent
心流时间11 小时前
读书笔记-PostgreSQL实战
数据库·postgresql
AllData公司负责人1 天前
通过Postgresql同步到Doris,全视角演示AllData数据中台核心功能效果,涵盖:数据入湖仓,数据同步,数据处理,数据服务,BI可视化驾驶舱
java·大数据·数据库·数据仓库·人工智能·python·postgresql
小猿姐1 天前
GitLab on Kubernetes:使用 KubeBlocks 部署生产级高可用 PostgreSQL 和 Redis
redis·postgresql·kubernetes