四通八达:服务接入、连接池与路由
22 四通八达:服务接入、连接池与路由
应用需要的不是“连接 10.10.10.11”,而是:
机器地址只回答“TCP 包送到哪里”,服务端点还必须回答:
只要其中一项没有写清,端口号就是一个未经定义的偶然实现。
本章把 PostgreSQL backend、PgBouncer、HAProxy、Patroni 和客户端驱动放进
同一条证据链。学习顺序不是先背 5433/5434/5436/5438,而是先定义服务
语义,再计算连接预算,选择池化模式,最后验证 Pigsty 的具体映射与切换行为。
本章目标
完成本章后,你应当能够:
- 把连接 URI 当作版本化服务合同,而不是主机别名;
- 区分主写、只读、同步读取、离线读取和直连管理端点;
- 解释异步副本为什么不能天然提供 read-your-writes;
- 用 LSN、sticky-primary 或业务一致性 token 设计读取策略;
- 说明一个 PostgreSQL backend 的进程、内存、锁、事务和会话成本;
- 拒绝用调大
max_connections代替容量规划; - 把应用进程数、应用池、PgBouncer pool 与数据库保留槽放进同一预算;
- 正确比较 session、transaction 与 statement pooling;
- 判断临时表、会话 GUC、
LISTEN、咨询锁和安全上下文是否兼容事务池; - 区分协议级 prepared statement 与 SQL
PREPARE; - 从
SHOW POOLS证明排队、复用与服务端连接上限; - 阅读 HAProxy 的 Patroni 角色健康检查、backup 选择和连接关闭策略;
- 为切换中的断连、退避、抖动和提交结果未知编写客户端合同;
- 识别角色已经正确、但池化后端仍保留旧角色状态的情况;
- 在 Pigsty 中声明、渲染、校验和重载服务,而不是手改产物;
- 完成一次带端点、池化、异步可见性和双向计划切换的可重放演练。
前置与后续
前置:
- 第 18 章 PostgreSQL 数据平台与替代边界 已定义服务、SLO 和 责任边界;
- 第 19 章 部署基线 已固定 exact Pigsty v4.5.0 四机沙箱;
- 第 20 章 高可用 已解释 Patroni、timeline、 计划切换与提交结果未知;
- 第 21 章 备份体系与恢复演练 已证明 HA、backup 和 service recovery 不能互相替名;
- 读者已掌握 Linux、SQL、事务、锁和基本网络连接概念。
后续:
- 第 23 章把身份、HBA、TLS、RLS、审计与池化上下文纳入安全模型;
- 第 24 章把端点合同、切换 SOP 和例外变成组织治理;
- 第 25、26、27 章分别补齐指标、容量压测与参数治理;
- 第 31、34 章会复用本章的 queue、timeout、reserve 与 reconnect 控制点 处理事故和过载。
学习路径
这条路径故意把“端口”放在后面。5433 不是 PostgreSQL 标准语义;只有
当声明、渲染配置、健康检查、池状态和 SQL 观察相互吻合时,它才是当前
环境里的主写服务。
五层连接证明
| 层 | 要回答的问题 | 本章证据 |
|---|---|---|
| 声明 | 服务本来应当选择什么 | Pigsty inventory/defaults、ADR |
| 渲染 | 实际代理与池配置是什么 | HAProxy service file、SHOW CONFIG |
| 运行 | 当前哪些 backend 合格、池是否排队 | Patroni、HAProxy health、SHOW POOLS |
| SQL | 客户端最终落到什么角色与会话 | recovery/read-only、PID、GUC、token |
| 故障 | 角色变化时确认/未知结果如何收敛 | client event、timeline、token reconcile |
任意一层单独通过都不够:
Pigsty 默认服务语义
本章 exact v4.5.0 沙箱保留四个服务:
| 名称 | 入口 | 目标 | Patroni 检查 | 本章用途 |
|---|---|---|---|---|
| primary | 5433 | 当前主库 PgBouncer :6432 | /primary |
短 OLTP 读写 |
| replica | 5434 | 副本优先 PgBouncer :6432 | /read-only |
可容忍陈旧的只读 |
| default | 5436 | 当前主库 PostgreSQL :5432 | /primary |
管理、迁移、会话敏感工具 |
| offline | 5438 | 指定离线副本 PostgreSQL :5432 | /replica |
受控 OLAP/ETL |
replica 的 primary 是 backup,offline 的普通 replica 也是 backup。
因此名称表达“选择偏好与降级策略”,不是永恒承诺。客户端仍要用
target_session_attrs、只读事务和业务策略防止错误落点。
这些都是可修改的 Pigsty 默认值,不是 PostgreSQL 标准端口。实际系统必须 读取自己的声明与渲染产物。
正式实验
池化实验临时把入口节点的默认 server pool 从:
改为:
所有值在角色切换前精确恢复。正式观测:
最重要的观测不是某个毫秒数字,而是一个跨层失配:
pg-test-2的 PostgreSQL 已经是只读副本,Patroni 和 HAProxy 健康检查 也正确,但 PgBouncer 的既有 pool 仍可能保留上一次角色周期的状态。RECONNECT test让服务端连接重新发现当前角色,端点验证才重新通过。
正式切换因此把三节点 RECONNECT test 计入恢复路径,并要求刷新后出现新的
确认写入。它不是隐藏的实验准备,更不是生产 SLO。
结论边界
十四项例外
沿用第 19 章六项与第 20 章四项,本章新增:
本章目录
22.1 服务端点的语义
22.2 连接的服务端成本
22.3 PgBouncer 池化模式
- 22.3.1 session、transaction、statement pooling
- 22.3.2 临时表、会话 GUC、监听与咨询锁
- 22.3.3 预备语句支持必须绑定 PgBouncer 与驱动版本
- 22.3.4 池等待、服务时间与背压
22.4 路由与故障切换
22.5 连接预算与过载边界
22.6 Pigsty 服务接入层
22.7 实战:写入、只读与管理三类接入
实验入口
lab-contract.md:风险、动作、停机线与解释边界;requirements.json:机器验收合同;endpoint-contract.json:四类端点语义;routing-adr.md:路径、池化与切换决策;topology.mmd:实验拓扑;task.sh:唯一安全入口;connection-run.json:正式参考结果;negative-cases.json:十五个反例。
task.sh all 只重验既有证据,不连接 fixture、不改配置、不切换角色,也不
删除对象。drill:service 和 reset:fixture 是两条独立、精确守卫的路径。
参考资料
- PostgreSQL 18:连接与认证参数
- PostgreSQL 18:libpq 连接参数与 target_session_attrs
- PgBouncer:Configuration
- PgBouncer:Feature map
- PgBouncer:Administration console
- Pigsty:PostgreSQL Service
- HAProxy:Health checks
上一章:未雨绸缪:备份体系与恢复演练 · 返回下卷导读 · 下一章:固若金汤:认证、授权与数据安全 · 查看全书目录 · 查看索引中心
22.1 服务端点的语义
数据库连接串经常被当成部署细节:
但每个字段都在选择行为:
所以 URI 是应用与数据平台之间的 API。修改它可能改变一致性、会话、性能、 安全与故障行为,不能当作无语义的运维替换。
22.1.1 主写、只读、同步只读与直连管理端点
从业务动作定义端点
先列动作,再列端口:
| 动作 | 必要语义 | 不必要或有害的假设 |
|---|---|---|
| 创建订单 | 当前可写主库、提交结果可判定 | 固定机器 |
| 查看刚下的订单 | read-your-writes | 任意异步副本 |
| 浏览商品目录 | 可接受少量陈旧、只读 | 必须占用主库 |
| 生成日报 | 大查询、资源隔离、允许陈旧 | 普通 OLTP 副本 |
| 执行迁移 | 跟随主库、稳定 backend、完整会话能力 | 事务池 |
| 调查故障 | 保留管理槽、短超时、可审计 | 与应用共享无界池 |
由此可以得到几类服务,而不是几个机器别名。
主写服务
主写服务至少承诺:
它不承诺:
客户端可用 libpq 的:
作为最后一道角色检查。它不能选主,也不能修复代理;它只是在连接建立后拒绝 不满足属性的 session。
对写端点还要定义:
- 事务被断开后是否自动重试;
- 哪些动作有 idempotency key;
- 提交结果未知时如何查询;
- 是否允许 session pooling;
- 应用侧 pool acquire/connect/statement timeout;
- 最大并发和排队位置。
只读服务
只读端点有两个不同维度:
transaction_read_only=on 只证明第一项,不证明第二项。一个停止 replay
数小时的副本仍然只读。
客户端可以组合:
以及事务级护栏:
前者防错误落点,后者把意图交给 PostgreSQL。二者都不能自动提供 bounded staleness。
只读服务还要明确降级:
Pigsty 默认 replica service 把 primary 作为 backup。因此它表达 “副本优先”,而不是“永远只去副本”。如果业务必须隔离主库,声明与客户端 都要 fail closed,不能只相信服务名称。
同步只读不是“副本端口”的同义词
同步复制控制提交确认条件。例如:
这仍不自动表示:
真正的“同步只读服务”至少要同时约束:
- 哪些 standby 进入同步集合;
- commit 等待到 write、flush 还是 apply;
- 端点只选择哪个集合;
- 同步状态丢失时 fail closed 还是降级;
- 客户端如何携带上次写入边界。
若 synchronous_commit=remote_apply 且读取端点只选择参与确认的已 apply
副本,可以收紧窗口,但切换、负载均衡和事务起点仍需单独证明。
离线/分析服务
“replica”表示复制角色,“offline”通常表达调度意图:
Pigsty 的 offline service 使用 /replica 检查,并通过 inventory selector
优先选 pg_offline_query 成员。它仍会 replay WAL,长查询、临时文件与 I/O
可能拖慢 replay;“离线”不表示与主集群完全隔离。
应额外写:
- 最大可接受 replay lag;
- 查询并发、statement timeout、temp file 限额;
- 是否允许 hot standby conflict 取消查询;
- 普通副本能否作为 backup;
- replay 明显落后时是否摘除。
直连管理端点
直连管理并不一定是固定主机。更实用的是:
它跟随当前主库,但绕过事务池,适合:
- schema migration;
- 需要 session advisory lock 的部署工具;
LISTEN/NOTIFYconsumer;- 大型
COPY或特殊驱动工作流; - 需要稳定 backend 的诊断;
- CDC/逻辑复制管理,在完成额外评审后。
绕过池不等于无限连接。管理端点通常应有更小、更受控的来源网段、身份、连接 预算与审计。
Pigsty 默认 default service 是这种路径:HAProxy 仍用 /primary
选择主库,但目标是 PostgreSQL 5432 而不是 PgBouncer 6432。
写一份端点合同
一个最小合同可以写成:
端口只是这个合同的实现字段之一。
22.1.2 复制延迟、一致性与 read-your-writes
“刚写完却读不到”为什么完全正常
异步流复制的路径是:
写请求收到成功,只表示其提交满足当前 synchronous_commit 规则。若规则不
等待目标副本 apply,随后的 replica query 可能先到。
形式化地,令:
要让该副本具备读取边界,至少需要:
这仍不代表业务一定读到目标行:查询条件、事务 snapshot、权限、分区、 soft delete 和应用 cache 都可能改变结果。
四种常用策略
策略一:写后粘主
优点是简单;缺点是时间窗口不是因果证明,可能过长浪费主库,也可能过短。
适合:
- 用户刚提交后查看详情;
- 一次 HTTP request 内写后读;
- 没有 LSN token 基础设施的普通应用。
策略二:携带一致性 token
写事务完成后,从主库取得一个 WAL 边界:
客户端把它作为 opaque token 传给后续读取。读取端点在副本上观察:
只有 replay 越过 token 才查询,否则:
注意:
- LSN 必须来自提交之后;事务内提前读取可能落在 commit record 之前;
- timeline 改变后,不能只做字符串大小比较;
- token 协议要绑定 cluster identity;
- 等待必须受 request deadline 限制;
- connection pool 可能在两次语句间换 backend;
- 读取事务 snapshot 必须在 replay 达标之后建立。
事务池场景最好把“等待边界 + 业务查询”放在同一只读事务或服务端函数里, 避免检查和查询落到不同副本。
策略三:同步 apply 与限定路由
可以让提交等待同步副本 replay,再让读取只去被确认的集合。它增加写延迟, 并把副本可用性纳入提交路径。
必须定义降级时:
没有明确降级合同的“同步”会在故障时悄悄变成另一个语义。
策略四:业务版本/事件边界
有些系统不用裸 LSN,而用:
副本读到至少该业务版本才返回。这更贴近业务,但底层仍需可靠映射和超时。
延迟指标的几个阶段
副本延迟不是一个数字:
| 阶段 | PostgreSQL 观察 | 能推出什么 |
|---|---|---|
| sent | primary sent_lsn |
WAL 已发到何处 |
| write | primary write_lsn |
standby OS 收到/写入 |
| flush | primary flush_lsn |
standby 持久化 |
| replay | primary replay_lsn / standby replay LSN |
查询可见边界接近何处 |
| query | 业务 token 查询 | 目标事实是否可见 |
replay_lag=0 是某次采样,不是未来保证。低流量时 timestamp lag 也可能显得
陈旧,LSN gap 与 wall-clock delay 要结合读。
本章的一个样本
正式实验:
它只证明:
在这个时刻、这个 token、这个小型异步沙箱和当前负载下,副本在约 11.092 ms 后返回了该行。
它不能证明:
因此 evidence 把 EX22-ASYNC-READ-OBSERVATION 作为必需例外。
读端点失败也是语义
本章准备实验时曾观察到:
根因位于 PgBouncer server pool 的角色状态,而不是 PostgreSQL 复制本身。 执行受控的:
让空闲/完成的服务端连接重新建立后,端点属性恢复。
这说明一致性证据必须从客户端走完整路径。只在副本上执行
SELECT pg_is_in_recovery(),不能证明应用的 5434 连接会得到同样 session。
22.1.3 DNS、VIP、代理与客户端发现
四种入口解决不同问题
固定节点地址
- 优点:简单、可诊断。
- 缺点:节点故障就是入口故障;即使 HAProxy 能路由数据库角色,客户端也到 不了它。
本章 formal run 只验证这种单入口,因此明确保留例外。
DNS
DNS 可以:
- 把名称指向 VIP/代理;
- 返回多个 A/AAAA 记录;
- 在切换时修改地址;
- 为不同服务提供稳定名称。
但 DNS 不迁移既有 TCP 连接。还要考虑:
“TTL 5 秒”不等于 5 秒内所有应用都换地址。
VIP
VIP 把一个网络地址移动到健康入口。它解决入口地址连续性,不决定数据库主库。
需要证明:
- 谁持有 VIP,使用什么仲裁;
- split brain 时是否可能双持;
- gratuitous ARP/NDP 与交换网络收敛;
- 跨网段/跨 AZ 是否可达;
- VIP manager 与 Patroni 状态如何组合;
- 入口节点的 HAProxy/PgBouncer 是否健康。
数据库主库、VIP owner 和 HAProxy backend 是三个状态机,不能假设天然一致。
客户端多 host
libpq 连接串可以列多个 host:
客户端按规则尝试,target_session_attrs 拒绝错误角色。这减少单一入口依赖,
但每个应用/驱动的:
- host 顺序;
- DNS 展开;
- 并行或串行尝试;
- overall deadline;
- pool 的 address refresh;
- TLS hostname verification;
都要实测。
专用代理/服务发现
外部 HAProxy、云负载均衡、Kubernetes Service 或服务网格可以提供入口。 仍要验证:
一个完整发现链
每一步都有独立的 stale state:
切换测试必须穿过整个链,而不是只验证最末端的数据库角色。
target_session_attrs 的位置
它是客户端接受条件:
具体取值以当前 libpq 官方文档为准。它不能:
- 限制副本延迟;
- 保证连接经过或绕过 PgBouncer;
- 替代 TLS hostname 检查;
- 让失败事务自动迁移;
- 识别业务上的“正确集群”。
因此连接串还需要正确的 host、database、user、证书和 cluster identity 治理。
入口验收矩阵
| 场景 | 要执行的动作 | 通过条件 |
|---|---|---|
| 正常主库 | 从每个入口连接写服务 | 都选同一可写 leader |
| 正常副本 | 连接只读服务 | 只读,成员在允许集合 |
| 一个入口故障 | 停止/隔离入口 | 客户端转向另一入口 |
| DNS 变化 | 修改记录并保留旧连接 | 新连接收敛,旧连接行为已定义 |
| 数据库切换 | planned role change | 入口不变,新连接到新主库 |
| proxy reload | 配置校验后 reload | 现有/新连接行为符合 draining 合同 |
| stale pool | 角色变化后保留 server pool | 能检测并安全 refresh |
| TLS rotation | 轮换 CA/cert | 新旧窗口与 hostname 验证通过 |
本章只执行正常四端点和数据库 planned switch。入口节点故障、DNS/VIP 和 TLS 留给生产准入矩阵,不能由一次成功连接替代。
本节检查表
参考资料
- PostgreSQL 18:libpq connection parameters
- PostgreSQL 18:hot standby
- PostgreSQL 18:warm standby settings
- Pigsty:PostgreSQL Service
返回本章目录 · 下一节:连接的服务端成本 · 查看全书目录 · 查看索引中心
22.2 连接的服务端成本
PostgreSQL 不是把所有客户端请求放进一个无状态线程池。经典架构中,每个已 认证客户端连接对应一个 backend process:
因此“连接数”同时占用身份、进程、内存、文件描述符、共享结构与调度能力。 连接池的目标不是让数据库接受无限请求,而是把大量客户端等待放到比 backend 更便宜、更可控的位置。
22.2.1 后端进程、内存、事务与会话状态
一个连接得到什么
backend 建立后持有:
- OS process、PID、栈和私有地址空间;
- 与 shared memory 的映射;
- database、role、application name、client address;
- session GUC 和 prepared statement;
- 临时 schema、临时表与 cursor;
LISTENregistration;- session-level advisory lock;
- relation/catalog cache;
- 当前事务、snapshot、lock 与 resource owner;
- socket buffer、日志上下文和统计状态。
可以观察:
state='idle' 只表示当前没有执行 query,不表示连接免费。它仍占用 backend
slot 和 process,并保留 session state。
connection、session、transaction、statement
四个边界不能混用:
直连 PostgreSQL 时:
事务池时:
应用仍觉得自己有一个长连接,但 server session 已不是它的私有状态容器。
私有内存与共享内存
连接成本不能简化成固定的“每连接 10 MB”。大致有:
work_mem 不是每个连接一次,而可能是每个执行节点、并行 worker 各自一次:
若 200 个 backend 同时执行多 hash/sort 查询,work_mem=64MB 并不意味着
最多使用 ;实际可能更高。
本章沙箱观察:
这些是配置事实,不是“500 个并发重查询安全”的证明。
共享结构也随连接上限变化
某些 shared memory 与数组在启动时按:
等参数估算。提高 max_connections 可能要求重启,并增加:
- backend/proc array;
- lock table 的潜在规模;
- per-backend shared bookkeeping;
- snapshot、predicate lock 等间接压力。
更隐蔽的是 CPU scheduler:大量 runnable backend 会争抢 CPU 和 cache, 吞吐可能下降而不是上升。
事务状态比连接数更危险
最常见的坏状态是:
它可能:
- 保留 snapshot,妨碍 vacuum 清理 dead tuple;
- 持有 row/table/advisory lock;
- 阻塞 DDL;
- 增长 xmin horizon;
- 长期占用事务池 backend。
观察:
护栏:
本章沙箱全局值是 600 秒。生产应按 workload 和 role 收紧,不要把长 ETL 与 OLTP 共用一个默认。
建连本身也有成本
一次新 PostgreSQL connection 包含:
若请求每次新建直连,建连成本和认证压力会被放大。连接池复用 server connection,既减少延迟,也把认证、TLS 和 backend churn 从每请求移开。
但复用不能隐藏身份边界:不同 database/user 通常形成不同 server pool, 不同 TLS、startup parameter 和 session feature 也可能阻止复用。
22.2.2 max_connections 不是容量规划答案
ceiling、budget 与 concurrency
区分三个数字:
把 ceiling 调高,只改变第一项。
先保留逃生通道
PostgreSQL 有:
达到普通上限后,拥有相应资格的角色仍可连接。PostgreSQL 18 中,
reserved_connections 与 pg_use_reserved_connections 可以为非 superuser
的受控运维角色保留槽。
预算示意:
但这仍不是建议同时运行 400 条重查询。PgBouncer server pool 应进一步把 active backend 控制在 CPU、I/O 和 workload 能承受的范围。
Little’s Law 先给量级
稳态系统中:
其中:
- :系统内平均并发请求;
- :每秒到达/完成请求数;
- :平均服务时间。
例如:
则平均 active database concurrency 约:
这不表示 pool 只设 20:要考虑 P95/P99、burst、锁等待、连接抖动和 headroom。 但它说明 500 backend 不一定比 40 更快。
排队不可消灭,只能选择位置
当 arrival rate 超过 service capacity:
总会有一个地方排队。好的设计让它:
- 边界明确;
- 有上限;
- 能超时;
- 可观察;
- 不占用昂贵 backend;
- 能按租户/服务隔离;
- 过载时先拒绝低价值工作。
坏的设计把 20,000 个客户端全部变成 PostgreSQL backend,再让 CPU scheduler 充当连接池。
max_connections 提高后的反效果
常见链条:
这是正反馈。正确诊断要看:
不能只看“连接占了 95%”。
何时真的需要提高上限
可能合理的场景:
- 大量长期 idle session,活跃并发低,且 session 语义无法池化;
- 多租户/多 database 需要更多独立最小 pool;
- 运维、复制、监控预算被明确分离;
- 经压测证明 backend 增长不破坏延迟/内存;
- 迁移到更大 CPU/内存并更新所有 guardrail。
变更前至少证明:
22.2.3 应用池、代理池与数据库预算
三层池的乘法
假设:
潜在客户端连接:
如果每个客户端直连 PostgreSQL,这也是潜在 backend 数。
经过 PgBouncer transaction pool 后,server connection 通常按:
形成。粗略上限:
还要受:
约束。
default_pool_size 是每个 pair,不是全局
本章 PgBouncer:
若有:
不能理解成“总共 50 条 PostgreSQL connection”。默认 pool size 会对每个 实际活跃 pair 生效,再被 db/user 上限裁剪。
因此 role 爆炸、database-per-tenant 和多 PgBouncer 实例会改变预算。
一个预算例子
业务:
潜在客户端:
设计 PgBouncer:
数据库预算:
数字必须由压测和生产 profile 校准,但计算结构应先存在。
应用 pool 不应等于线程数
每个 HTTP worker 都持一个数据库连接,常造成:
如果每次请求只有 5 ms 数据库时间,真实 active concurrency 可能很低。
应用 pool 应由:
- 每实例数据库并发;
- acquire deadline;
- request fan-out;
- transaction duration;
- burst headroom;
- PgBouncer queue 目标;
决定,而不是由 CPU thread 数机械复制。
多层 timeout 要共同设计
如果:
请求已取消后,数据库工作仍可能运行数十秒甚至无限。预算不是只有连接数, 还包括“连接占用多久”。
更合理的关系:
具体顺序会因重试和事务而变化,但下游不能普遍比上游 deadline 更长且不传播 取消。
按 workload 分池
不要让所有动作共享一个无差别 pair:
独立 role/pool 可以提供:
- 不同 pool size;
- 不同 statement/lock/idle timeout;
- 不同权限;
- 不同端点;
- 不同监控标签;
- 过载时独立降级。
但 role/pool 太多又会造成 pool multiplication。目标是按失败域与资源合同 分组,不是每个微服务随意造一个 50-connection pool。
预算表
| 消费者 | endpoint | client 上限 | server 上限 | active 目标 | queue timeout | 备注 |
|---|---|---|---|---|---|---|
| shop API | primary pooled | 128 | 32 | 24 | 200 ms | 短事务 |
| worker | primary pooled | 32 | 8 | 6 | 1 s | 可退避 |
| catalog read | replica pooled | 64 | 12 | 8 | 300 ms | 允许陈旧 |
| report | offline direct/pool | 8 | 4 | 2 | 2 s | 长查询限额 |
| migration | default direct | 2 | 2 | 1 | fail fast | 独立身份 |
| monitor/admin | direct/private | 10 | 10 | low | fail fast | 保留槽 |
client 上限 可以大于 server 上限,差值由有界队列吸收。若 arrival 持续
超过 capacity,队列必须超时/拒绝,不能无限累积。
观察预算是否成立
PostgreSQL:
PgBouncer:
重点列:
应用:
三个视角必须用同一 service/database/user/application_name 对齐。
本节检查表
参考资料
- PostgreSQL 18:Connections and Authentication
- PostgreSQL 18:Resource Consumption
- PostgreSQL 18:Monitoring Database Activity
- PgBouncer:Configuration
上一节:服务端点的语义 · 返回本章目录 · 下一节:PgBouncer 池化模式 · 查看全书目录 · 查看索引中心
22.3 PgBouncer 池化模式
PgBouncer 的核心价值是把:
复用到:
复用边界越短,利用率通常越高;但 client session 能拥有的 server state 越少。选择 pool mode 本质上是在效率和会话语义之间选合同。
22.3.1 session、transaction、statement pooling
session pooling
特点:
- client session 稳定绑定同一个 PostgreSQL backend;
- 大部分 PostgreSQL session feature 可用;
- server connection 复用发生在客户端 session 之间;
- 大量长连接会长期占住 server slot,即使 idle。
适合:
- 依赖 session state 的旧应用;
LISTEN/NOTIFY;- session advisory lock;
- persistent temp table;
- 无法修改的 driver/tool;
- 需要逐 session 安全上下文且已经审查。
它减少建连 churn,却不一定显著减少同时 backend 数。
transaction pooling
下一个事务可能得到另一个 backend。优点:
- idle client 不占 PostgreSQL backend;
- 短事务 workload 复用率高;
- 可以在 PgBouncer 处排队;
- 应用连接数与 database active concurrency 解耦。
代价:
- arbitrary session state 不能视为 client 私有;
- backend PID 会变化;
- session-scoped feature 可能错误、泄漏或失效;
- driver behavior 必须按协议和版本测试。
Pigsty 默认 pgbouncer_poolmode: transaction,本章 exact 运行也是
transaction mode。
statement pooling
它提供最强复用,也最严格:
- multi-statement transaction 不可作为一般能力;
- transaction-level state 都难以保留;
- 许多应用和 driver 不兼容;
- 显式
BEGIN通常会被禁止。
除非 workload 真正是独立 statement 且通过完整测试,不应只为追求更少连接 就使用。
模式比较
| 能力 | session | transaction | statement |
|---|---|---|---|
| client 稳定绑定 backend | 是 | 事务期间 | 单 statement |
| multi-statement transaction | 是 | 是 | 否/受限 |
| idle client 占 server | 常见 | 否 | 否 |
| arbitrary session SET | 通常可 | 不可依赖 | 不可依赖 |
| session advisory lock | 可 | 不可依赖 | 不可 |
| LISTEN | 可 | 不可依赖 | 不可 |
| persistent temp table | 可 | 风险高 | 不可 |
| server connection 复用 | 低 | 高 | 最高 |
| 应用兼容成本 | 低 | 中/高 | 高 |
具体能力矩阵必须以当前 PgBouncer feature map 为准,不能把表格跨版本永久化。
pool mode 是接口版本
从 session 改 transaction,不是性能参数微调,而是 API breaking change:
需要:
- inventory/配置 diff;
- driver/ORM feature inventory;
- integration test;
- canary;
- pool 与 SQL 双层观察;
- rollback;
- release note。
长事务会抵消事务池
transaction pool 只有在事务短时才有效:
如果应用:
它仍长期独占 backend。优化 pool mode 不能替代缩短事务。
22.3.2 临时表、会话 GUC、监听与咨询锁
会话 GUC:状态跟 backend,不跟 client
危险例子:
transaction pool 中,第二个事务可能:
- 落到另一 backend,没有
tenant_42; - 另一 client 借到第一条 backend,继承
tenant_42; - 与 PgBouncer tracked parameter 行为交互。
本章正式实验强制两个 backend:
两个失败方向都出现:
安全替代:
或:
- fully qualified object name;
- 把 context 作为 SQL 参数;
- 使用 PgBouncer 明确支持/跟踪的 startup parameter;
- 为必须 session state 的工作使用 session/direct endpoint。
SET LOCAL 生命周期被限制在事务内,与 transaction pooling 边界一致。
server_reset_query 不能想当然
本章 PgBouncer 配置:
看到 DISCARD ALL 不能立刻得出“每个 transaction 后一定清理”。具体执行
条件与 pool mode 受 PgBouncer 配置语义约束。本章故意用实验验证,而不是
从配置名推断。
若将 server_reset_query_always=1 作为补救,还要评估:
- 每事务额外成本;
- prepared statement 与 cache;
- extension/session cleanup;
- 是否真正覆盖所有业务状态;
- 当前版本行为。
更安全的原则仍是:不要跨事务依赖未声明的 server session state。
临时表
PostgreSQL temporary table 通常属于 session:
transaction pool 中后续事务可能到另一 backend,表不存在;另一 client 也 可能得到保留该 temp schema 的 backend。
可选策略:
- 在一个显式事务内创建、使用并
ON COMMIT DROP; - 使用普通 staging table + run/tenant key + 权限/清理;
- 选择 session/direct endpoint;
- 把计算改成 CTE、unnest、COPY 到受控表;
- 对 driver/ORM 的隐式 temp table 做集成测试。
即使 ON COMMIT PRESERVE ROWS 在 feature map 中有特定支持描述,也不要把
“某些操作可工作”升级成“temp session semantics 完整保留”。
LISTEN/NOTIFY
LISTEN channel 注册在 PostgreSQL session。transaction pool 释放 backend
后,client 不再稳定拥有那个 registration。
消费者应使用:
- session pooling;
- direct endpoint;
- 专用少量连接;
- reconnect 后重新
LISTEN; - 通知丢失后的 durable catch-up。
NOTIFY 不是持久消息队列。断连与切换时要从表/outbox/offset 补齐。
咨询锁
区分:
事务池中,优先使用 transaction-level lock,并让整个受保护动作处于同一事务。
session lock 的危险:
部署工具常用 session advisory lock 保证单实例迁移;这类工具应走直连管理 端点,不能在没有验证时经过 transaction pool。
cursor、portal 与 COPY
一般原则:
- WITH HOLD cursor 跨事务;
- 某些 ORM server-side cursor;
- streaming result;
- COPY 双向协议;
- replication protocol;
都要按具体 driver/PgBouncer 版本测试。不要只看 SQL 文本。
安全上下文
尤其危险:
若 RLS policy 或函数依赖这些 session GUC,而 transaction pool 没有可靠 设置/清理,可能形成跨租户泄漏。
更安全:
并:
- deny-by-default policy;
- 每事务显式设置;
- missing/invalid context 立即失败;
- 注入 backend reassignment 测试;
- pool 与 security review 联动。
第 23 章会深入该问题。
22.3.3 预备语句支持必须绑定 PgBouncer 与驱动版本
为什么旧结论互相矛盾
常见说法:
另一种新说法:
两句都过度概括。至少要区分:
协议级 prepared statement
PostgreSQL extended query protocol 使用:
现代 PgBouncer 在:
时可以跟踪/重写协议级 prepared statement,并在 client 换 backend 时准备 对应 server statement。
但结论必须绑定:
- PgBouncer version;
max_prepared_statements;- driver version;
- driver prepare threshold/cache;
- query string identity;
- pool mode;
- failover/reconnect;
- ORM query mode。
本章 exact matrix:
实验先在 backend 65171 建立协议 prepared 状态,再占住它,让同一 client
去 65172。所有结果仍正确。这只接受该版本组合。
SQL PREPARE
这些是普通 SQL text。PgBouncer 不按协议 prepared statement 的方式重写 它们。名称只存在于创建它的 PostgreSQL session。
正式实验:
这是正确的负面结果。不能因为协议级测试通过,就允许 SQL PREPARE 跨
transaction。
driver 可能悄悄改变协议
需要检查:
- simple vs extended query;
- auto prepare threshold;
- named vs unnamed statements;
- statement cache size/lifetime;
- pooler compatibility option;
- binary parameter/result;
- multi-statement batch;
- connection reset hook。
升级 driver 或 PgBouncer 后,应把同一 compatibility suite 重跑。版本说明 不能替应用自己的 query shape。
prepared statement 的容量成本
max_prepared_statements 也不是免费开关。PgBouncer 要维护映射,PostgreSQL
backend 要保存 prepared plan。大量唯一 SQL text、动态注释或 query
literal 可能造成:
- mapping/cache 增长;
- server-side prepared statement 增长;
- deallocation churn;
- generic/custom plan 行为变化;
- schema change 后 invalidation。
监控并限制 query shape,参数化而不是把值拼进 SQL。
一个兼容性测试矩阵
输出应写“这个矩阵通过”,而不是“prepared statements 支持”。
22.3.4 池等待、服务时间与背压
SHOW POOLS 是瞬时状态
关键列:
采样一次 cl_waiting=0 不证明没有排队。要:
- 周期采样;
- 导出 Prometheus 指标;
- 记录 acquire/wait histogram;
- 与应用和 PostgreSQL active backend 对齐。
本章两槽实验
配置在第一个 test/test 客户端连接前临时变为:
12 个 client 同时执行:
结果:
最慢请求大约经历 6 个 250 ms 服务批次。它说明 pool 正在做有界排队,不是 性能 SLO;SSH 采样、调度和连接开销也包含在时间里。
queueing latency
当:
短 burst 可以排队后恢复。
当:
队列长度和延迟持续增长。必须:
- 超时;
- 拒绝;
- 降级;
- 限流;
- 减少工作;
- 或增加经过验证的容量。
不能靠无限 max_client_conn 吸收持续过载。
query_wait_timeout
PgBouncer 的 query_wait_timeout 限制 client 等待 server connection 的
时间。超时会断开 client,从而:
- 释放无限排队;
- 给应用一个可观察失败;
- 迫使请求遵守 deadline。
它要小于业务还能接受的剩余 deadline,并与应用 acquire timeout 协调。
过小:
过大:
reserve pool
reserve_pool_size 允许等待超过 reserve_pool_timeout 后额外建立 server
connection。它适合有限 burst headroom,不是永久绕过预算。
要问:
- reserve 乘以多少 database/user pair;
- 多个 PgBouncer instance 的总和;
- PostgreSQL 是否仍有保留槽;
- burst 激活时 CPU/I/O 是否安全;
- reserve 使用是否告警。
背压应向上游传播
一个健康链条:
一个危险链条:
第 22.5 节会把 timeout、breaker 和负载削减放进同一控制面。
恢复配置
实验使用 finally:
恢复失败是 stop condition,不能“继续看看切换会怎样”。生产变更应通过 Pigsty 声明管理,本章 runtime override 只为低噪声教学实验。
本节检查表
参考资料
- PgBouncer:Feature map
- PgBouncer:Configuration
- PgBouncer:Administration console
- PostgreSQL 18:PREPARE
- PostgreSQL 18:SET
上一节:连接的服务端成本 · 返回本章目录 · 下一节:路由与故障切换 · 查看全书目录 · 查看索引中心
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 池化模式 · 返回本章目录 · 下一节:连接预算与过载边界 · 查看全书目录 · 查看索引中心
22.5 连接预算与过载边界
连接治理的目标不是让所有请求最终都能排到数据库,而是:
这需要同时预算 connection、active transaction、queue length、等待时间和
重试。只设置一个 max_connections 没有形成过载边界。
22.5.1 按服务分配连接、并发与队列
先分服务,再分数字
一个数据库集群常同时承担:
它们的价值、服务时间和失败策略不同。共享一个 pool 意味着:
按服务分配至少包括:
| 维度 | 问题 |
|---|---|
| client connections | 应用能保持多少逻辑连接 |
| server connections | 最多占多少 PostgreSQL backend |
| active concurrency | 同时执行多少事务/查询 |
| queue length/time | 多久后拒绝,最多积压多少 |
| resource | CPU、I/O、work_mem、temp、lock |
| priority | 过载时谁先被削减 |
| reserve | 谁能在拥塞时进入控制面 |
connection budget 不是 concurrency budget
transaction pool 可以有:
三者都合理,只要:
- client process/FD/TLS 成本可承受;
- server pool 不超过数据库预算;
- active workload 经压测;
- 960 个等待者不会无限停留;
- deadline 和拒绝策略有效。
把所有实例相加
预算必须按全局 deployment:
例如:
若 PgBouncer server pool 是 32,客户端能排队;若直连,则一次 blue/green 发布就可能把 backend 翻倍。
database/user pair 的隔离
PgBouncer pool key 通常包含 database 与 user。可以用:
建立不同预算与权限。
不要无界创建 role:
即使每个 pair 很小,乘法也可能超过 PostgreSQL。可通过:
max_db_connections;max_user_connections;- per-database/user override;
- group role + application context;
- tenant 分片;
- idle pool cleanup;
控制。
负载并发门槛
连接池限制的是 backend 数,不直接限制一条连接里 query 的资源。还需要:
- application concurrency semaphore;
- job worker count;
- query governor;
- statement timeout;
- role/database resource policy;
- workload isolation/offline replica。
例如 8 条并行 hash join 可能比 32 条短索引查询更重。server pool size 要按 workload mix 压测。
queue 的接受条件
定义:
长期稳定至少要求:
否则任何有限 queue 最终都会满。
短 burst 的近似吸收能力:
这不是精确 queueing model,但迫使团队问“burst 多大、持续多久”,而不是把 max queue 随手设成 10,000。
Pigsty/HAProxy/PgBouncer 三处上限
本章渲染配置含:
大数字不表示应使用到它。真正的有效边界是这些限制、活跃 pair 和所有实例 总和的组合。
如果 HAProxy 允许 5000、PgBouncer 允许 20000,而应用 deadline 2 秒, 仍应在更早层用 application concurrency/query wait timeout 拒绝过期工作。
一份分配账本
headroom 不是浪费,而是吸收估算误差、maintenance 和 incident action。
22.5.2 超时层级、取消传播与熔断
一次请求经过多个时钟
若各自独立设置,会出现:
资源在用户离开后继续消耗。
deadline budget
把请求总预算 分解:
各层不一定串行,有些重叠;这个式子是设计账本,不是精确 profiler。
例如 2 秒交互请求:
PgBouncer query_wait_timeout=120s 显然不匹配该请求。可以按服务 override,
或让应用 acquire/overall deadline 更早终止并正确 cancel。
connect timeout 不是 failover timeout
多 host 串行尝试时:
[ T_{\text{connect worst}} \approx \sum_{\text{host}} T_{\text{connect host}}
- DNS/TLS/backoff ]
三个 host × 5 秒可能已经超过 request deadline。驱动是否并行尝试、每 host 还是全局 timeout,要按版本确认。
statement_timeout
PostgreSQL 从收到命令开始计时;达到后取消当前 statement。它不一定包含:
- 应用 pool 等待;
- PgBouncer queue;
- DNS/TCP/TLS;
- 客户端处理结果;
- 上一次 idle transaction。
按 role/database 设置比一个全局值更实用:
事务级可以:
lock_timeout
它只限制等待 lock 的时间,不限制 query 总执行时间。
DDL/migration 常用:
意图是:
不要把 lock timeout 设置得比 statement timeout 更长而期待它生效。
idle transaction timeout
切断已开始事务但长期不发 query 的 session。它是 vacuum/lock 保护,不应拿来 清理普通 idle pool connection。
还有普通 idle_session_timeout,但连接池可能把 idle connection 视为资产;
使用前要评估 reconnect churn 和 middleware 兼容。
取消传播
客户端取消 PostgreSQL query 通常需要单独 cancel request/connection path。 经过 pool/proxy 时要验证:
- cancel 能定位正确 backend;
- client 已换 backend 后不会 cancel 别人;
- proxy 是否转发;
- timeout 后 transaction 是否处于 aborted;
- connection 是否应丢弃;
- server query 是否确实停止。
不能只看到 HTTP 499/timeout 就认为数据库工作结束。
熔断器
breaker 保护的是下游与自身:
触发信号应比“任意 SQL error”精细:
- pool acquire timeout;
- connection/role check failure;
- sustained queue;
- downstream saturation;
- known infrastructure outage。
不要因业务约束错误、syntax error 或唯一冲突打开数据库 breaker。
retry budget
如果原始请求率为 ,平均重试 次:
故障时 r 往往上升,正好放大最脆弱的下游。为服务定义:
load shedding
在数据库彻底饱和前:
- 拒绝可选报表/推荐;
- 降低 background worker;
- 使用缓存/较旧副本;
- 限制 expensive endpoint;
- 保留写入/控制面;
- 必要时进入只读或功能降级。
load shedding 是业务决策,不应完全交给随机 connection timeout。
22.5.3 为 ch34 的止血动作预留控制点
第 34 章会处理连接风暴、CPU、内存、磁盘和 I/O 过载。本章要提前提供可用 控制点,否则事故中只能粗暴重启。
控制点一:客户端并发
优点:最接近业务价值,能在请求进入数据库前止血。
控制点二:应用 pool
动态缩 pool 可能只影响新借用,不能立即终止 in-flight transaction。变更行为 按具体 driver 验证。
控制点三:PgBouncer
管理 console 可观察/控制:
这些动作风险不同:
PAUSE等待 server connection 释放,可用于维护;RECONNECT database刷新 server connection;KILL更具破坏性;- runtime
SET会形成声明漂移; - global action blast radius 可能跨服务。
必须有 exact database/instance/role guard 和复位证据。
控制点四:HAProxy
把流量移到副本/其他主库前,先确认目标容量与一致性。转移过载往往只是移动 事故。
控制点五:PostgreSQL role/database
其中 revoke/terminate 可能影响业务或锁,属于受控事故动作。不要把示例 SQL 做成无 guard 的一键脚本。
控制点六:保留管理路径
过载时最怕 DBA 也连不进去。预留:
- superuser/reserved connection;
- 独立 admin role;
- direct endpoint;
- source network allowlist;
- 小而独立的 admin pool;
- break-glass credential;
- out-of-band host access。
监控 exporter 也不应完全依赖已满的普通业务池。
每个控制点都要有回滚
事故动作表:
| 动作 | 目的 | 成功证据 | 副作用 | 复位 |
|---|---|---|---|---|
| 降 worker | 减少 arrival | queue/DB active 降 | backlog 增 | 分阶段恢复 |
| 缩 app pool | 限制 client | acquire wait 可控 | request reject | 恢复声明 |
| pool PAUSE | 维护/切换 | server released | client wait | RESUME |
| RECONNECT db | 刷新 backend | role/path probe 通过 | login burst | 无持久配置 |
| HAProxy drain | 摘除 backend | sessions 收敛 | 容量下降 | enable/weight |
| cancel query | 释放资源 | query 消失 | 事务 abort | 应用重试 |
| terminate | 强制止血 | backend 结束 | outcome unknown | reconcile |
“止血成功”不等于“事故解决”。必须保留证据、找根因和复位。
最小过载仪表盘
应用:
PgBouncer:
PostgreSQL:
代理:
端到端 correlation 必须有 service、database、user、application_name 和时间。
本节检查表
参考资料
- PostgreSQL 18:Client Connection Defaults
- PostgreSQL 18:Server Configuration
- PgBouncer:Configuration
- PgBouncer:Administration console
上一节:路由与故障切换 · 返回本章目录 · 下一节:Pigsty 服务接入层 · 查看全书目录 · 查看索引中心
22.6 Pigsty 服务接入层
Pigsty 不发明 PostgreSQL 的主库、副本或 session 语义。它把:
组合成可交付实现。
理解 Pigsty 服务层的关键不是记命令,而是能把任何观察反向映射到原生组件。
22.6.1 服务定义、角色选择与端口
默认变量
本章参考实现的关键声明形态:
版本和自定义配置可能不同。读取实际 inventory、role defaults 与 rendered file,不要把这段当成跨版本常量。
dest
服务 destination 可以表达:
因此:
在本章 pg_default_service_dest=pgbouncer 时走连接池;如果用户改成
postgres,同一个 5433/5434 就绕过池。
端口名不能替实际路径。
check
check 是 Patroni REST health path:
HAProxy 对成员的 8008 检查,数据流则去 dest。角色判断来自 Patroni,
不是 HAProxy 解析 PostgreSQL protocol。
selector
selector 从 cluster member inventory 中选普通 backend。
表示全集。
offline 示例只选择:
这让平台能把特定 replica 标为重查询目标。selector 是期望集合,运行时健康 检查仍可能摘除不合格成员。
backup
backup selector 形成 HAProxy backup server。它决定正常集合不可用时是否 降级。
要把 backup 语义写进服务合同:
- replica 回 primary 是否允许;
- offline 回普通 replica 是否允许;
- backup 激活是否告警;
- 目标是否有容量;
- client
target_session_attrs会接受还是拒绝。
pg_service_provider
默认空值通常在每个 PostgreSQL node 上交付 local HAProxy service。
也可指定专用 HAProxy node group。此时要重新设计:
- provider 高可用;
- provider 到数据库网络;
- DNS/VIP/multi-host;
- config rollout;
- source IP/HBA;
- stats/metrics;
- 故障域。
把 HAProxy 从数据库节点移出,不自动获得入口 HA。
VIP 与 DNS
相关声明包括:
这是交付入口的机制选择。启用前要按第 22.1.3 节验证网络、仲裁、DNS cache 与证书,不能因为变量存在就宣称通过。
自定义业务服务
可以在 pg_services 添加服务,而不是修改默认列表。例如概念上:
生产声明还应补:
- backup/fail-closed;
- maxconn;
- balance;
- options/rise/fall;
- owner 和用途;
- TLS/网络;
- driver endpoint。
不要为每个应用随意开端口;只有语义或资源/失败域不同才需要新服务。
22.6.2 PgBouncer、HAProxy 与数据库的证据链
第一步:声明证据
从 reviewed inventory 提取 secret-free projection:
凭据值不进入报告,只记录:
第二步:rendered HAProxy
本章 /etc/haproxy/pg-test-*.cfg 投影:
同时核对:
只核对文件 diff 仍不够;进程可能未 reload 或 runtime state 不同。
第三步:HAProxy runtime
从 stats socket/API 看:
敏感 stats user/password 不输出。使用 local protected socket 比把管理页面凭据 写入脚本更安全。
第四步:PgBouncer config
在 local Unix admin socket:
本章 safe projection:
不要采集:
- password;
- SCRAM verifier;
- auth file 内容;
- inventory secret;
- admin credential。
数据库 LOGIN 不等于池化身份已交付
本章开发过程中故意撞到一个重要边界:
因为本章 Pigsty 默认:
PgBouncer authentication surface 由声明式用户清单管理。只有数据库 catalog 里存在 role,不等于 pooler 的 auth file/query 已经认识它。
生产用户应在 Pigsty pg_users 中声明并明确:
实际字段与 secret workflow 以当前版本文档和组织规范为准。不要手改
userlist.txt 制造不可追踪漂移。
本章 formal run 因此使用既有、Pigsty 已声明的 nonproduction test 用户,
只创建专属 schema/table;脚本永不修改或删除该 role。
若启用 pgbouncer_auth_query,还要评审:
auth_user与查询权限;- query 在 replica/primary 的行为;
- password rotation;
- role expiration;
- auth database;
- failover;
- secret exposure。
第五步:PostgreSQL 原生状态
对每个 member:
并观察连接预算:
pg_postmaster_start_time() 在本章用来把经过 local Unix socket 的 PgBouncer
session 映射回具体 member;inet_server_addr() 对 Unix backend 可能为空。
第六步:从 client 走完整路径
每个 service 用真实 database/user:
预期:
| service | recovery | read_only | path |
|---|---|---|---|
| primary 5433 | false | false | HAProxy → PgBouncer |
| replica 5434 | true | true | HAProxy → PgBouncer |
| default 5436 | false | false | HAProxy → PostgreSQL |
| offline 5438 | true | true | HAProxy → PostgreSQL |
再到每台 PgBouncer SHOW POOLS,证明 pooled endpoint 真正在对应 process
形成了 database/user pool。
第七步:行为证据
配置与角色通过后仍要测:
- 12-client/2-server queue;
- backend reassignment;
- session state 丢失/泄漏;
- protocol prepared;
- SQL PREPARE negative;
- async token visibility;
- planned switch/reconnect;
- final config/topology restore。
这才完成从声明到用户体验的链。
证据矩阵
| Claim | 声明 | 渲染 | runtime | SQL/client |
|---|---|---|---|---|
| 5433 主写 | service check/dest | primary cfg | backend status | writable |
| 5434 副本优先 | backup/selector | replica cfg | selected pool | read-only/member |
| 事务池 | pool mode | pgbouncer ini | SHOW CONFIG/POOLS | PID reassignment |
| 2 server cap | runtime override | N/A | sv_active ≤ 2 | 12 clients complete |
| prepared 支持 | max_prepared | SHOW CONFIG | two server PID | correct protocol results |
| switch recovery | Patroni/service | health config | topology/pool refresh | token reconcile |
22.6.3 配置变更、reload 与连接行为验证
不要直接编辑 rendered file
错误流程:
问题:
- inventory 不知道;
- 下次 automation 覆盖;
- 多节点不一致;
- review/rollback 不完整;
- secret/权限可能漂移。
正确流程:
service tag
参考代码中服务生成/reload 由 pg_service 相关 task/tag 管理,典型调用形态:
生产执行前必须按当前 Pigsty 版本查看 help/plan、限定 inventory 和 host。 不要从书中复制命令直接指向未知集群。
render task 会生成 service config,并在 reload 前运行 HAProxy config check。
配置校验
原生检查:
它证明语法/引用可加载,不证明路由语义正确。
PgBouncer reload:
不是所有配置都支持在线改变;某些需要 reconnect/restart。SHOW CONFIG 的
changeable 列与当前文档共同决定。
reload 与现有连接
必须回答:
“reload 成功”不能替代这些答案。
role change 与 config change 是两类变更
config change:
role change:
二者可能同时发生,但 rollback 不同。故障切换时不应顺手修改持久配置, 否则难以分辨恢复来自哪项动作。
本章 runtime pool override 只用于实验,并在切换前恢复,正是为了隔离变量。
staged rollout
多入口环境:
- 选一个无生产或低流量 provider;
- render/check;
- reload;
- direct health + client probe;
- 观察 queue/error/session;
- 扩到下一 provider;
- 完整端点矩阵;
- 保留旧配置与回滚。
若所有 provider 同时 reload,错误配置会同时摧毁入口冗余。
变更后的强制验证
如果变更涉及 role/promotion,再执行 pool role-state refresh 检查。
回滚
回滚不是把文件复制回去:
如果数据库 role 已在期间改变,旧 rendered config 的成员角色仍由 health check 动态判断,但 selector/backup/destination 可能不再合适,要重新评审。
secret 与证据
服务变更会接触:
- inventory password;
- PgBouncer userlist/verifier;
- HAProxy stats auth;
- TLS key;
- HBA/identity。
证据只保留:
不要把整个 inventory、auth file 或 config 原文无差别上传。正式 lab 对
临时 credential inventory 要求 mode 0600,使用后删除副本,报告
secret_values_exported=0。
本节检查表
参考资料
- Pigsty:PostgreSQL Service
- Pigsty:PgBouncer Administration
- PgBouncer:Configuration
- HAProxy:Configuration tutorials
上一节:连接预算与过载边界 · 返回本章目录 · 下一节:实战:写入、只读与管理三类接入 · 查看全书目录 · 查看索引中心
22.7 实战:写入、只读与管理三类接入
本节把前六节变成一个可重放验收:
它在本地 Pigsty nonproduction sandbox 执行 L1/L2 动作。不要把 guard 改掉后 指向生产。
22.7.1 为 pg36_shop 配置端点和连接预算
先写生产设计,后映射 sandbox
pg36_shop 的概念设计:
| logical service | 用途 | role/path | session | freshness |
|---|---|---|---|---|
pg36_shop_rw |
API/worker 短写事务 | primary pooled | transaction | primary |
pg36_shop_ro |
catalog/非因果读 | replica pooled | transaction | 声明 staleness |
pg36_shop_admin |
migration/诊断 | primary direct | full session | primary |
pg36_shop_olap |
报表/ETL | offline direct/受控 pool | workload-specific | 可陈旧 |
本章不创建真实 pg36_shop database,而把它映射到保留沙箱:
为什么不临时创建一个 LOGIN:
正式 runner 从 private reviewed Pigsty inventory 读取既有 test credential,
写入 mode 0600 的临时 libpq service file,结束后删除;credential 不打印、
不 hash 到报告、不进入 Git。
生产 identity 应如何声明
生产应在 reviewed Pigsty inventory/secret workflow 中声明:
字段与 secret 语法按当前 Pigsty 版本确认。还要设置:
- owner/group role 与 login role 分离;
- least privilege;
- connection limit;
- default privilege;
- role/database timeout;
- TLS/HBA;
- rotation;
- application_name;
- direct admin role;
- PgBouncer per-user/database budget。
不要把书中 placeholder 作为可用 secret。
libpq service file
概念结构:
密码应来自 .pgpass、secret manager 或受控 service material。文件权限:
本章沙箱 PgBouncer client TLS 是 disable,使用 sslmode=prefer 只为匹配
事实,并保留 EX20-CLIENT-PROXY-NO-TLS;生产必须另做 TLS 验收。
预算草案
假设:
客户端上限:
不是 278 个 backend。一个候选 server budget:
需要在生产规模压测后定稿。
fixture 合同
setup.sql 创建:
它验证:
- existing schema owner/comment;
- exact columns/type/nullability;
- primary/unique constraints;
- declared login safe attributes;
- grants only USAGE/SELECT/INSERT;
- role 未被 runner 创建、修改或接管。
fixture 是 synthetic data,drill 不自动删除它。
四端点预期
SQL 同时记录 postmaster start time,与直连三成员的基线映射,解决 PgBouncer
local Unix backend 下 inet_server_addr() 可能为空的问题。
22.7.2 验证会话状态、预备语句与只读一致性
风险分级
| 动作 | 风险 | 改动 |
|---|---|---|
capture |
L0 | 只读快照 |
verify/review/all |
L0 | 重验既有证据 |
| schema setup | L1 | synthetic schema/table |
| pool override | L1 | 一个 PgBouncer process runtime 值 |
| queue/session/prepare/visibility | L1 | synthetic connection/row |
| planned switch + restore | L2 | Patroni role/timeline |
reset:fixture |
L3 | 删除 synthetic schema |
all 从不:
preflight
在任何 mutation 前,第 19 章 gate 验证:
第 22 章 capture 再验证:
任何 drift 先停。
pool role-state baseline
在第一个应用 probe 前:
在三台 PgBouncer 分别执行,清除上一轮角色周期遗留的服务端连接状态。
这一步来自真实失败发现:
它只影响 sandbox teaching database,且是 evidence-bearing action。
临时两槽 pool
先 snapshot:
再 runtime SET:
为什么 runtime:
- 让 12-client queue 低噪声可观察;
- 避免为实验 saturate 50+30;
- 不修改 rendered file;
- exact
finallyrollback。
生产 pool policy 必须回到 Pigsty declaration,不照抄 runtime SET。
endpoint probe
每个 service 连接后:
同时三节点 SHOW POOLS 证明 test/test pooled path。
saturation probe
12 个 client 同时:
管理 console 周期采样:
验收:
正式:
session counterexample
为确定性分配:
RECONNECT test;- A 在 backend X
SET search_path=pg_catalog,commit; - B 借到 X 并保持 transaction;
- A 被迫借 backend Y;
- 比较 PID 与 search_path;
- 关闭 client,
RECONNECT test清理实验状态。
正式:
既证明 state leakage,也证明 state loss。
protocol prepared
Psycopg:
正式:
接受范围:
SQL PREPARE negative
占住创建 backend,再:
正式:
这是必须出现的失败。
replica visibility
写端点插入 unique token,commit 后取得主库 LSN;只读端点轮询 exact token, 记录:
正式:
验收只要求在 5 秒 sandbox window 内看见,不形成 freshness SLO。
pool rollback gate
以上任一步成功或失败,finally 恢复:
读取 SHOW CONFIG exact compare。只有:
才允许 L2 切换。
22.7.3 注入切换与连接风暴,观察退避和恢复
这里“注入”的边界
本节只执行:
不注入 process/network/storage/DCS failure。标题中的“连接风暴”是小型 6-worker 重连探针,不是生产规模压力。
exact guards
需要两份 private input:
在普通环境两者可以来自同一 reviewed inventory 的安全副本。本书 local sandbox 的已部署 v4.5 baseline 与当前工作目录声明版本不同,因此 formal run 明确分离,避免用新声明冒充旧部署。
执行:
所有值 exact match。output 非空、inventory 缺失/权限错误、topology drift 都会拒绝。
client workload
六个 worker,24 秒:
每 attempt 短连接,service:
失败:
forward
exact executor:
完成条件:
然后三节点:
必须出现一次 refresh 之后的 acknowledged write,才能进入回切。
restore
完成:
再次刷新三节点 pool,并要求首笔确认。
pool refresh evidence
正向三成员 action:
回切:
这些 action time 只是管理命令耗时;write gap 还包括 topology、health、 server login 和 client backoff。
reconcile
结束后查询本 run 的所有 worker row:
分类:
正式:
unknown_absent=48 不是失败;它们已被确定分类。若 unknown committed > 0,
也可以通过,只要 token lookup 明确且业务不重复执行。真正不允许的是
unreconciled。
时间口径
conservative gap:
它包含 probe interval、connection attempt 和 backoff,不是纯数据库 promotion 时间,也不是 production RTO。
postflight 和反例
第 19 章 postflight 再次通过。十五个 evidence mutation 必须被指定错误码拒绝:
反例不是额外单元测试装饰,它防止 validator 只检查“文件存在”。
evidence tree
完整证据含 token 和运行细节,应放 private evidence store,不提交 Git。
仓库只保留 secret-free 聚合 connection-run.json。
read-only 重验
应输出:
reset
reset 与 drill 完全分离:
确认 token 为兼容已发布的实验接口保留旧名称,但当前 reset 只:
它不回滚 timeline、不清理 evidence、不改 pool。删除前仍应阅读脚本并确认 exact target。
失败时保守恢复
脚本:
- pool override 已开始就尝试恢复 baseline;
- switch 未开始则不触碰 topology;
- 若
pg-test-2是唯一稳定 leader,允许计划切回; - topology ambiguous/degraded 时不猜、不 force;
- 保留 failure manifest;
- 不自动 drop fixture。
finally 能降低风险,不能替代 operator inspection。
生产准入差距
本章 sandbox contract:
生产仍需:
- 在 reviewed Pigsty inventory 声明 database/user/service/budget;
- 验收 client/server TLS 与证书轮换;
- 证明 VIP、DNS 或 multi-host entry failover;
- 跑真实 driver/ORM/query-mode matrix;
- 在 production-class 资源做容量和 reconnect load test;
- 注入 unplanned failure、partial network 与 cancel;
- 为每个 replica workload 定义 consistency contract;
- 把 pool refresh 自动化、告警化并限定 blast radius;
- 把结果纳入 SLO/SOP/change review;
- 由业务 owner、安全与平台共同签署。
不要把本章 8.510 秒写进生产 SLO。
本章完成定义
读者应能独立解释并证明:
若只能背端口和 RECONNECT 命令,本章还没有完成。
参考资料
- Pigsty:PostgreSQL Service
- PgBouncer:Feature map
- PgBouncer:Configuration
- PgBouncer:Administration console
- PostgreSQL 18:libpq connection parameters
上一节:Pigsty 服务接入层 · 返回本章目录 · 下一章:固若金汤:认证、授权与数据安全 · 查看全书目录 · 查看索引中心