Patroni 的选主并不是依靠 PostgreSQL 流复制自身配置或者 keepalived 这类 IP 漂移工具完成的,而是把分布式共识问题交给了 DCS。DCS 作为仲裁者,保存集群当前的主库标识、成员列表以及租约信息,所有节点通过读写这些键值参与竞选。理解 DCS 选主机制,需要从 Patroni 启动时如何与 DCS 交互开始。

Patroni 启动后会根据配置里的 scope 和 namespace 计算 DCS 路径,并尝试读取 leader 键。只要 leader 键中记录的节点不是自己,Patroni 就会把本地 PostgreSQL 实例维持在备库状态。只有当 leader 键不存在时,节点才会进入竞选流程。这个设计保证了任何时刻最多只能有一个节点通过 DCS 的原子写入拿到主库锁,避免多个 PostgreSQL 同时对外提供写服务。
DCS 在 Patroni 中的数据模型
Patroni 对 DCS 的使用可以抽象成一个带租约的键值存储。集群内最重要的键是 /service/cluster-a/leader,它保存当前主库的成员名、连接串和 API 地址。除 leader 键外,/service/cluster-a/members 记录所有成员状态,而 /service/cluster-a/initialize、/service/cluster-a/pause 等键负责初始化标记和人工维护开关。每个键的生命周期和更新方式不同,只有 leader 键必须配合租约使用。
leader 键的具体内容通常是一个 JSON 文档,它让所有节点都能快速知道当前主库是谁、怎么连接以及如何访问管理接口。下面是一个简化后的 leader 键值示例:
{
"leader": "postgresql1",
"conn_url": "host=192.168.10.11 port=5432",
"api_url": "http://192.168.10.11:8008",
"slots": {}
}
leader 键不是写入一次就永久有效,而是带有一个 TTL。持有该键的主节点必须周期性地向 DCS 续租,否则 TTL 到期后 DCS 会自动删除这个键。多个备库如果同时尝试创建 leader 键,只有第一个完成原子写操作的节点会成功,其余节点会收到键已存在的错误。这正是 Patroni 能避免双主的基础。不同的 DCS 后端在实现这个语义时使用的原语不同,后面会展开。
选主流程与关键参数
Patroni 的 HA 主循环每隔 loop_wait 秒运行一次。在一个循环里,节点会先从 DCS 拉取最新集群状态,然后根据自身角色执行不同动作。如果节点是主库,它会检查本地 PostgreSQL 是否仍然健康,健康则调用 DCS update API 续租 leader 键;不健康则先尝试重启 PostgreSQL,若无法恢复就会释放 leader 键并降级为备库。如果节点是备库,它会检查 DCS 中 leader 键是否存在。若 leader 键不存在且当前集群未设置暂停标记,它就会开始评估自己是否适合成为新主库。
评估过程会查看多个条件,包括 PostgreSQL 是否已经启动、是否处于恢复模式、复制延迟是否在 maximum_lag_on_failover 允许范围内等。如果开启同步复制,Patroni 会优先考虑同步备库,以降低已提交事务丢失的概率。满足条件的节点会调用 DCS 的原子写操作尝试创建 leader 键。创建成功的节点随后调用 PostgreSQL 的 promote 命令提升为主库,并在 DCS 中更新成员表;创建失败的节点继续作为备库等待下一轮循环。
scope: cluster-a namespace: /service/ ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 synchronous_mode: false
参数中 ttl 和 loop_wait 共同决定故障切换速度。假设 ttl 为 30 秒、loop_wait 为 10 秒,主库每 10 秒续租一次,若主库连续多个周期未能续租,leader 键将在 30 秒后过期。备库最迟在下一轮循环发现键已消失并开始竞选,因此从主库故障到新主库提升,理论最小时间约 30 秒,实际还会叠加 PostgreSQL 恢复、重启和复制检查的时间。把 loop_wait 调小可以缩短发现时间,但会增加 DCS 请求频率;把 ttl 调小可以更快判定主库失联,但也可能因为网络抖动造成误切换。生产环境通常保留默认值,仅在专有网络或高要求业务下调整。
retry_timeout 决定节点与 DCS 通信超时后的重试时长。在 etcd 短暂不可用时,Patroni 不会立即切换主库,而是持续重试直到超时。这样能防止 DCS 集群本身抖动时出现双主,但代价是故障切换时间可能被拉长。理解了三个参数之间的关系后,就可以根据网络质量和业务对 RPO 的要求做平衡。
不同 DCS 后端的实现差异
Patroni 支持 etcd、Consul、ZooKeeper 以及 Kubernetes 等后端,选主语义通过各自的原子写入和租约机制实现,但底层原语并不一样。etcd v3 使用 lease 和 transaction:先创建一个 30 秒的 lease,然后在事务中判断 leader 键的创建版本号,如果为 0 才执行 put 并把键与该 lease 绑定。续租时通过 keepalive 延长 lease,etcd 保证 lease 过期后自动删除绑定的键。这个模型清晰,也是官方推荐的首选后端。
Consul 使用 session 与 KV 键绑定。Patroni 创建 session 并设置 TTL,再通过 session 写入 leader 键。主节点必须定期调用 session renew 续租,一旦 session 失效,Consul 会自动删除所有与之绑定的 key。ZooKeeper 则依赖 ephemeral 节点,客户端会话断开后临时节点会被删除。它的 watch 是一次性的,Patroni 每次收到事件后都要重新注册 watch,导致实现上更复杂。三种后端都能满足选主要求,差异主要体现在运维成本、watch 语义和网络分区时的表现上。生产环境如果以 PostgreSQL 高可用为目标,etcd 通常是更稳妥的选择。
# 创建一个 TTL 为 30 秒的 lease
etcdctl lease grant 30
# 用 lease 原子创建 leader key,只有当 key 不存在时才写入
etcdctl put /service/cluster-a/leader '{"leader":"postgresql1"}' --lease=LEASE_ID
Kubernetes 后端虽然也能模拟租约行为,但它通过 ConfigMap 或 Endpoints 等资源保存状态,本质上不是专门的分布式配置存储。对于关键生产库,建议使用 etcd 或 Consul 这类具备成熟一致性保障的 DCS,以便获得更稳定的选主行为。
故障切换与脑裂防护
当主库发生断电或 PostgreSQL 进程崩溃时,Patroni 主节点可能仍然存活。它会在 HA 循环中检测到数据库异常,并按配置尝试本地重启。如果重启失败,主节点会主动释放 leader 键,或者因为无法续租而使 leader 键自动过期。备库看到 leader 键消失后启动竞选,成功者执行 pg_ctl promote 提升。此时旧主库如果没有被真正隔离,可能会继续接受写入,这是脑裂风险最大的时刻。
Patroni 的脑裂防护主要依赖两层机制。第一层是 DCS 的租约锁,确保同一时刻只有一个节点能持有 leader 键;第二层是提升后的处理,新主库会更新 DCS 中的成员信息,旧主库回来后通过 Patroni 判断自己的角色并主动降级,必要时使用 pg_rewind 从新主库拉取差异。对于已经提交但未同步到备库的事务,如果配置了同步复制,Patroni 会优先选择同步备库,尽量减少丢失。实际故障切换流程可以用以下日志片段简要观察:
INFO: no action. I am (postgresql2), the standby INFO: Lock owner: None; I am postgresql2 INFO: promoted self to leader because I had the lock INFO: cleared rewind state after becoming the leader
需要特别强调,如果 DCS 集群整体不可用,Patroni 不会盲目提升备库。因为所有节点都无法创建或续租 leader 键,任何主动提升都可能造成双主。这是 Patroni 在一致性和可用性之间的取舍:宁可暂时不可写,也不允许脑裂。理解了这一点,才能正确设计 DCS 的部署拓扑,例如 etcd 使用三个或五个节点,避免 DCS 自身成为单点。选主机制的核心并不是让 PostgreSQL 自己决定主备,而是通过 DCS 的租约、原子写入和回调事件形成可靠的分布式锁,这正是 Patroni 能稳定支撑 PostgreSQL 高可用的关键。