本文的範圍是 Patroni-based HA 的 failover lifecycle:從 leader 失聯偵測到舊 leader 重新接回,detection、election、promotion、reconfiguration、recovery 各段的配置、failure mode 與 recovery,以及 production 故障演練與容量規劃。

Failover lifecycle:detection、election、promotion、reconfiguration、recovery 各有配置

PostgreSQL 原生沒有 auto-failover;primary 掛了、application 卡死、SRE 手動 promote standby — 整個過程通常 5-30 分鐘。Patroni 把這條鏈拆成 自動化的 5 段 lifecycle、每段有自己的 trigger、配置、失敗模式:

段觸發動作失敗模式
DetectionLeader heartbeat 在 DCS(etcd / Consul)失聯Standby 們開始觀察、累積失聯時間到 TTLDCS 本身分裂 → false detection 啟動失敗 failover
ElectionTTL 過、DCS 開放 leader lockStandby 競爭寫 leader key(DCS quorum-based)Network partition → 兩邊都自認 leader(split-brain)
Promotion新 leader 寫 DCS key 成功跑 pg_ctl promote、停 streaming replication、開始接寫Standby 落後太多 → 拒 promote 或承接時資料缺
ReconfigurationPatroni REST API 通知 routing 層HAProxy / PgBouncer 切流量到新 leaderRouting 層 health check 慢 → 流量持續打舊 leader
Recovery舊 leader 恢復(手動 / 自動)跑 pg_rewind + 重接 streaming replication 為 standbyWAL divergence 太大 → 必須重 base backup

每段都有獨立配置、不是「設一個 timeout 就好」。後面分段展開。

Detection — DCS heartbeat 跟 TTL

 1# patroni.yml 核心配置
 2scope: myapp-pg-cluster
 3namespace: /db/
 4name: pg-node-1                                # 跟 hostname 一致
 5
 6etcd:
 7  hosts: etcd1:2379,etcd2:2379,etcd3:2379       # DCS quorum
 8  protocol: https
 9
10bootstrap:
11  dcs:
12    ttl: 30                                     # leader lock TTL
13    loop_wait: 10                               # patroni 主循環間隔
14    retry_timeout: 10                           # DCS retry 上限
15    maximum_lag_on_failover: 1048576            # standby 落後 1MB 內才能 promote
16    synchronous_mode: false                     # async / sync 取捨

關鍵直覺:

  • TTL (30s) = leader 失聯多久才被視為 dead。設太短(< 15s)會把 transient network jitter 當 dead;設太長(> 60s)unavailability 拖長
  • loop_wait + 2 × retry_timeout ≤ TTL:Patroni 文件要求調整這三個值時維持這條不等式;上面的 loop_wait=10、retry_timeout=10、ttl=30 剛好是 10 + 2 × 10 = 30
  • maximum_lag_on_failover:standby WAL 落後超過這個閾值就 不參與 election;防止「promote 一個落後 5 分鐘的 standby」資料丟失

Election — DCS quorum + watchdog 防 split-brain

1watchdog:
2  mode: required                                # required / automatic / off
3  device: /dev/watchdog
4  safety_margin: 5

Election 期間最大風險是 split-brain — network partition 下、舊 leader 還活著但跟 DCS 斷線;新 leader 從 standby 升上來、application 同時連兩個 PostgreSQL 寫。資料 divergence 後 無法自動 reconcile。

防護機制兩層:

  1. DCS quorum:etcd / Consul 至少 3 node、過半 quorum 才能寫 leader key — 少數派 partition 無法 elect 新 leader
  2. Watchdog (Linux kernel):required mode 強制 — Patroni 必須定期 poke /dev/watchdog、若 Patroni 自己掛或被 OS 凍結、kernel 自動 reboot 整台機器、避免舊 leader 在 DCS 失聯後繼續接寫

Watchdog required 是 production-grade 的硬要求:required 模式下 watchdog 啟用失敗的 node 不會成為 leader;automatic 在有 watchdog device 時使用、沒有就略過,off 完全不用——後兩種都不保證失聯的 leader 一定會被 reset。

Promotion — pg_ctl + replication slot 切換

新 leader 寫 DCS key 成功後、Patroni 自動執行:

1# Patroni 內部、不要手動跑
2pg_ctl promote -D /var/lib/postgresql/data
3# 刪掉 standby.signal、結束 recovery、開始接受寫入
4# timeline 從 1 換成 2:pg_wal/ 多出 00000002.history,之後的 WAL segment 檔名以 00000002 開頭
5# postgresql.auto.conf 裡的 primary_conninfo 原樣留著,不在 standby 模式時不會被使用

Promotion 期間關鍵議題:

  • timeline divergence:新 leader promote 時開新的 timeline ID,分歧點是它 promote 當下已經 replay 到的 WAL 位置(記在 .history 檔裡)。WAL 沒有超過分歧點的 standby 直接跟著新 timeline 往下 replay(recovery_target_timeline 預設 latest);需要 pg_rewind 的是 WAL 寫過分歧點的 node,通常是舊 leader
  • replication slot 處理:舊 leader 上的 replication slot 在 DCS 中已 stale、新 leader 重建 slot;如果 logical replication consumer 沒 idempotent、會 replay 部分訊息
  • promotion latency:通常 3-10 秒(pg_ctl 本身 < 5s、加 DCS 寫確認)

Reconfiguration — client routing 切換

PostgreSQL 自己升 leader 還不夠、application 不知道;要靠前端 routing 層把連線轉到新 leader,常見的架構是 client 經過 HAProxy 或 pgBouncer 再到各個 node:

1[client] → [HAProxy / pgBouncer] → [pg-node-1 (leader)]
2                                 → [pg-node-2 (standby, read)]
3                                 → [pg-node-3 (standby, read)]

Patroni REST API 暴露 /leader / /replica / /health endpoint、HAProxy 用 health check 跑這些 endpoint:

1# haproxy.cfg
2backend pg-write
3  option httpchk OPTIONS /leader
4  http-check expect status 200
5  server pg-node-1 pg-node-1:5432 check port 8008
6  server pg-node-2 pg-node-2:5432 check port 8008 backup
7  server pg-node-3 pg-node-3:5432 check port 8008 backup

Reconfiguration 期間關鍵延遲:

  • HAProxy health check 間隔(預設 2s)+ failure threshold(預設 3 次)= ~6s 切換感應
  • PgBouncer 不主動 health check、要靠 application 端 retry 跟 connection drop 觸發重連
  • 整個 reconfiguration 端到端通常 10-20s(含 PostgreSQL promotion 時間)

Recovery — pg_rewind 跟 base backup 取捨

舊 leader 恢復後變 standby,但 WAL 已 divergence — 必須選一條 recovery path:

  • pg_rewind:rewind 舊 leader WAL 到分歧點、重新接 streaming replication;條件 = 分歧 WAL 量小(< 幾 GB)且 timeline 可對齊
  • 重 base backup:用 pg_basebackup 從新 leader 拉完整 base + WAL;條件 = 任何時候都可、但時間長(TB 級 1-4 小時)

use_pg_rewind: true 時 Patroni 先嘗試 pg_rewind(這個選項預設 false);rewind 失敗之後要不要清掉 data dir、改做 base backup,由 remove_data_directory_on_rewind_failure 決定,它也預設 false。production 配置:

1postgresql:
2  use_pg_rewind: true
3  remove_data_directory_on_rewind_failure: true   # rewind 失敗自動清 data dir、再 base backup
4  remove_data_directory_on_diverged_timelines: true

Production 故障演練

Split-brain due to DCS partition

徵兆:兩個 PostgreSQL node 都在接寫、application 大量寫入 conflict / unique constraint violation。

根因:DCS(etcd)發生 network partition 後,舊 leader 所在的 network 分區拿不到 etcd quorum、leader key 到期;而舊 leader 上的 Patroni 沒有及時把 PostgreSQL demote(Patroni process 卡住或被暫停、又沒有 watchdog)。拿到 quorum 的 standby 升成新 leader,兩個 PostgreSQL 在同一段時間都接受寫入。

修法:

  1. DCS 必須奇數 node(3 / 5 / 7)、過半 quorum 嚴格 enforce
  2. DCS 部署跨 AZ / region 時、quorum size 要考慮 partition 機率(3 AZ 各 1 node 是 production 最低標)
  3. Watchdog required mode 是最後一道閘門 — DCS partition 加 quorum 失靈時、watchdog 強制 reboot 失聯 node

Standby 落後太多、無法 failover

徵兆:primary 失聯後、Patroni log 顯示 Following members have lag greater than maximum_lag_on_failover、所有 standby 都被拒 promote、cluster unavailable。

根因:maximum_lag_on_failover 設 1MB、但 standby replication lag 累積到 50MB(write-heavy workload + slow disk on standby)。安全機制觸發、但代價是 無 standby 可升、需要人工降低門檻或等 standby catch up。

修法:

  1. 預防:standby 容量 / IO 對齊 primary、避免 lag 累積;prometheus alert pg_replication_lag_bytes > 10MB 觸發前 catch
  2. 臨時:手動 patronictl edit-config 把 maximum_lag_on_failover 暫時拉到 50MB、接受可能丟 50MB worth of writes、換 availability
  3. 長期:sync replication(一個 standby 強制同步)、保證至少一個 standby zero-lag

Promotion 後 application connection storm

徵兆:failover 完成後 30-120 秒內、application log 大量 connection refused / password authentication failed、application 自己 retry storm。

根因:新 leader 剛 promote、PostgreSQL max_connections 容量還在 warm up(shared memory / cache 未 prime)、application 同時湧入大量 connection request;應用 retry 不夠 jitter、queue 堆積。

修法:

  1. Application 用 exponential backoff with jitter、不要 immediate retry
  2. PgBouncer / connection pool 限制每 application instance 對 PG 的 connection 上限、不直連 PG
  3. 預先在 standby 跑 pg_prewarm 把熱表 cache 預熱、promotion 後 cache miss 不爆

pg_rewind 失敗、退到 base backup 沒做

徵兆:舊 leader 恢復後、Patroni log 顯示 pg_rewind failed、舊 leader 一直 STARTING、無法重接 cluster;SRE 手動跑 pg_basebackup 才恢復。

根因:remove_data_directory_on_rewind_failure: false(預設)— rewind 失敗時 Patroni 不主動清 data dir、需要 SRE 手動處理;運維沒 runbook、卡在這步幾小時。

修法:

  1. Production 設 remove_data_directory_on_rewind_failure: true + remove_data_directory_on_diverged_timelines: true、讓 Patroni 自動 fallback
  2. data dir 跑在獨立 PV / disk、清掉風險可控(不要跑 root disk)
  3. 容量規劃:base backup 時間預估納入 RTO(TB 級 base backup 1-4 小時、不是 RTO 30 分鐘所能承受)

Watchdog 觸發整機 reboot、誤殺

徵兆:production server 在無故障時 unexpected reboot、dmesg 顯示 watchdog: BUG: soft lockup。

根因:Patroni 主循環因 etcd 短暫慢回應卡住 60+ 秒、kernel watchdog 觸發 reboot;但實際 PostgreSQL 沒 hang、是 Patroni-watchdog 鏈過敏。

修法:

  1. 拉長 ttl 或調小 safety_margin:watchdog 的 timeout 是 ttl - safety_margin 秒,這個值越大、Patroni 主循環能卡住的時間越長;調大 safety_margin 反而讓 watchdog 更早觸發
  2. etcd 跟 Patroni 部署在低延遲 network 內(同 AZ < 5ms)、跨 region etcd 不建議
  3. watchdog device 用 softdog(軟體模擬)vs 硬體 watchdog、debug 時 softdog 容易觀察

容量規劃

維度估算警戒
Cluster size3-5 node(含 leader + 2-4 standby)< 3 不能 HA(單 standby 失敗整 cluster 掛)
DCS size3 / 5 / 7 node(奇數 quorum)etcd 5 node 是 prod standard
TTL30s(default 30、production 20-60)< 15s 過敏、> 60s 過鈍
maximum_lag_on_failover1MB(default)大表 write-heavy 可放 10-100MB
Synchronous standby1 個 sync + N 個 async 是 production 預設全 async 容易丟資料、全 sync write latency 爆
RTO10-45 秒(detection 最長到 TTL 30s + promotion 5-10s + reconfig 5s)> 60s 要 audit 鏈路
RPOsync mode 接近 0、async mode 跟 lag 同數量級async 在 disk IO 慢時 lag 可能 MB-GB level

整合 / 下一步

跟 PgBouncer 整合

PgBouncer 不主動感知 Patroni failover、要靠:

  1. HAProxy 在 PgBouncer 上層:HAProxy 跑 Patroni health check、PgBouncer connection 重新路由
  2. PgBouncer RECONNECT:failover 後由 automation 在 pgBouncer admin console 下 RECONNECT(pgBouncer 1.9 起),pgBouncer 把每條 server connection 在目前的使用者釋放之後關掉、之後按需要重新連線;新連線連到新 leader 的前提是 pgBouncer 設定裡的位址已經指向新 leader
  3. Connection pool drain:application 端 connection pool 設 pool_lifetime_max=5min、舊 connection 自然汰換

跟 cert-manager(TLS rotation)

Patroni REST API 跟 PostgreSQL streaming replication 都用 TLS、cert rotation 不能停服務:

  1. cert-manager 自動換證後、Patroni 跟 PostgreSQL 都需要 reload(不是 restart)
  2. patronictl reload <cluster> 不會觸發 failover、只 reload config
  3. PostgreSQL pg_ctl reload 是 SIGHUP、平滑載入新 cert

跟 backup / PITR

Patroni 不管 backup — 但 standby promotion 後、WAL archive 必須跟新 leader 的 timeline 對齊:

  1. WAL segment 檔名開頭的 8 個 hex 字元就是 timeline ID(promote 之後的新 segment 從 00000002 開始),archive_command = 'wal-g wal-push %p' 推上去的檔名自帶 timeline,新 leader 的 WAL 不會跟舊 timeline 的同號 segment 撞名
  2. Backup tool(pgBackRest / WAL-G)支援 timeline 切換、archive 不會中斷
  3. 詳見 PostgreSQL PITR + WAL archiving

下一步議題

  • Multi-region Patroni:跨 region 部署的 DCS quorum 設計、跟單 region 的取捨完全不同
  • PostgreSQL 16+ streaming replication slot 持久化:簡化 standby promotion 後 logical consumer 重連
  • 跟 Kubernetes operator 整合:Patroni 跑在 K8s 時、StatefulSet + pod identity + DCS 部署模式

相關連結