22.2 连接的服务端成本
PostgreSQL 不是把所有客户端请求放进一个无状态线程池。经典架构中,每个已 认证客户端连接对应一个 backend process:
client TCP/socket
-> postmaster accepts
-> one backend process
-> one session state
-> zero or one current transaction因此“连接数”同时占用身份、进程、内存、文件描述符、共享结构与调度能力。 连接池的目标不是让数据库接受无限请求,而是把大量客户端等待放到比 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、日志上下文和统计状态。
可以观察:
SELECT pid,
usename,
datname,
application_name,
client_addr,
backend_start,
xact_start,
state,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY backend_start;state='idle' 只表示当前没有执行 query,不表示连接免费。它仍占用 backend
slot 和 process,并保留 session state。
connection、session、transaction、statement
四个边界不能混用:
connection
一条客户端到 PostgreSQL/PgBouncer 的协议连接
session
登录后到断开前的逻辑上下文
transaction
BEGIN/COMMIT 或一个 autocommit statement 的原子边界
statement
一条 SQL 执行直连 PostgreSQL 时:
client connection ~= PostgreSQL backend session事务池时:
client connection A
transaction 1 -> backend X
transaction 2 -> backend Y
client connection B
transaction 1 -> backend X应用仍觉得自己有一个长连接,但 server session 已不是它的私有状态容器。
私有内存与共享内存
连接成本不能简化成固定的“每连接 10 MB”。大致有:
baseline private process memory
+ catalog/relation cache growth
+ query executor memory
+ sort/hash/work_mem consumers
+ temp_buffers actually touched
+ protocol/result buffers
+ extension/PL runtime state
+ kernel socket/process overheadwork_mem 不是每个连接一次,而可能是每个执行节点、并行 worker 各自一次:
[ M_{\text{query}} \approx \sum_{\text{memory node}} work_mem \times \text{workers} ]
若 200 个 backend 同时执行多 hash/sort 查询,work_mem=64MB 并不意味着
最多使用 (200 \times 64\text{MB});实际可能更高。
本章沙箱观察:
max_connections 500
superuser_reserved_connections 10
reserved_connections 0
work_mem 64 MB
temp_buffers 8 MB
max_locks_per_transaction 500这些是配置事实,不是“500 个并发重查询安全”的证明。
共享结构也随连接上限变化
某些 shared memory 与数组在启动时按:
max_connections
max_prepared_transactions
max_locks_per_transaction
max_worker_processes等参数估算。提高 max_connections 可能要求重启,并增加:
- backend/proc array;
- lock table 的潜在规模;
- per-backend shared bookkeeping;
- snapshot、predicate lock 等间接压力。
更隐蔽的是 CPU scheduler:大量 runnable backend 会争抢 CPU 和 cache, 吞吐可能下降而不是上升。
事务状态比连接数更危险
最常见的坏状态是:
idle in transaction它可能:
- 保留 snapshot,妨碍 vacuum 清理 dead tuple;
- 持有 row/table/advisory lock;
- 阻塞 DDL;
- 增长 xmin horizon;
- 长期占用事务池 backend。
观察:
SELECT pid,
usename,
now() - xact_start AS xact_age,
state,
wait_event_type,
wait_event,
left(query, 120) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;护栏:
ALTER ROLE pg36_shop_app IN DATABASE pg36_shop
SET idle_in_transaction_session_timeout = '30s';本章沙箱全局值是 600 秒。生产应按 workload 和 role 收紧,不要把长 ETL 与 OLTP 共用一个默认。
建连本身也有成本
一次新 PostgreSQL connection 包含:
TCP + TLS
authentication
postmaster fork/exec or process setup
startup parameters
catalog/role/database initialization
extension/session initialization若请求每次新建直连,建连成本和认证压力会被放大。连接池复用 server connection,既减少延迟,也把认证、TLS 和 backend churn 从每请求移开。
但复用不能隐藏身份边界:不同 database/user 通常形成不同 server pool, 不同 TLS、startup parameter 和 session feature 也可能阻止复用。
22.2.2 max_connections 不是容量规划答案
ceiling、budget 与 concurrency
区分三个数字:
max_connections
PostgreSQL 接受的普通 backend slot 上限
connection budget
分配给应用、运维、复制/扩展和紧急访问的合同
active concurrency
某时刻真正占用 CPU/I/O/lock 执行工作的事务数把 ceiling 调高,只改变第一项。
先保留逃生通道
PostgreSQL 有:
superuser_reserved_connections
reserved_connections达到普通上限后,拥有相应资格的角色仍可连接。PostgreSQL 18 中,
reserved_connections 与 pg_use_reserved_connections 可以为非 superuser
的受控运维角色保留槽。
预算示意:
max_connections 500
- superuser reserved 10
- platform/monitor/admin 40
- logical replication/maintenance 20
- incident reserve 30
= maximum allocatable app slots 400但这仍不是建议同时运行 400 条重查询。PgBouncer server pool 应进一步把 active backend 控制在 CPU、I/O 和 workload 能承受的范围。
Little’s Law 先给量级
稳态系统中:
[ L = \lambda W ]
其中:
- (L):系统内平均并发请求;
- (\lambda):每秒到达/完成请求数;
- (W):平均服务时间。
例如:
2000 transactions/s
average database service time 10 ms则平均 active database concurrency 约:
[ 2000 \times 0.010 = 20 ]
这不表示 pool 只设 20:要考虑 P95/P99、burst、锁等待、连接抖动和 headroom。 但它说明 500 backend 不一定比 40 更快。
排队不可消灭,只能选择位置
当 arrival rate 超过 service capacity:
application queue
PgBouncer cl_waiting
HAProxy queue
PostgreSQL lock wait
CPU run queue
storage queue总会有一个地方排队。好的设计让它:
- 边界明确;
- 有上限;
- 能超时;
- 可观察;
- 不占用昂贵 backend;
- 能按租户/服务隔离;
- 过载时先拒绝低价值工作。
坏的设计把 20,000 个客户端全部变成 PostgreSQL backend,再让 CPU scheduler 充当连接池。
max_connections 提高后的反效果
常见链条:
pool acquire timeout
-> raise max_connections
-> more active queries
-> CPU/cache contention + I/O queue
-> service time grows
-> connections live longer
-> pool still exhausted这是正反馈。正确诊断要看:
arrival rate
transaction service time
active vs idle connection
pool waiting
lock wait
CPU run queue
I/O latency
result size/client consumption
retry amplification不能只看“连接占了 95%”。
何时真的需要提高上限
可能合理的场景:
- 大量长期 idle session,活跃并发低,且 session 语义无法池化;
- 多租户/多 database 需要更多独立最小 pool;
- 运维、复制、监控预算被明确分离;
- 经压测证明 backend 增长不破坏延迟/内存;
- 迁移到更大 CPU/内存并更新所有 guardrail。
变更前至少证明:
memory worst-case reviewed
shared memory/startup impact reviewed
reserved slots preserved
OS pid/fd limits sufficient
pool multiplication calculated
load test latency/throughput acceptable
rollback and restart plan present22.2.3 应用池、代理池与数据库预算
三层池的乘法
假设:
application replicas A
pool max per replica P
deployment versions/environments D潜在客户端连接:
[ C_{\text{client}} = A \times P \times D ]
如果每个客户端直连 PostgreSQL,这也是潜在 backend 数。
经过 PgBouncer transaction pool 后,server connection 通常按:
database × user × pool configuration × PgBouncer instance形成。粗略上限:
[ C_{\text{server}} \le \sum_{\text{pool keys}} (default_pool_size + reserve_pool_size) ]
还要受:
max_db_connections
max_user_connections
global process/FD capacity
PostgreSQL max_connections约束。
default_pool_size 是每个 pair,不是全局
本章 PgBouncer:
default_pool_size=50
reserve_pool_size=30
max_db_connections=100
max_user_connections=100若有:
10 database × 5 user不能理解成“总共 50 条 PostgreSQL connection”。默认 pool size 会对每个 实际活跃 pair 生效,再被 db/user 上限裁剪。
因此 role 爆炸、database-per-tenant 和多 PgBouncer 实例会改变预算。
一个预算例子
业务:
8 app instances
每实例 max client pool 16
突发 worker 4 × pool 8
后台任务 2 × pool 6潜在客户端:
8×16 + 4×8 + 2×6 = 172设计 PgBouncer:
pg36_shop_app server pool 32
pg36_shop_worker server pool 8
pg36_shop_report server pool 4
reserve shared/limited 8数据库预算:
application active backend 52
monitor/exporter 8
admin/migration 10
maintenance/extension 10
incident reserve 20
other databases 80
headroom 20
total 200数字必须由压测和生产 profile 校准,但计算结构应先存在。
应用 pool 不应等于线程数
每个 HTTP worker 都持一个数据库连接,常造成:
100 app pods × 20 workers = 2000 client connections如果每次请求只有 5 ms 数据库时间,真实 active concurrency 可能很低。
应用 pool 应由:
- 每实例数据库并发;
- acquire deadline;
- request fan-out;
- transaction duration;
- burst headroom;
- PgBouncer queue 目标;
决定,而不是由 CPU thread 数机械复制。
多层 timeout 要共同设计
如果:
HTTP deadline 2s
application pool wait 5s
PgBouncer query wait 120s
statement_timeout 0请求已取消后,数据库工作仍可能运行数十秒甚至无限。预算不是只有连接数, 还包括“连接占用多久”。
更合理的关系:
pool acquire < connect < database work < request deadline具体顺序会因重试和事务而变化,但下游不能普遍比上游 deadline 更长且不传播 取消。
按 workload 分池
不要让所有动作共享一个无差别 pair:
oltp app
background worker
reporting
migration/admin
monitor独立 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:
SELECT usename,
datname,
state,
count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY usename, datname, state
ORDER BY count(*) DESC;PgBouncer:
SHOW POOLS;
SHOW STATS;
SHOW DATABASES;
SHOW USERS;重点列:
cl_active / cl_waiting
sv_active / sv_idle / sv_login
maxwait / maxwait_us
total_xact_count / total_query_count
avg_xact_time / avg_query_time / avg_wait_time应用:
pool in-use/idle/waiters
acquire latency and timeout
request deadline/cancel
retry count
database call duration三个视角必须用同一 service/database/user/application_name 对齐。
本节检查表
[ ] 知道 idle backend 仍然有成本
[ ] active transaction 与 connection 数分开观测
[ ] work_mem 按执行节点/worker 而非连接一次估算
[ ] max_connections 保留运维和事故槽
[ ] 用吞吐×服务时间估算 active concurrency 量级
[ ] 应用实例×pool×版本的乘法已计算
[ ] PgBouncer 按 database/user/instance 的乘法已计算
[ ] queue 有上限、超时和指标
[ ] workload 按资源/失败域分池,不过度碎片化
[ ] load test 同时看 throughput、tail latency 和资源参考资料
- PostgreSQL 18:Connections and Authentication
- PostgreSQL 18:Resource Consumption
- PostgreSQL 18:Monitoring Database Activity
- PgBouncer:Configuration
上一节:服务端点的语义 · 返回本章目录 · 下一节:PgBouncer 池化模式 · 查看全书目录 · 查看索引中心