Patroni + etcd + PostgreSQL 高可用架构(一):选举、领导者锁与脑裂防护

举报
yd_220455628 发表于 2026/11/09 16:29:53 2026/11/09
【摘要】 PostgreSQL流复制有数据冗余,却无主库权威判定。Patroni将主库身份外置到etcd,以带TTL的领导者锁和Raft共识确定唯一主库。本文拆解健康检查、领导者键续约与竞选、切换前检查,以及etcd不可达时的failsafe与watchdog防护,并界定其能力边界。下期讨论同步复制与流量切换。

Patroni + etcd + PostgreSQL 高可用架构:从“外挂式”协调到选举机制的内核

PostgreSQL 流复制解决了数据冗余的问题,但没有解决“谁说了算”的问题。主库宕机后,备库不会自动接管,集群会停在只读状态或彻底不可用,直到有人手工干预。Patroni 的价值不在于它“会做故障切换”,而在于它把“谁是主库”这一判断,从每个节点的本地视角,提升成了一个需要分布式共识才能单方面决定的全局事实。理解这一点,是理解整套架构的起点。

原生流复制的结构性缺口:没有“主库身份”的权威来源

PostgreSQL 的流复制允许备库持续接收并重放主库的 WAL,数据层面是冗余的。但“当前哪个节点是主库”这个信息,在原生 PG 中并不存在于任何共享的、有共识保障的位置。每个节点只知道自己的 pg_is_in_recovery() 状态,不知道其他节点是否还活着、是否仍然认为自己可写。当主库进程崩溃或所在节点失联时,没有任何内置机制去原子性地决定“由谁来接替”,也没有机制阻止原主库在恢复后继续以可写身份运行——这就是脑裂的根源。

Patroni 的设计选择非常明确:不改造 PostgreSQL 内核,而是在每个 PG 实例旁边放一个 Patroni 进程,把“主库身份”外置到一个分布式配置存储(DCS)中,通过持有 DCS 中的一把带 TTL 的领导者锁来声明“我的 PostgreSQL 实例是主库”。只有能持续续约这把锁的节点,才被允许让 PostgreSQL 保持可写状态。这把锁的原子性和唯一性,由 DCS 的共识协议保障。在 etcd 作为 DCS 的场景下,这个保障来自 Raft。

etcd 的角色:不是“存状态的数据库”,而是选举的共识基座

把 etcd 理解为“存集群配置的键值库”是不够的。它在 Patroni 架构中的核心职能是提供一个线性一致性的、支持原子 CAS(比较并交换)操作的领导者锁,使得在任意时刻,至多只有一个 Patroni 节点能成功持有这把锁。

etcd 用 Raft 算法在集群内部达成共识。一个写请求(比如创建或续约领导者键)必须经过多数派节点确认才能提交,因此一旦写入成功,所有后续读取都会看到这个结果。Patroni 正是利用这一点:节点的“主库资格”取决于它能否在 etcd 中成功执行一次写操作,而不是取决于本地时钟或本地状态判断。

这解释了为什么生产环境推荐 etcd 至少 3 个节点:Raft 的多数派要求意味着 3 节点集群可以容忍 1 个 etcd 节点故障而不失去选举能力;2 节点集群则无法在任意 1 个节点故障时维持多数派,选举会停滞。etcd 集群的规模通常建议在 3 到 7 个奇数节点之间,超过 7 个带来的写性能开销通常得不偿失。Patroni 与 etcd 之间的交互通过 etcd v3 API 的 gRPC 接口完成,领导者键的更新是一次带有版本或值校验的原子操作:如果当前键的版本已经变化(意味着另一个节点抢先一步),本次更新就会失败,该节点不会获得主库资格。

健康检查循环:每一次循环都在回答“我还配不配当主库”

Patroni 的核心运行逻辑是一个以 loop_wait(默认 10 秒)为周期的心跳循环。每一轮循环中,Patroni 做三件事:读取 DCS 中领导者键的当前状态;判断自己是否持有该键;根据判断结果决定续约、竞选或保持从库身份。

对于当前主库的 Patroni,循环的核心动作是续约领导者键。它在 etcd 中更新键的值(通常携带新的 renewTime),同时刷新 TTL。如果续约成功,它的 PostgreSQL 继续以主库身份运行;如果续约失败——不是因为键被别人抢走,而是因为 etcd 不可达、网络分区或本地 Patroni 进程异常——Patroni 会立即将 PostgreSQL 降级为只读(pg_ctl promote 的反向操作,实际上是通过 pg_ctl stop 后以恢复模式启动,或直接执行 SELECT pg_demote()),而不是让一个无法证明自己“仍然被授权”的节点继续接受写入。

对于从库的 Patroni,每一轮循环都在监视领导者键的 TTL。当键过期或消失时,它们进入领导者竞争状态:向 etcd 发起创建/抢占领导者键的 CAS 请求。只有一个节点会成功,因为 etcd 的 Raft 层保证了这次写操作的原子性;其余节点的请求会因为版本不匹配而失败。

从“锁过期”到“新主就绪”:故障切换的判定条件

领导者键过期只是触发了竞选信号。一个从库 Patroni 要真正获得 promotion 资格,还必须通过三项数据层面的检查,这些检查决定了故障切换是“无损”还是“有损”。

复制延迟检查。 候选节点的 replication lag 不能超过 maximum_lag_on_failover 的阈值。如果某个备库落后太多,即使它最先抢到锁,也不应该被提升,否则会丢失大量已提交事务。

WAL 时间线兼容性。 候选节点必须处在一个与集群当前状态兼容的 WAL 时间线上。如果之前发生过切换或 pg_rewind,时间线会发生分叉,不兼容的节点即使数据最新,也不应被提升。

同步复制下的法定人数。 如果集群启用了同步复制,候选节点必须在 synchronous_standby_names 所定义的同步备库集合中,意味着它已经确认接收了主库崩溃前所有需要同步的 WAL 记录。这是用“写可用性”换“写持久性”的取舍:在无法确保事务不丢失时,Patroni 会选择让写服务短暂不可用,而不是让一个可能缺数据的节点成为新主。

最先抢到锁且通过上述检查的节点,其 Patroni 调用 pg_promote() 将本地 PostgreSQL 提升为主库。其余节点随后会观察到新的领导者键持有者,并将自己的复制源切换到新主。当原主库在故障后恢复、重新加入集群时,如果它发现领导者键已被其他节点持有,Patroni 会将其 PostgreSQL 停止,必要时执行 pg_rewind 将它的数据目录与新主的时间线对齐,然后以从库身份重新启动。

脑裂防护的另一层:当 etcd 本身不可达时

上述机制依赖一个前提:Patroni 能判断“续约失败是因为我出了问题,还是因为 etcd 出了问题”。如果 etcd 集群短暂不可达,而主库实际上完全健康,直接降级会造成不必要的可用性损失。Patroni 的DCS 故障安全模式试图在不牺牲安全性的前提下缓解这个问题。

在 failsafe 模式下,当前主库在续约失败后,不会立即降级,而是通过 Patroni REST API 向 /failsafe 键中记录的所有集群成员发起探测。只有当所有已知成员都确认可达时,主库才被允许维持主库身份;任何一个成员不可达,主库仍然会降级。这个设计刻意不采用 etcd 的多数派逻辑,因为 Patroni 集群的节点分布不一定与 etcd 节点分布对齐;一个在 etcd 少数派一侧的主库,可能在 Patroni 层面仍然是大多数存活节点的连接对象,但为了避免脑裂,它必须降级,而 failsafe 模式提供了一条仅在“所有成员都可证实地活着”时才维持写服务的受控通道。

另一层防线是 watchdog。当 Patroni 进程本身假死、无法执行降级动作时,watchdog 设备(硬件或软件)会在心跳超时后强制重启节点,确保一个“卡住但还没死”的 PostgreSQL 不会继续接受写入。这解决的是 Patroni 自身控制平面失效的场景,而不是 etcd 或网络场景。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。