跳至内容

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;
  • LISTEN registration;
  • 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 overhead

work_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_connectionspg_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 present

22.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。

预算表

消费者endpointclient 上限server 上限active 目标queue timeout备注
shop APIprimary pooled1283224200 ms短事务
workerprimary pooled32861 s可退避
catalog readreplica pooled64128300 ms允许陈旧
reportoffline direct/pool8422 s长查询限额
migrationdefault direct221fail fast独立身份
monitor/admindirect/private1010lowfail 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 和资源

参考资料


上一节:服务端点的语义 · 返回本章目录 · 下一节:PgBouncer 池化模式 · 查看全书目录 · 查看索引中心

最后更新于