跳至内容

22.4 路由与故障切换

高可用控制面决定“谁应当是主库”,服务接入层决定“客户端新连接实际去了 哪里”。二者相关,却不是同一个状态机:

Patroni/DCS       membership, leader, promotion/demotion
HAProxy           health sample, eligible backend, TCP lifecycle
PgBouncer         client/server pool, authentication, session/protocol state
client pool       cached address, existing socket, retry/backoff
PostgreSQL        transaction, commit, recovery/read-only state

故障切换必须让这五层收敛。只看到 Patroni leader 变化,不能宣告应用恢复。

22.4.1 HAProxy 健康检查与角色判断

角色健康接口

Patroni REST API 可以按当前角色返回健康状态。Pigsty 默认 HAProxy service 使用 HTTP health check:

/primary    只选择当前 primary
/replica    选择 replica
/read-only  接受可提供只读的成员

本章渲染配置的核心形态:

listen pg-test-primary
    bind *:5433
    mode tcp
    option httpchk
    http-check send meth OPTIONS uri /primary
    http-check expect status 200
    server pg-test-1 10.10.10.11:6432 check port 8008
    server pg-test-2 10.10.10.12:6432 check port 8008
    server pg-test-3 10.10.10.13:6432 check port 8008

数据流是 TCP 到 6432,健康检查却到 Patroni 8008。不要把 destination port 与 check port 混为一谈。

“UP”表示什么

对这种配置,HAProxy backend UP 表示:

最近若干次 Patroni HTTP 检查满足期望状态

它不直接证明:

  • PostgreSQL 对业务 role 的认证成功;
  • PgBouncer userlist/auth query 正确;
  • pool 有可用 server slot;
  • session 角色属性满足客户端;
  • 数据足够新;
  • SQL 可以提交;
  • TLS hostname/CA 正确。

因此需要 SQL 端到端 probe。

检查节奏和抖动

本章实际 default-server:

inter=2s
fastinter=1s
downinter=2s
rise=3
fall=3
timeout check=3s
slowstart=30s
maxconn=3000
maxqueue=128
on-marked-down shutdown-sessions

粗略地,状态发现时间受:

[ T_{\text{detect}} \approx \text{interval} \times \text{rise/fall}

  • \text{request/timeout jitter} ]

但切换总时间还包括:

Patroni decision/promotion
HAProxy next health samples
old connection close
PgBouncer backend refresh
client reconnect/backoff
application readiness

不能从 fall=3, inter=2s 单独推导应用 RTO。

shutdown-sessions

当 backend 被标记 down,HAProxy 可以关闭其活动 session。这会让客户端尽快 离开旧主库,避免长连接继续停留在错误角色。

代价是:

  • in-flight transaction 中断;
  • client 收到连接错误;
  • commit 结果可能未知;
  • reconnect 同时发生;
  • cancel/cleanup 不一定完成。

它是故障收敛手段,不是透明迁移。

backup 不是注释

本章 replica service:

pg-test-2, pg-test-3 normal
pg-test-1 primary backup

offline service:

pg-test-3 offline normal
pg-test-2 ordinary replica backup

当 normal backend 不可用时,backup 改变 workload 去向。因此降级时可能:

  • 只读流量回到 primary;
  • 分析流量回到普通 replica;
  • 原有容量隔离消失。

告警不能只说“service still up”,还要说“service is on backup backend”。

健康检查本身也要保护

检查太宽松:

TCP 端口开 -> 误把错误角色当健康

检查太严格:

非关键依赖抖动 -> 反复摘除健康数据库

角色服务应检查决定路由所需的最小语义。业务 readiness 可以另有端点,避免 把每个业务依赖都塞进数据库角色检查。

从 HAProxy stats 取证

可以通过受控 stats socket/API 观察:

pxname / svname
status
check_status / check_code
lastchg
queue/current sessions
selected backup

不要把 stats credential 或完整配置中的密码写进证据。正式实验只投影服务、 地址、端口、健康路径、backup flag 和安全参数。

22.4.2 旧连接、重连风暴与客户端退避

新连接路由不迁移旧连接

HAProxy 更新 backend 选择后:

new connections -> new eligible backend
existing TCP     -> old backend until closed

旧主库 demote/restart、HAProxy shutdown session 或 PostgreSQL close 才会让 旧连接离开。应用 pool 可能继续把坏 socket 发给请求,直到 validation 或 query 暴露错误。

因此 client pool 需要:

  • borrow 前/失败后的 connection validation;
  • 最大 connection lifetime;
  • idle timeout;
  • broken connection eviction;
  • address/DNS refresh;
  • pool warmup 限速;
  • 切换后的 error budget。

reconnect storm

假设 200 个 app instance,每个 pool 20:

role change
  -> 4000 sockets fail
      -> every worker immediately reconnects
          -> proxy accept/auth/server-login storm

即使数据库已恢复,风暴也可能把它再次压垮。

客户端退避:

[ d_n = \min(d_{\max}, d_0 2^n) \times J ]

其中 (J) 是随机 jitter,例如 [0.75, 1.25]

还要:

  • overall request deadline;
  • 最大尝试次数;
  • 每 host connect timeout;
  • circuit breaker;
  • global concurrency limiter;
  • retry budget;
  • readiness 与 background reconnect 分离。

什么可以重试

读:

  • 幂等 SELECT 通常可在新 transaction 重试;
  • 仍要考虑 snapshot、timeout 和 side-effect function。

写:

连接前失败
  较可能没有发送,但仍按 driver stage 判断

执行中/commit 时断开
  结果未知:可能提交,也可能没有

不能把所有 connection error 当作“未提交”,否则重试可能重复扣款、下单或 发券。

安全模式:

INSERT INTO request_log(idempotency_key, ...)
VALUES ($1, ...)
ON CONFLICT (idempotency_key)
DO UPDATE ...
RETURNING ...;

断连后用同一个 token 查询。重试同一个业务动作时,唯一性与状态机必须使其 幂等;不要生成新 token 假装是新动作。

本章 client probe

六个 worker:

short connection per attempt
target_session_attrs=read-write
unique token per logical attempt
0.15 s normal interval
exponential backoff + jitter on failure
24 s total

每个 event 记录:

worker/attempt/token
monotonic start/end
acknowledged or unknown
error class + SQLSTATE, no credential/message
backend/postmaster identity for acknowledged

失败 token 不盲目重发。最终统一查询数据库:

acknowledged_missing
unknown_committed
unknown_absent
duplicate_tokens
unreconciled_unknown

正式结果:

events                  387
acknowledged            339
unknown                  48
unknown committed         0
unknown absent           48
acknowledged missing      0
duplicates                0
unreconciled              0

“48 个 unknown 都 absent”是事后 lookup 结果,不应在异常发生瞬间假设。

probe 不是生产 driver

它使用 Psycopg、短连接和 synthetic INSERT。生产应用还要按实际:

  • driver;
  • pool;
  • ORM;
  • transaction wrapper;
  • retry middleware;
  • service mesh;
  • load balancer;
  • request deadline;

运行矩阵。否则 middleware 可能在你不知道的地方二次重试。

22.4.3 故障切换中的 DNS、连接池和事务失败

一次切换的状态序列

t0   old primary serves writes
t1   switchover/failure decision
t2   old connections interrupted or rejected
t3   candidate promotes
t4   Patroni topology converges
t5   HAProxy health converges
t6   PgBouncer server pool reflects new role
t7   client reconnects after backoff
t8   first acknowledged business write

应用恢复点是 t8,不是 t3

DNS 不处理 transaction

即使 DNS 立即指向新入口:

  • 已解析地址仍在 client cache;
  • 已建立 socket 不重新解析;
  • pool 可能只在耗尽时建新连接;
  • in-flight transaction 已经失败;
  • old primary commit outcome 仍未知。

DNS 是 discovery 层,不是 transaction continuity。

PgBouncer role-state refresh

本章发现:

Patroni              replica/running
PostgreSQL direct    recovery=true, transaction_read_only=true
HAProxy health       backend UP
PgBouncer path       target_session_attrs rejects

对具体 database 执行:

RECONNECT test;

后,新的 server connection 重新发现当前角色,连续属性检查恢复。

正式切换因此在每次 topology 稳定后:

  1. 对三台 PgBouncer 发 RECONNECT test
  2. 等待每条旧 server connection 在安全边界关闭/重建;
  3. 要求 client probe 出现一次新的 acknowledged write;
  4. 把这段时间计入 conservative write gap。

不要把它泛化成“任何切换都必须手工 RECONNECT”。正确结论是:

角色切换后必须端到端验证 pooled path;若 pool 保留错误角色/协议状态, 应有受控、可观测的 refresh 机制。

自动 callback、PgBouncer restart、database reconnect 或 connection lifetime 各有不同 blast radius,需按平台设计。

pool refresh 的风险

RECONNECT database

  • 让对应 database 的 server connection 在释放后重新连接;
  • 不应误操作所有 database;
  • 会增加短时 server login/auth;
  • 可能让等待者暂时增加;
  • 要与 client backoff 协同;
  • 多 PgBouncer 实例必须全覆盖。

正式实验只操作 sandbox test,并记录三成员 action 与刷新后的首笔确认。

正向和回切证据

initial   pg-test-1, timeline 9
forward   pg-test-2, timeline 10
restored  pg-test-1, timeline 11

forward patronictl command       2.774 s
restore patronictl command       2.766 s
forward conservative write gap   6.995 s
restore conservative write gap   8.510 s

为何 command time 小于 write gap:

command return
  != all replicas streaming
  != HAProxy health converged
  != pool refreshed
  != client backoff ended
  != first write committed

8.510 秒是一个 sandbox observation,不是 RTO。

planned switch 不能证明 unplanned failover

本章没有注入:

  • primary process crash;
  • host power loss;
  • network partition;
  • DCS loss;
  • storage stall;
  • split brain;
  • watchdog/fencing failure;
  • proxy entry failure。

planned switch 知道 leader/candidate,成员健康且可协调。unplanned failure 的 检测、仲裁、RPO 和 fencing 风险完全不同。

故障时的 transaction 分类

client 观察可安全推出不能推出
connect refused此次连接未建立前一请求未提交
read-only errorsession 角色不满足集群没有主库
serialization/deadlock当前事务回滚可无界立即重试
connection lost during query结果未知一定未执行
connection lost during COMMIT结果未知一定提交/未提交
unique token already exists同 token 有结果业务 payload 必然一致

token 表还要验证 payload hash/状态,防止同 key 被不同请求误用。

切换验收不变量

topology:
  exactly one primary
  expected member set
  replicas streaming
  timeline advances

service:
  write endpoint read-write
  read endpoint read-only/allowed member
  direct endpoint follows primary
  pool state refreshed/observable

client:
  acknowledged rows present
  unknown reconciled
  duplicates zero
  retry/backoff bounded

restore:
  final intended leader
  pool configuration exact
  postflight baseline passes

任何一层失败都不该被“Patroni 已正常”覆盖。

旧主库恢复后的流量

旧主库变成 replica 后:

  • primary health 应摘除它;
  • replica/offline 策略可能纳入它;
  • 原 server connection/session 必须重新评估角色;
  • replay lag 要回到门槛;
  • connection storm 不应阻塞 rewind/rejoin;
  • 监控要区分新的 timeline。

回切会再经历一次完整过程,不是把 timeline 倒回去。本章 9 -> 10 -> 11 证明每次 promotion 都生成新 timeline。

本节检查表

[ ] HAProxy destination 和 Patroni check port 分开理解
[ ] backend UP 不被当成业务 SQL 可用
[ ] rise/fall/interval 不被直接冒充应用 RTO
[ ] backup backend 激活有告警
[ ] old TCP connection 行为已定义
[ ] reconnect 有 jitter、上限、deadline 和 retry budget
[ ] 写入结果未知用 token reconcile
[ ] 角色变化后验证 pooled session 属性
[ ] pool refresh 覆盖所有实例且有首笔恢复证据
[ ] planned 与 unplanned 结论严格分开
[ ] 回切被当成第二次 promotion/timeline

参考资料


上一节:PgBouncer 池化模式 · 返回本章目录 · 下一节:连接预算与过载边界 · 查看全书目录 · 查看索引中心

最后更新于