# 连接的服务端成本

LLMS 索引： [llms.txt](/llms.txt)

---

PostgreSQL 不是把所有客户端请求放进一个无状态线程池。经典架构中，每个已
认证客户端连接对应一个 backend process：

```text
client TCP/socket
  -> postmaster accepts
      -> one backend process
          -> one session state
              -> zero or one current transaction
```

因此“连接数”同时占用身份、进程、内存、文件描述符、共享结构与调度能力。
连接池的目标不是让数据库接受无限请求，而是把大量客户端等待放到比 backend
更便宜、更可控的位置。

## 22.2.1 后端进程、内存、事务与会话状态 {#item-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、日志上下文和统计状态。

可以观察：

```sql
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

四个边界不能混用：

```text
connection
  一条客户端到 PostgreSQL/PgBouncer 的协议连接

session
  登录后到断开前的逻辑上下文

transaction
  BEGIN/COMMIT 或一个 autocommit statement 的原子边界

statement
  一条 SQL 执行
```

直连 PostgreSQL 时：

```text
client connection ~= PostgreSQL backend session
```

事务池时：

```text
client connection A
  transaction 1 -> backend X
  transaction 2 -> backend Y

client connection B
  transaction 1 -> backend X
```

应用仍觉得自己有一个长连接，但 server session 已不是它的私有状态容器。

### 私有内存与共享内存

连接成本不能简化成固定的“每连接 10 MB”。大致有：

```text
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}\)；实际可能更高。

本章沙箱观察：

```text
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 与数组在启动时按：

```text
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，
吞吐可能下降而不是上升。

### 事务状态比连接数更危险

最常见的坏状态是：

```text
idle in transaction
```

它可能：

- 保留 snapshot，妨碍 vacuum 清理 dead tuple；
- 持有 row/table/advisory lock；
- 阻塞 DDL；
- 增长 xmin horizon；
- 长期占用事务池 backend。

观察：

```sql
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;
```

护栏：

```sql
ALTER ROLE pg36_shop_app IN DATABASE pg36_shop
  SET idle_in_transaction_session_timeout = '30s';
```

本章沙箱全局值是 600 秒。生产应按 workload 和 role 收紧，不要把长 ETL 与
OLTP 共用一个默认。

### 建连本身也有成本

一次新 PostgreSQL connection 包含：

```text
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` 不是容量规划答案 {#item-22-2-2}

### ceiling、budget 与 concurrency

区分三个数字：

```text
max_connections
  PostgreSQL 接受的普通 backend slot 上限

connection budget
  分配给应用、运维、复制/扩展和紧急访问的合同

active concurrency
  某时刻真正占用 CPU/I/O/lock 执行工作的事务数
```

把 ceiling 调高，只改变第一项。

### 先保留逃生通道

PostgreSQL 有：

```text
superuser_reserved_connections
reserved_connections
```

达到普通上限后，拥有相应资格的角色仍可连接。PostgreSQL 18 中，
`reserved_connections` 与 `pg_use_reserved_connections` 可以为非 superuser
的受控运维角色保留槽。

预算示意：

```text
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\)：平均服务时间。

例如：

```text
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：

```text
application queue
PgBouncer cl_waiting
HAProxy queue
PostgreSQL lock wait
CPU run queue
storage queue
```

总会有一个地方排队。好的设计让它：

- 边界明确；
- 有上限；
- 能超时；
- 可观察；
- 不占用昂贵 backend；
- 能按租户/服务隔离；
- 过载时先拒绝低价值工作。

坏的设计把 20,000 个客户端全部变成 PostgreSQL backend，再让 CPU scheduler
充当连接池。

### `max_connections` 提高后的反效果

常见链条：

```text
pool acquire timeout
  -> raise max_connections
      -> more active queries
          -> CPU/cache contention + I/O queue
              -> service time grows
                  -> connections live longer
                      -> pool still exhausted
```

这是正反馈。正确诊断要看：

```text
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。

变更前至少证明：

```text
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 应用池、代理池与数据库预算 {#item-22-2-3}

### 三层池的乘法

假设：

```text
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 通常按：

```text
database × user × pool configuration × PgBouncer instance
```

形成。粗略上限：

\[
C_{\text{server}}
\le
\sum_{\text{pool keys}}
(default\_pool\_size + reserve\_pool\_size)
\]

还要受：

```text
max_db_connections
max_user_connections
global process/FD capacity
PostgreSQL max_connections
```

约束。

### `default_pool_size` 是每个 pair，不是全局

本章 PgBouncer：

```text
default_pool_size=50
reserve_pool_size=30
max_db_connections=100
max_user_connections=100
```

若有：

```text
10 database × 5 user
```

不能理解成“总共 50 条 PostgreSQL connection”。默认 pool size 会对每个
实际活跃 pair 生效，再被 db/user 上限裁剪。

因此 role 爆炸、database-per-tenant 和多 PgBouncer 实例会改变预算。

### 一个预算例子

业务：

```text
8 app instances
每实例 max client pool 16
突发 worker 4 × pool 8
后台任务 2 × pool 6
```

潜在客户端：

```text
8×16 + 4×8 + 2×6 = 172
```

设计 PgBouncer：

```text
pg36_shop_app server pool        32
pg36_shop_worker server pool      8
pg36_shop_report server pool      4
reserve shared/limited            8
```

数据库预算：

```text
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 都持一个数据库连接，常造成：

```text
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 要共同设计

如果：

```text
HTTP deadline           2s
application pool wait   5s
PgBouncer query wait    120s
statement_timeout       0
```

请求已取消后，数据库工作仍可能运行数十秒甚至无限。预算不是只有连接数，
还包括“连接占用多久”。

更合理的关系：

```text
pool acquire < connect < database work < request deadline
```

具体顺序会因重试和事务而变化，但下游不能普遍比上游 deadline 更长且不传播
取消。

### 按 workload 分池

不要让所有动作共享一个无差别 pair：

```text
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：

```sql
SELECT usename,
       datname,
       state,
       count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY usename, datname, state
ORDER BY count(*) DESC;
```

PgBouncer：

```sql
SHOW POOLS;
SHOW STATS;
SHOW DATABASES;
SHOW USERS;
```

重点列：

```text
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
```

应用：

```text
pool in-use/idle/waiters
acquire latency and timeout
request deadline/cancel
retry count
database call duration
```

三个视角必须用同一 service/database/user/application_name 对齐。

## 本节检查表

```text
[ ] 知道 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](https://www.postgresql.org/docs/18/runtime-config-connection.html)
- [PostgreSQL 18：Resource Consumption](https://www.postgresql.org/docs/18/runtime-config-resource.html)
- [PostgreSQL 18：Monitoring Database Activity](https://www.postgresql.org/docs/18/monitoring-stats.html)
- [PgBouncer：Configuration](https://www.pgbouncer.org/config.html)

---

[上一节：服务端点的语义](../01/) · [返回本章目录](../) · [下一节：PgBouncer 池化模式](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
