Patroni + etcd + PostgreSQL 高可用架构(三):可观测性与日常运维——监控分层、PITR 与 flap
一、监控:不要问“端口通不通”,要问“这个节点被授权做什么”
第一期讲过,Patroni 通过 etcd 的领导者锁决定主库身份。这个事实在监控层面的直接推论是:一个节点的 PostgreSQL 端口可达,不等于它被授权接受写入;一个节点进程活着,不等于它应该出现在读流量池里。
Patroni 的 REST API,默认监听 8008,暴露了一组健康检查端点,它们的 HTTP 状态码直接反映节点在集群中的角色授权状态,而不仅仅是进程存活。关键端点包括:
-
/primary,或/read-write:仅在节点持有领导者锁且 PostgreSQL 以主库运行时返回 200。这是写流量代理应该使用的检查端点。 -
/replica:仅在节点处于 running 状态、角色为 replica 且noloadbalance标签未设置时返回 200。还可以附加?lag=<max-lag>参数,进一步要求复制延迟低于指定阈值才返回 200。 -
/leader:只要节点持有领导者锁就返回 200,不区分它是主库还是 standby cluster 的 leader。 -
/health:仅当 PostgreSQL 进程可响应时返回 200,不反映角色。如果只用这一个端点做负载均衡健康检查,一个被降级为只读的旧主仍然会通过检查,写流量会被导向一个不再接受写入的节点。
监控体系因此需要分层,而不是一个“通/不通”的二元信号:
第一层,角色授权层。 采集 /primary 和 /replica 的 HTTP 状态码。/primary 从 200 变为非 200,是写入口消失的最直接信号;/replica 的非 200 意味着该节点不应再接收读流量。
第二层,复制健康层。 patronictl list 输出的 Lag 列,或 REST API 返回的复制延迟字段,是第二道防线。Patroni 在计算延迟时,使用的是 DCS 中 cluster.last_leader_operation 记录的领导者 WAL 位置与本地重放位置的差值。这个值持续增长,说明备库正在落后,即使 /replica 仍返回 200,读一致性风险也在累积。
第三层,DCS 层。 etcd 的可用性和延迟是整套控制平面的地基。日常巡检应包含 etcdctl endpoint health 和 etcdctl endpoint status --write-out=table,关注 IsLeader 分布和请求延迟。etcd 的磁盘 I/O 延迟如果超过 1 秒,Raft 心跳和领导者锁续约都可能超时,进而触发不必要的切换。一个常被忽略的实践是:etcd 应独立部署在专用节点上,不要与 PostgreSQL 共置。数据库负载的 I/O 尖峰如果影响到 etcd 的 fsync 延迟,控制平面的稳定性会被数据面的波动直接拖垮。
这三层的分工可以这样理解:DCS 层回答“控制平面是否健康”,角色授权层回答“这个节点当前被允许做什么”,复制层回答“如果现在切换,数据代价有多大”。只看任何一层,都会漏掉一类故障模式。
二、备份:HA 不覆盖“数据被错误修改”和“存储层整体损坏”
第一期的脑裂防护和第二期的 pg_rewind 都在回答“故障后如何恢复服务”。但有一类场景不在 HA 的覆盖范围内:数据被错误地修改或删除,并且错误已经通过流复制传播到了所有备库。DELETE 不带 WHERE 条件、应用 bug 写入了错误数据、或者恶意操作——这些场景下,所有节点上的数据是一致的,但一致地错了。HA 让你高可用地拥有一份错误数据。PITR,即时间点恢复,才是这类场景的出口。
pgBackRest 与 Patroni 的集成在标准模板中已经给出了明确形态:archive_mode: 'on',archive_command 调用 pgbackrest archive-push,archive_timeout: 300。这三个配置共同定义了一个恢复窗口:
-
左边界:最旧的可恢复基础备份链的起点。
-
右边界:已成功归档到仓库的最新一条 WAL 所对应的时间点。
archive_timeout: 300 的工程含义值得单独说明。在写入量很小的集群上,一个 WAL 段可能很长时间都填不满,如果 PostgreSQL 一直等待段写满才归档,最近的恢复点就会不断远离当前时间。设置 300 秒超时后,PostgreSQL 会强制切换一个只填充了一部分的 WAL 段并触发归档,从而把恢复窗口的右边界保持在“当前时间减去数分钟”的量级。它不是精度的保证,但它防止了恢复窗口在低写入期间无声地扩大。
恢复窗口的宽度由备份保留策略决定,但“有窗口”和“能在窗口内恢复”是两件事。 pgBackRest 的 check 命令、以及定期执行的恢复演练,才能验证备份链是否完整、WAL 归档是否有缺口。一个从未被恢复过的备份,在真正需要它的时刻只是一个假设。生产实践中的一条硬规则是:备份必须可恢复、可时间点定位、且不在同一故障域内——备份仓库与数据库节点共享存储或同一可用区,等于没有备份。
三、flapping:频繁切换不是“太灵敏”,而是某个层在反复撒谎
节点频繁切换,即 flapping,是 Patroni 集群在生产中最消耗信任的故障模式。表面上看是“自动切换太积极了”,但根因几乎总是某个底层组件在间歇性地报告错误状态,导致领导者锁被反复获取和释放。
flapping 的根因可以按层拆解:
第一层:DCS 不稳定。 etcd 的磁盘 I/O 延迟毛刺、Raft 选举抖动、或网络分区导致领导者锁续约间歇性失败。Patroni 日志中的典型模式是 Failed to acquire leader lock 或 etcd timeout 反复出现。这一层的信号应当从 etcd 侧确认:etcdctl endpoint status 的延迟分布、etcd_server_leader_changes_seen_total 是否在增长、以及 etcd 所在节点的磁盘 fdatasync 延迟。
第二层:节点间网络或 Patroni API 超时。 Patroni 节点之间通过 REST API 互相探测。如果节点间 RTT 周期性超过 retry_timeout 允许的范围,或者防火墙对 8008 端口的策略导致部分探测丢失,就会出现“节点活着,但 Patroni 认为它不可达”的状态。ping 和 mtr 的丢包率、以及 journalctl -u patroni 中与连接超时相关的日志,是这一层的证据。
第三层:PostgreSQL 自身间歇性不可用。 OOM Killer 杀死 postgres 进程、WAL 目录被写满、checkpoint 期间 I/O 尖峰导致健康检查连续失败——这些都会让 Patroni 的 /health 探测在短时间内多次失败,从而触发降级。dmesg 中的 Killed process、pg_wal 的磁盘使用率、以及 postgresql.log 中的 PANIC/FATAL 是这一层的关键线索。
诊断 flapping 时,不要从“切换逻辑”入手,而要从“哪一层的信号在抖动”入手。一个实用的顺序是:先确认 etcd 的延迟和领导者稳定性,再确认节点间网络质量,最后检查 PostgreSQL 进程和磁盘。如果三层都干净,再回头看 ttl、loop_wait、retry_timeout 之间的比例是否过于激进。默认的 ttl=30、loop_wait=10 在多数环境中是合理的起点;把 ttl 调得过低来“加快切换”,恰恰会放大底层抖动对控制平面的影响。
四、rewind 失败后的两条路:重试对齐,或者接受一次重建
第二期讨论了 pg_rewind 在旧主回归中的角色,以及 wal_log_hints 或数据校验和是它生效的前置条件。当这个前置条件未被满足,或者 rewind 因为时间线、系统标识符不兼容而失败时,Patroni 需要一条明确的兜底路径。
Patroni 提供了一个与 rewind 失败相关的参数:remove_data_directory_on_rewind_failure。默认值为 false。当它保持 false 时,如果 rewind 失败,Patroni 会尝试让该节点跟随新领导者继续以从库运行,而不是自动清空数据目录。当它设为 true 时,Patroni 会删除该节点的 PostgreSQL 数据目录,并通过基础备份重新克隆一个备库。
这个选择的分界线是恢复时间与人工干预的取舍。设为 false 的好处是保留了“也许还能救一下”的可能性——有时候 rewind 的失败是暂时的,手动修复兼容性问题后可以重新触发。代价是如果 rewind 的根因无法快速解决,这个节点会长期处于不可用或异常状态,需要人工执行 patronictl reinit 来强制重建。设为 true 则把这条路径自动化:接受一次全量基础备份的时间成本,换取节点自动回到健康状态,不需要人工介入。
对于数据量较大、但节点网络和备份仓库带宽充足的集群,remove_data_directory_on_rewind_failure: true 是一个更偏向“可运维性”的选择:它把一次需要人工判断的故障,转化为一次可预测的自动化重建。对于备份窗口有限、重建成本极高的集群,保持 false 并配合对 rewind 失败事件的告警,把决策留给运维人员,是更谨慎的做法。无论选哪条路,关键是在集群规划阶段就明确这个决策,而不是等 rewind 第一次失败时临时决定。
- 点赞
- 收藏
- 关注作者
评论(0)