22.4 路由与故障切换
高可用控制面决定“谁应当是主库”,服务接入层决定“客户端新连接实际去了 哪里”。二者相关,却不是同一个状态机:
故障切换必须让这五层收敛。只看到 Patroni leader 变化,不能宣告应用恢复。
22.4.1 HAProxy 健康检查与角色判断
角色健康接口
Patroni REST API 可以按当前角色返回健康状态。Pigsty 默认 HAProxy service 使用 HTTP health check:
本章渲染配置的核心形态:
数据流是 TCP 到 6432,健康检查却到 Patroni 8008。不要把 destination
port 与 check port 混为一谈。
“UP”表示什么
对这种配置,HAProxy backend UP 表示:
它不直接证明:
- PostgreSQL 对业务 role 的认证成功;
- PgBouncer userlist/auth query 正确;
- pool 有可用 server slot;
- session 角色属性满足客户端;
- 数据足够新;
- SQL 可以提交;
- TLS hostname/CA 正确。
因此需要 SQL 端到端 probe。
检查节奏和抖动
本章实际 default-server:
粗略地,状态发现时间受:
[ T_{\text{detect}} \approx \text{interval} \times \text{rise/fall}
- \text{request/timeout jitter} ]
但切换总时间还包括:
不能从 fall=3, inter=2s 单独推导应用 RTO。
shutdown-sessions
当 backend 被标记 down,HAProxy 可以关闭其活动 session。这会让客户端尽快 离开旧主库,避免长连接继续停留在错误角色。
代价是:
- in-flight transaction 中断;
- client 收到连接错误;
- commit 结果可能未知;
- reconnect 同时发生;
- cancel/cleanup 不一定完成。
它是故障收敛手段,不是透明迁移。
backup 不是注释
本章 replica service:
offline service:
当 normal backend 不可用时,backup 改变 workload 去向。因此降级时可能:
- 只读流量回到 primary;
- 分析流量回到普通 replica;
- 原有容量隔离消失。
告警不能只说“服务仍然可用”,还要指出“服务正在使用备用后端”。
健康检查本身也要保护
检查太宽松:
检查太严格:
角色服务应检查决定路由所需的最小语义。业务 readiness 可以另有端点,避免 把每个业务依赖都塞进数据库角色检查。
从 HAProxy stats 取证
可以通过受控 stats socket/API 观察:
不要把 stats credential 或完整配置中的密码写进证据。正式实验只投影服务、 地址、端口、健康路径、backup flag 和安全参数。
22.4.2 旧连接、重连风暴与客户端退避
新连接路由不迁移旧连接
HAProxy 更新 backend 选择后:
旧主库 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:
即使数据库已恢复,风暴也可能把它再次压垮。
客户端退避:
其中 是随机 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。
写:
不能把所有 connection error 当作“未提交”,否则重试可能重复扣款、下单或 发券。
安全模式:
断连后用同一个 token 查询。重试同一个业务动作时,唯一性与状态机必须使其 幂等;不要生成新 token 假装是新动作。
本章 client probe
六个 worker:
每个 event 记录:
失败 token 不盲目重发。最终统一查询数据库:
正式结果:
“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、连接池和事务失败
一次切换的状态序列
应用恢复点是 t8,不是 t3。
DNS 不处理 transaction
即使 DNS 立即指向新入口:
- 已解析地址仍在 client cache;
- 已建立 socket 不重新解析;
- pool 可能只在耗尽时建新连接;
- in-flight transaction 已经失败;
- old primary commit outcome 仍未知。
DNS 是 discovery 层,不是 transaction continuity。
PgBouncer role-state refresh
本章发现:
对具体 database 执行:
后,新的 server connection 重新发现当前角色,连续属性检查恢复。
正式切换因此在每次 topology 稳定后:
- 对三台 PgBouncer 发
RECONNECT test; - 等待每条旧 server connection 在安全边界关闭/重建;
- 要求 client probe 出现一次新的 acknowledged write;
- 把这段时间计入 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 与刷新后的首笔确认。
正向和回切证据
为何 command time 小于 write gap:
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 error | session 角色不满足 | 集群没有主库 |
| serialization/deadlock | 当前事务回滚 | 可无界立即重试 |
| connection lost during query | 结果未知 | 一定未执行 |
| connection lost during COMMIT | 结果未知 | 一定提交/未提交 |
| unique token already exists | 同 token 有结果 | 业务 payload 必然一致 |
token 表还要验证 payload hash/状态,防止同 key 被不同请求误用。
切换验收不变量
任何一层失败都不该被“Patroni 已正常”覆盖。
旧主库恢复后的流量
旧主库变成 replica 后:
- primary health 应摘除它;
- replica/offline 策略可能纳入它;
- 原 server connection/session 必须重新评估角色;
- replay lag 要回到门槛;
- connection storm 不应阻塞 rewind/rejoin;
- 监控要区分新的 timeline。
回切会再经历一次完整过程,不是把 timeline 倒回去。本章 9 -> 10 -> 11
证明每次 promotion 都生成新 timeline。
本节检查表
参考资料
- HAProxy:Health checks
- Patroni:REST API
- PgBouncer:Administration console
- PostgreSQL 18:libpq connection parameters
上一节:PgBouncer 池化模式 · 返回本章目录 · 下一节:连接预算与过载边界 · 查看全书目录 · 查看索引中心