概述
前文《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。
文章组织架构图
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 Sender 和 WAL Receiver。
1.1.1 启动流程与握手
当一个备库启动时,它会经历一个从"追赶"到"跟随"的状态转换。
- 备库发起连接 :备库的启动进程(Startup Process) 根据
primary_conninfo配置项指定的连接字符串,向主库发起一个标准的 PostgreSQL 客户端连接。在连接参数中,它会声明自己的身份、要复制的 WAL 起始位置(LSN)以及复制槽名称(如果使用)。 - 主库启动 WAL Sender :主库的WAL Sender 进程 在收到复制请求后,会首先进行握手。它检查
pg_hba.conf中的replication权限条目,验证请求者身份。一旦通过,WAL Sender 就成为一个专门为该备库服务的后端进程,其生命周期与复制连接绑定。 - 握手与参数协商 :主库与备库之间会进行协议参数协商,包括:
- 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 日志可被应用。
图 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 = off 或 local)是默认的行为,也常是追求极致性能时的选择。主库提交事务时,完全不等待备库的任何确认。
- 内部实现 :事务提交时,
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,我一个都不能丢,不要清理!"
-
内部机制 :
- 创建:在主库执行
SELECT pg_create_physical_replication_slot('slot_for_sby1');。这会在pg_replslot/slot_for_sby1/目录下创建元数据,并在共享内存中注册。 - 使用:备库在
primary_conninfo或recovery.conf(PostgreSQL 12之前版本)中配置primary_slot_name = 'slot_for_sby1'。连接时,主库会将该备库的进程与这个槽位关联。 - 向前推进:每当备库接收并确认(flush)了某个位置的 WAL 后,它会向主库报告其最新的
flush_lsn。主库收到报告后,会将复制槽的restart_lsn推进到备库确认的安全位置。 - 断连保护:当备库离线时,报告中断,复制槽的
restart_lsn就停在了它离线前的位置。主库的检查点(Checkpointer)进程在执行 WAL 清理时,会遍历所有活跃的复制槽,并永远不会清理 小于任何restart_lsn的 WAL 文件。
- 创建:在主库执行
-
监控与风险 :
sqlSELECT 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.signalvsrecovery.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)
计划内切换是数据库运维的必备技能,用于主库硬件升级、内核升级等场景。它允许零数据丢失的优雅切换。
步骤与原理:
-
旧主库优雅关闭(
pg_ctl -m fast stop) :-m fast: 终止所有活跃连接,回滚未完成的事务,执行一次 Checkpoint后关闭。这确保了数据文件的完整性,且在关闭前所有的 WAL 都已刷盘。
-
检查备库同步状态 :
- 在切换前,必须在旧主库上反复确认备库已追上。
sqlSELECT 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,表示所有数据已同步,可以做到零数据丢失。 -
将选定的备库提升为新主库(
pg_ctl promote) :- 这是最关键的一步。在选定的备库上执行
pg_ctl -D /var/lib/postgresql/16/main promote。 - 内部原理 :
pg_ctl promote并非执行新的命令,而是在备库的数据目录创建一个promote.signal文件,然后向备库的启动进程发送SIGUSR1信号。启动进程收到信号并检测到文件后,会完成当前WAL段的应用,然后立刻切换角色:将standby.signal标记为已失效,关闭恢复模式,启动写事务的能力,并时间线(Timeline)增加 1。
- 这是最关键的一步。在选定的备库上执行
-
将旧主库配置为新备库 :
- 在旧主库上,创建
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 节点上的守护进程,其核心工作逻辑是一个事件循环:
- 本地监控:它通过本地 Unix Socket 定期轮询本地的 PostgreSQL 进程状态。
- 主库心跳 :它维护一条到当前记录的主库节点的连接,并执行轻量级查询(如
SELECT 1)作为心跳检查。 - 状态上报 :它将本地节点的健康状态、WAL 位置等信息写入到每个节点的本地
repmgr元数据库中(该库通过流复制同步)。 - 故障检测 :当备库上的
repmgrd检测到与主库的心跳连接连续失败(超过reconnect_attempts * reconnect_interval)时,它会与其他备库节点通信,发起故障转移。
2.2 自动 Failover 流程
图 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 集群交互,构成了一个强大的分布式高可用系统。
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 端口)。
HAProxy或Pgpool-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
scope与name: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 作为一个反向代理,接管了所有应用的数据库连接。
图 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. 三种高可用方案对比与选型建议
单一的工具无法覆盖所有场景,理解其差异是做出合理架构决策的前提。
图 5:三种高可用方案对比矩阵图
- 图表主旨概括:此图旨在通过多维度的对比,帮助读者快速定位 Patroni、repmgr 和 Pgpool-II 的各自定位和组合关系。
- 逐层/逐元素分解 :
- 功能定位 :
Patroni和repmgr是故障转移(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
操作步骤:
- 确认当前状态 :
patronictl list,确认pg-node-1为 Leader。 - 模拟主库宕机 :在
pg-node-1上执行sudo systemctl stop patroni或kill -9Patroni 和 PostgreSQL 进程。 - 观察 etcd 行为 :在一个 etcd 节点上执行
etcdctl watch /service/my_pg_cluster/leader。 - 观察备库行为 :查看
pg-node-2的 Patroni 日志tail -f /var/log/patroni.log。 - 检查新主库 :执行
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-2的Role变为Leader,且Timeline从 3 变为 4,表示发生了一次promote操作。pg-node-3的角色变为Sync,并紧跟随新的 Timeline 4。
-
日志解读 (
pg-node-2的patroni.log) :vbnetINFO: 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 仲裁
操作步骤(模拟网络分区):
-
环境:一个3节点的 Patroni 集群,etcd 也在3个节点上。
pg-node-1是 Leader。 -
制造分区 :在
pg-node-1上使用iptables阻断它与pg-node-2和etcd集群多数派的连接。bashsudo 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)的通信
预期现象与输出解读:
pg-node-1视角:它与 etcd 集群多数派失去联系,无法完成 Leader Key 续约。pg-node-1日志 :出现大量与 etcd 连接超时的错误。在 TTL 时间后,它会自动将自己降级为只读的备库(read-only) ,以防止脑裂。patroni.log会显示"Demoting self because of DCS is not accessible and I am a leader"。- etcd 集群视角 :
pg-node-1的 Leader Key 过期被删除。 pg-node-2与pg-node-3视角 :它们构成 etcd 的多数派,Watch 到 Key 删除后,发起新一轮选举,其中一个(如pg-node-2)成功当选为新 Leader。- 最终结果 :
patronictl list会显示pg-node-1处于非健康状态或降级备库,而pg-node-2是新 Leader。完全杜绝了双主现象。
6.3 故障三:Pgpool-II 感知主备切换
操作步骤:
- Pgpool-II 已配置通过
wd_lifecheck或自定义脚本查询 Patroni REST API。 - 在 Patroni 集群上执行一次 Switchover:
patronictl switchover。 - 观察 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."的信息。
图 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),如一份数据至少三副本(一主一同步备一异步备),防止同步备库故障导致主库写操作全部挂起。
- 内部实现 :启动时,备库通过
- 多角度追问 :
- 架构追问:如果网络抖动导致同步备库短暂失联再迅速恢复,会发生什么?主库上的事务会被阻塞直到备库重连或超时/被手动从同步列表中移除。
- 性能追问 :主备之间的网络延迟对
remote_apply的性能影响有多大?影响非常大,因为每个事务提交都多了一次网络往返(RTT),对于高频小事务型场景可能是灾难性的。 - 安全追问 :流复制的网络连接是加密的吗?不默认加密,应通过
pg_hba.conf强制使用scram-sha-256认证,并强烈建议在网络上使用SSH Tunnel或SSL/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,成功者晋升为新主库。
- 详细解释 :
- 内部实现 :
- 注册与续约:集群启动时,第一个成功向 etcd 写入 Leader Key 的节点成为主库。此后,主库的 Patroni 会周期性(如每 TTL/2 秒)向 etcd 发送续约请求,刷新 Key 的 TTL。
- 故障检测与选主 :所有备库都在 Watch 这个 Key。一旦主库宕机,续约停止,Key 在 TTL 过期后被 etcd 删除。所有 Watch 的备库几乎同时收到删除事件,并立即尝试通过一个原子性的
CAS(Compare-And-Swap,即比较并交换)操作("如果 Key 不存在,则创建")去创建 Leader Key。仅有一个节点能成功,它即成为新 Leader。
- 内部实现 :
- 多角度追问 :
- 架构追问:如果 etcd 集群自身发生分区,只剩下少数派(如3节点中的1个),Patroni 集群还能工作吗?不能。etcd 的 Raft 协议要求多数派存活才能提供服务。因此,故障转移也会失败,所有 Patroni 节点都将进入只读状态以防止脑裂。
- 安全追问 :如何防止一个被错误恢复的旧主库("僵尸"主库)再次写入,造成脑裂?当它恢复后,其 Patroni 会发现自己不是 Leader,因为 etcd 中的 Leader Key 已不属于它。它会立即执行降级(
demote)操作,将自己变成一个跟随新主库的备库,完美解决了脑裂。 - 运维追问 :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 功能相对单一,专注于节点管理和切换。
- 多角度追问 :
- 选型追问 :如果公司只有2台物理服务器跑 PG,该选谁?这种情况下部署完整 etcd 集群不现实。repmgr + Witness 是绝佳选择,Witness 可以轻松地放在一台管理服务器或虚拟机上。
- 架构追问:在 Kubernetes 上部署 PG 高可用,两者如何处理?K8s 本身就是一套类似 DCS 的系统。Patroni 可以很好地利用 K8s API 代替 etcd 来存储状态和进行选主,是 K8s 环境的事实标准。repmgr 的部署模式在K8s上则显得水土不服。
- 体验追问:从一个初级 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,识别出SELECT或INSERT/UPDATE/DELETE等。通过load_balance_mode=on配合black_function_list/white_function_list来控制函数的路由。对于显式事务(BEGIN...COMMIT),如果在事务中出现写操作,Pgpool-II 会将整个事务内的所有后续语句(包括读)都发往主库,以保证数据一致性。 - 感知切换 :集成方案有多种:
- 脚本方式 :最灵活。配置
failover_command和follow_primary_command,脚本内调用curl查询 Patroni REST API 获取新拓扑,然后用pcp_*命令更新 Pgpool-II 内部状态表。 wd_lifecheck方式:直接配置 Pgpool-II 去 ping Patroni 节点的 API。这是与 Patroni 集成的最佳实践。- SQL 探测方式 :传统方式,通过定时执行
SELECT pg_is_in_recovery()来判断节点角色,延迟相对较高。
- 脚本方式 :最灵活。配置
- 读写分离实现 :Pgpool-II 的
- 多角度追问 :
- 一致性问题 :读写分离带来的"复制延迟"问题,Pgpool-II 如何解决?没有完美方案。可以通过
delay_threshold参数让 Pgpool-II 自动摘除延迟过大的备库,但这是一种粗粒度的逃避。根本解决还是在应用层面容忍一定延迟,或对实时性要求高的查询指定发往主库。 - 连接池追问 :Pgpool-II 的连接池和数据库直连的性能差异?连接池在短连接、高并发的场景下优势巨大,可以显著减少 PG 的
fork开销和连接数。但在长连接、连接数少的场景下,其自身作为一个代理可能会增加几毫秒的网络延迟。 - 高可用追问:如果 Pgpool-II 自己挂了怎么办?使用 Pgpool-II 自身的 Watchdog 功能部署多个 Pgpool-II 节点,并通过 VIP(虚拟IP)实现自身的高可用。
- 一致性问题 :读写分离带来的"复制延迟"问题,Pgpool-II 如何解决?没有完美方案。可以通过
- 加分回答 :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的 WALlag;2) 设置max_slot_wal_keep_size,允许主库在空间不足时自动"抛弃"落后的备库以保护自身。
- 内部实现 :它是一个持久化的指针
- 多角度追问 :
- 架构追问 :物理复制槽和逻辑复制槽有什么区别?物理复制槽同步的是二进制 WAL 字节流,主备数据块完全一致。逻辑复制槽则对 WAL 进行解码(Decode),输出的是
INSERT、UPDATE等逻辑变更,用于跨版本、跨平台或部分表同步。 - 运维追问 :如何清理一个不再使用的僵尸复制槽?使用
SELECT pg_drop_replication_slot('slot_name');。在删除前,务必确认没有下游在使用它,否则会导致备库同步中断。 - 故障追问:如果备库因故障丢失了复制槽,会怎样?备库启动时会报告找不到槽,无法同步。处理办法是:在主库为它重建一个新槽,然后在备库执行全量重建流程。
- 架构追问 :物理复制槽和逻辑复制槽有什么区别?物理复制槽同步的是二进制 WAL 字节流,主备数据块完全一致。逻辑复制槽则对 WAL 进行解码(Decode),输出的是
- 加分回答 :在 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 而无法完成投票流程。
- 方案一:Patroni + etcd (依赖分布式共识) :etcd 通过 Raft 协议保证了其内部状态的强一致性。只有获得 etcd Leader Key 的节点才能作为主库。当旧主库处于网络分区时,它无法联系到 etcd 多数派,导致其 Leader Key 过期。Patroni 会检测到这一点并立刻执行自我降级(
- 多角度追问 :
- 极端场景:如果 Patroni 的自我降级代码有 Bug 并没能执行成功,该怎么办?这属于极端灾难。某些极高级别安全系统会引入 STONITH(Shoot The Other Node In The Head,即爆头)机制。由集群仲裁程序通过网络控制的 PDU(电源分配单元)直接切断旧主库服务器的电源,在物理上消灭它。
- repmgr 细节:repmgr 的 Witness 故障了,集群还有脑裂保护吗?没有。在两个数据节点的集群中,Witness 单点故障将导致集群无法进行自动故障转移,因为任何一侧的备库都无法获得绝对多数(2/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 节点的/patroniAPI。- 正常时,脚本可以通过查询
/cluster端点列出所有节点及其角色。 - 故障转移时,Pgpool-II 检测到后端某个节点挂了,调用
failover_command脚本。脚本通过 API 触发 Patroni 的自动恢复或感知到新主库,然后用pcp_attach_node/pcp_promote_node等命令更新 Pgpool-II 的路由表。
- 正常时,脚本可以通过查询
- 多角度追问 :
- 解耦追问 :如果不想让 Pgpool-II 和 Patroni 有这种直接集成,有其他方案吗?完全可以。在它们中间加一层
HAProxy。HAProxy 通过 Patroni REST API/primary和/replica来做健康检查。应用连HAProxy的读写端口做写操作,连其只读端口做读操作。Pgpool-II 只作为连接池连在中间,这个架构的解耦程度更高。 - 顺序追问 :Pgpool-II 的
failover_command如果比 Patroni 更早检测到故障并执行,会不会乱套?这是一个必须规避的竞态条件(race condition)。最佳实践是让failover_command脚本保持幂等,并且主要是探测和等待,而不是主动去触发pg_ctl promote。Promote 动作必须由 Patroni 集中决策。 - 性能追问:Pgpool-II 定期查询 API 会带来多大的开销?极低。通常只是一个简单的 HTTP GET 请求查询健康状态,响应体很小。即使每秒查询一次,对 Patroni 进程和网络的影响也微乎其微。
- 解耦追问 :如果不想让 Pgpool-II 和 Patroni 有这种直接集成,有其他方案吗?完全可以。在它们中间加一层
- 加分回答 :一个典型的
failover.sh脚本的核心逻辑是一个循环:while true; do sleep 2; curl -s PatroniAPI | ... ; if [new_primary_found]; then pcp_command...; break; fi; done,优雅地处理了等待和幂等性。
8. 计划内的主备切换(Switchover)应该如何操作?有哪些注意事项?
- 一句话回答 :计划内切换推荐使用
patronictl switchover或repmgr standby switchover,工具会自动执行预检、同步待命、切换角色、更新拓扑等一系列步骤,比手工操作安全得多。 - 详细解释 :
- 手工步骤 :停止旧主库 -> 确保备库完全同步(
flush_lag = 0) -> 在目标备库上执行pg_ctl promote-> 将旧主库改为备库并启动跟随。 - 注意事项 :
- 应用停写:在切换窗口,务必先暂停业务写入或确认没有长事务,否则等待同步可能需要非常久。
- VIP/中间件切换 :手工切换最容易忘记的是 DNS/VIP 或负载均衡配置的更改。
Patroni+Pgpool-II/HAProxy的组合则完美规避了这个问题。 - 回滚 :切换前想好回滚方案。在 Patroni 中,回滚就是再执行一次到原节点的
switchover。
- 手工步骤 :停止旧主库 -> 确保备库完全同步(
- 多角度追问 :
- 内核追问 :
pg_ctl promote在数据库内部到底做了什么?它创建promote.signal文件,然后向 Startup Process 发送SIGUSR1信号。Startup Process 收到信号后,完成当前 WAL 应用,然后将standby.signal标记为失效,关闭恢复模式,开始允许写操作,并将时间线 +1。 - 故障追问 :切换过程中,如果旧主库关闭后,新主库提升失败(如硬件故障),怎么办?这是最糟糕的情况。此时必须立刻放弃旧主库,尝试在原备库中另选一台进行提升。Patroni 可以在
switchover命令失败后,用failover命令来执行强制切换。 - 版本追问 :切换可以用来进行大版本升级吗(比如 PG 15 -> 16)?不可以。流复制备库提升后依然是同版本。大版本升级必须使用
pg_upgrade工具或逻辑复制。
- 内核追问 :
- 加分回答 :Patroni 的
synchronous_mode=on确保了在switchover时,被提升的候选者一定是数据最新的同步备库,RPO 为零。而且整个过程 Patroni 都会通过分布式锁保护,防止多个管理员同时进行错误操作。
9. 如何验证一个高可用集群的故障转移时间(RTO)和数据丢失量(RPO)?
- 一句话回答:RTO 可以通过编写脚本持续写入并计时,同时模拟主库宕机,测量从写入失败到在新主库上写入成功的时长来验证;RPO 可以通过比较切换前后主备库自增列或特定写入埋点的 Max ID 来估算数据丢失量。
- 详细解释 :
- RTO 测量 :使用一个简单的脚本循环
INSERT数据,记录每条的时间戳。在主库上kill -9PG 进程,观察脚本报错的时间。脚本需实现重连和重试机制。当在新主库上写入成功后,记录时间。时间差即为近似 RTO。 - RPO 测量 :在主库上用一个有自增主键的表,以极高频率写入。宕机后,在主库(如果不丢盘)和备库分别执行
SELECT max(id)。- 异步复制下,通常备库的值会略小于原主库,差值即为可能丢失的事务数。
- Patroni 同步复制下,被提升的新主库
max(id)应等于原主库,实现了 RPO = 0。
- RTO 测量 :使用一个简单的脚本循环
- 多角度追问 :
- 工具追问 :有开箱即用的 RPO/RTO 测试工具吗?可以使用 Percona 的
sysbench或专为 Postgres 设计的pgbench做持续性压力测试,并配合故障模拟脚本来观察其自带的统计信息。 - 精确性追问 :
max(id)方法测量 RPO 为什么是近似的?因为自增序列的缓存(cache)值可能丢失,导致计算出的差距比实际差距要大,造成误判。更精确的方法是使用pg_wal_lsn_diff()对比断连前的 LSN 和新主库的最后应用 LSN。 - 基线追问 :业界对 PG 高可用 RTO 和 RPO 的常见目标是什么?一个成熟的 Patroni 集群,RTO 通常在 30-60 秒 (取决于
etcd.ttl和检测时间),同步模式下 RPO 为 零。异步模式下 RPO 为毫秒级到秒级的数据量。
- 工具追问 :有开箱即用的 RPO/RTO 测试工具吗?可以使用 Percona 的
- 加分回答:真正严谨的 RTO/RPO 测试需要考虑各种场景,包括网络分区、节点掉电、磁盘慢 IO 等。Google 的《Site Reliability Engineering》一书强调了这类混沌工程的必要性。对于核心金融服务,通常要求 RPO=0, RTO<1min。
10. 流复制环境下,如何在线添加一个新的备库而不影响主库?
- 一句话回答 :使用
pg_basebackup从主库或一个现有的、健康的备库上拉取完整基础备份,并使用复制槽来确保备份期间所需的 WAL 不被主库清理。 - 详细解释 :
pg_basebackup参数 :-D <dir>:新备库数据目录。-R:自动生成standby.signal和primary_conninfo。-S <slot_name>:在主库上创建一个临时的物理复制槽,pg_basebackup结束时不会自动删除(PG 16行为),必须手动清理。-Fp:plain格式,直接拷贝文件,更适合在添加备库时使用。-Xs:在备份期间使用流复制方式获取 WAL,而不是等待 WAL 归档文件。
- 为何不影响主库 :
pg_basebackup本质上是一个长连接的长查询/复制操作。它会对磁盘和网络有IO负载,但不会阻塞其他事务进行。关键在备份期间,主库会因为复制槽的存在而保留 WAL,因此必须监控pg_replication_slots并监控主库磁盘,确保不会因为备份时间过长导致磁盘写满。
- 多角度追问 :
- 架构追问 :为什么推荐从备库而不是主库拉取
pg_basebackup?这能显著降低主库的IO和网络开销,尤其是在主库负载极高的时候。这对于超大型数据库的扩容至关重要。 - 运维追问 :
pg_basabcup -R自动生成的备库配置,还需要手动修改什么?通常需要核对primary_conninfo的连接信息,并根据需要添加primary_slot_name,并确保pg_hba.conf允许该新备库的连接。 - 网络追问 :如果跨机房拉取,网络很慢怎么办?可以在源端(主/备库)使用
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(复制有延迟)将被用于确认同步提交。如果此时主库宕机,sby1和sby2都无法提供全量数据,RPO > 0。 - Patroni 的动态智能管理 :开启
synchronous_mode后,Patroni 会做两件关键的事:- 严格筛选 :它只将那些 flush_lsn 完全等于主库
pg_current_wal_lsn()的备库,动态地添加到synchronous_standby_names列表中。如果没有任何备库是完全同步的,它会保持此列表为空,此时主库上的同步事务提交将会被阻塞,直到有一个备库追上。 - 安全选举:在执行 Failover 候选检查时,它会额外要求候选者必须是名单中的"同步备库",保证当选者数据绝对最新。
- 严格筛选 :它只将那些 flush_lsn 完全等于主库
- 静态配置的缺陷 :如果手动配置
- 多角度追问 :
- 可用性追问:如果所有备库都突然出现延迟,主库是不是就宕了?是的,如果业务全是同步事务,那么所有写操作都会被阻塞。这虽然在数据安全上做到了极致,但牺牲了可用性。因此,生产环境通常采用"一同步、多异步"的部署架构来平衡。
- 架构追问 :
synchronous_mode_strict又是什么?这是更严格(也更危险)的模式。当它开启时,如果 Patroni 探测不到任何一个健康的同步备库,它会直接关闭主库,以避免主库在没有同步守护的情况下裸奔。除非有像金融核心账务库这样极端的安全要求,否则生产环境不建议开启。 - 配置追问 :
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 整体故障) :
- etcd 层面:DC1 中的 etcd 节点失联。由于 DC2 和 Site3 构成多数派,etcd 集群继续正常服务。
- Patroni 层面:DC1 主库的 Leader Key 因无法续约而过期。DC2 中的异步备库 Patroni 检测到 Key 消失,参与竞选。
- 数据一致性权衡 :这是关键点。由于是异步备库,直接晋升会有数据丢失。设计时需要 DC2 的 Patroni 配置
synchronous_mode: off,但需要写脚本确认数据丢失量(LSN diff)在可接受范围内,或者直接选择全自动 Failover 接受可能的数据丢失,或者半自动 Failover 人工确认。 - 流量接管:DC2 的 Patroni 晋升成功后,其 Pgpool-II 通过 Watchdog 或脚本自动将写路由指向新主库。DNS 或全局负载均衡(GSLB)将应用流量从 DC1 切换到 DC2。
- 事后重建:DC1 恢复后,其所有 PG 实例都以备库身份重新加入集群,数据将被 DC2 的新主库重写。
- 架构设计 :
-
多角度追问:
- 网络追问:DC1 和 DC2 之间的网络延迟假设 15ms,PG 流复制能力没问题。那 etcd 为什么不行?Raft 协议要求 Leader 将日志复制到多数派节点并得到确认后才能提交。跨 15ms 延迟会使 etcd 的写操作 TPS 急剧下降(可能不足 100),这会导致 Leader Key 续约瓶颈、超时,最终引发 etcd 集群不稳定和 Patroni 集群误判。
- 仲裁追问 :有没有办法让跨 DC 的 etcd 更稳定?使用 etcd 的 Learner 模式。Learner 是只读同步Raft日志但不参与投票的节点。可以将 DC2 的 etd 节点设为 Learner,这样在只有两个高延迟 DC 时,由 DC1 的两个 etcd 成员和 DC2 的一个 Learner 组成,Learner 不增加投票延迟,但在 Leader 切换时能快速追赶日志成为正式成员。
- 成本追问:有没有更简单的多数据中心方案?不使用分布式共识,而是采用"主备集群 + 异步流复制 + 脚本化灾备切换"的架构。平时只对灾备中心进行演练,真实切换时由 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 双节点集群防脑裂 | 极轻量,不存数据,可部署在任何一台闲置服务器或虚拟机上 |
延伸阅读
- PostgreSQL 官方文档: High Availability, Load Balancing, and Replication
- Patroni 官方文档: patroni.readthedocs.io/
- repmgr 官方文档: repmgr.org/
- Pgpool-II 官方文档: www.pgpool.net/
附录: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 启动时间。"