# 连接并非免费

> 根据核心数和延迟确定连接池大小，有意识地选择连接池模式，并设置超时，避免一个坏客户端耗尽服务器。

---

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

---

<span id="basics-chapter-28"></span>

事情通常这样发生：应用在负载下变慢，于是团队把它横向扩展。四个 pod 变成四十个。每个 pod 有一个大小为 20 的连接池，于是数据库现在看到 800 个连接，`max_connections` 也提高到 1,000 来容纳它们。吞吐量下降，p99 延迟变成原来的三倍。数据库 CPU 占用达到 100%，做的工作却比以前更少。

没有哪条查询是错的。数据库只是在一台 16 核、同一时间能做 16 件事的机器上，同时周旋于 800 个会话，而周旋本身已经成了工作负载。

连接更多不等于容量更大。每个连接都是一个进程，都有内存、CPU 和建立连接的代价；合适的数量取决于数据库的核心数和磁盘，而不是用户数量。

## 每个连接都是一个进程 {#every-connection-is-a-process}

客户端连接时，postmaster（服务器的父进程）会为它派生一个新的操作系统进程。这个进程就是一个**后端进程**，负责解析、规划和执行你的查询，并一直持有会话状态，直到你断开连接。它不会为其他人服务。`pg_stat_activity` 会列出这些进程：

```sql
select backend_type, count(*) from pg_stat_activity group by 1 order by 2 desc;
```

```text
         backend_type         | count
------------------------------+-------
 client backend               |     3
 io worker                    |     3
 autovacuum launcher          |     1
 checkpointer                 |     1
 ...
```

每个 `client backend` 都对应一个连接。其余进程负责维护工作（`io worker` 进程执行异步 I/O，PostgreSQL 18+）。这种模型带来了崩溃隔离，也让操作系统可以分别调度每个连接；代价是每个连接都很重。

> [!NOTE] 来自 MySQL 的读者
>
> MySQL 为每个连接运行一个线程，并配有线程缓存；它的文档描述了 Linux 通常处理 500 到 1,000 个连接的情况。Postgres 则为每个连接派生一个完整进程。在 MySQL 上运行过 2,000 个直连的团队，在这里需要连接池代理，并把 `max_connections` 设得低得多（[Postgres 不是 MySQL](/11-not-mysql/#basics-chapter-11)）。

<span id="proof-that-a-connection-is-an-os-process" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-28-proof-that-a-connection-is-an-os-process" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：连接是操作系统进程的证据
>
> 在数据库主机上，`ps` 会把每个后端显示为一个独立的 `postgres` 进程；进程标题经过改写，会显示它服务的对象和正在做什么，例如 `postgres: app book 10.0.0.7(51234) idle`。用 `kill -9` 杀掉它并不等于关闭连接：postmaster 会把后端意外死亡当作崩溃，并连带重启所有其他会话，所以应使用 `pg_terminate_backend()` 结束会话。实验通过 `dblink` 打开第二个连接，并找到了它的进程。
>
> > [!EXAMPLE]- 动手验证 · lab `27-connections`
> >
> > ```sql
> > select dblink_connect('c1', 'dbname=' || current_database()) \g /dev/null
> > select p as c1_pid from dblink('c1', 'select pg_backend_pid()') t(p int) \gset
> > select lab.prove('a new connection gets its own backend process, with its own pid',
> >   :c1_pid <> pg_backend_pid()
> >   and (select backend_type from pg_stat_activity where pid = :c1_pid) = 'client backend');
> > select lab.prove('that backend is an operating system process named postgres',
> >   pg_read_file('/proc/' || :c1_pid || '/status') like 'Name:%postgres%');
> > ```
> >
> > [完整实验脚本](https://www.flagon.io/books/eight-kilobytes/lab/27-connections.sql)

### 内存：空闲时 1.5 MB，排序中途 109 MB {#memory-15-mb-idle-109-mb-mid-sort}

空闲连接很便宜，正在工作的连接则不然。每个后端都有不属于共享缓冲区的私有内存（[后端内存是私有的](/05-buffers/#backend-memory-is-private)）；在我们的实验服务器上，一个空闲后端在其内存上下文中大约占用 1.5 MB。把[`work_mem`](/43-glossary/#work_mem)提高到 256 MB 后，同一个后端对 2,000,000 行 `events` 的两列排序，使用了 109 MB，因为 `work_mem` 是按每个后端的每个排序或哈希节点分配的（[work_mem 是按节点计算的，而且可以看到](/05-buffers/#work_mem-is-per-node-and-you-can-see-it)）。同样的排序在默认 4 MB 下则会溢写到磁盘。

内存占用也会随着模式规模增大而增长。一个访问过 1,000 张小表的后端，内存从 1.5 MB 增长到约 14 MB，因为[目录缓存只会增长](/05-buffers/#the-catalog-caches-only-grow)。当表和分区达到数千个时，每个后端都可能稳定在几十 MB。

乘以 800。

<span id="one-backends-memory-idle-and-mid-sort" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-28-one-backends-memory-idle-and-mid-sort" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：一个后端的内存，从空闲到排序中途
>
> 在容器中通过 `pg_backend_memory_contexts` 和 `/proc/<pid>/status` 测量，`work_mem` 设置为 256 MB：
>
> ```text
> idle backend
>   memory contexts:   1.5 MB
>   RssAnon:          3232 kB    private memory in use
>
> same backend, sorting 2 million rows (work_mem = 256MB)
>   Sort Method: quicksort  Memory: 111653kB
>   RssAnon afterwards: 11496 kB    private memory still held
> ```
>
> 排序结束后，它会保留一部分已经申请的内存。你的数字会略有不同；[配置章节](/30-configuration/#work_mem-multiply-before-you-raise-it)会把 `work_mem` 按计划节点和连接数相乘。
>
> > [!EXAMPLE]- 动手验证 · lab `27-connections`
> >
> > ```sql
> > select lab.prove('an idle backend holds a few MB of private memory',
> >   :idle_ctx < 4 * 1024 * 1024 and :idle_anon < 10 * 1024);
> > select lab.prove('mid-sort, one backend used over 100 MB of private memory',
> >   :sort_kb > 100 * 1024);
> > select lab.prove('afterwards it keeps more private memory than when idle',
> >   :after_anon > :idle_anon);
> > select lab.prove('touching 1,000 tables grows a backend''s caches by more than 10 MB',
> >   :after_ctx - :before_ctx > 10 * 1024 * 1024);
> > ```
> >
> > [完整实验脚本](https://www.flagon.io/books/eight-kilobytes/lab/27-connections.sql)

### CPU：超过核心数后，连接会排队 {#cpu-past-the-core-count-connections-wait-in-line}

一个需要 2 ms CPU 时间的查询，在 800 个并发执行时仍然需要 2 ms。16 个核心可以推进 16 个查询，其余查询排队等待；与此同时，操作系统还要花 CPU 周期在它们之间切换。共享结构（锁管理器、缓冲区映射、WAL 插入）会随着活动后端增加而承受更多争用，因此连接数超过某个点后，争用增长得比吞吐量还快。

### 建立连接：重连的代价是 70 倍 {#setup-reconnecting-cost-a-factor-of-seventy}

建立一个连接意味着 TCP 握手、通常还包括 TLS、身份验证（SCRAM 自己就会增加两次往返）、派生进程、初始化后端，然后面对冷缓存。对 `users` 执行同一个主键查找，用 [pgbench](/29-benchmarking/#pgbench-is-already-installed)让 4 个客户端运行 5 秒，先使用持久连接，再按事务重连（`-C`）：

```text
tps = 38551.781280 (without initial connection time)
...
tps = 533.066200 (including reconnection times)
```

在没有 TLS 的本地 Unix 套接字上，代价相差 70 倍。在真实网络上，建立连接很容易就要 10 到 50 ms，远远超过一个 1 ms 查询的时间。它还会带来页读取开销：在[运行手册实验](/36-playbook/#the-proof-one-app-eight-changes)中，新连接上的一次插入读取了约 239 页，其中大多是为外键检查建立环境所需的系统目录页；使用连接池时只读取了 36 页。

少建立连接，保持它们打开，并在请求之间共享。

<span id="the-reconnect-benchmark-in-full" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-28-the-reconnect-benchmark-in-full" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：完整的重连基准测试
>
> ```bash
> # lookup.sql:  \set id random(1, 100000)
> #              select * from users where id = :id;
> pgbench -n -f lookup.sql -c 4 -T 5 scratch
> # -C: new connection per transaction
> pgbench -n -f lookup.sql -c 4 -T 5 -C scratch
> ```
>
> ```text
> latency average = 0.104 ms
> initial connection time = 6.117 ms
> tps = 38551.781280 (without initial connection time)
>
> latency average = 7.504 ms
> average connection time = 1.844 ms
> tps = 533.066200 (including reconnection times)
> ```
>
> 测量时间有噪声；重点是比例。
>
> > [!EXAMPLE]- 动手验证 · lab `27-connections`
> >
> > ```sql
> > -- 50 connect-query-disconnect cycles versus 50 lookups on one connection, via dblink
> > select lab.prove('a new connection per query is several times slower than reusing one',
> >   (select ms_per_query from setup_cost where how = 'reconnect')
> >   > 5 * (select ms_per_query from setup_cost where how = 'persistent'));
> > ```
> >
> > [完整实验脚本](https://www.flagon.io/books/eight-kilobytes/lab/27-connections.sql)

## 把 max_connections 提高到几千会造成伤害 {#max_connections-in-the-thousands-hurts}

`max_connections`（默认 100，修改需要重启）不是容量旋钮。它只是上限；合适的值应该阻止服务器接受超过自身处理能力的并发工作。把它提高到几千会在三个方面造成伤害：

- **内存。** 一台能稳定运行 200 个连接的服务器，在糟糕的一天会因 2,000 个连接开始交换，或触发 OOM killer；那正是每个连接都在执行重查询的一天。
- **由它决定大小的共享内存。** 例如，锁表的大小是 `max_locks_per_transaction` 乘以连接槽位数，即使连接空闲也要付出这份代价。
- **它掩盖了真正的问题。** 达到上限意味着应用没有使用连接池，在非数据库工作期间占着连接，或者泄漏了连接。提高上限只会消除症状，让病根继续存在。

对大多数服务器来说，把 `max_connections` 设为几百是合理的；如果应用需要更多连接，就在前面加连接池代理。

超过上限后，客户端会收到 `FATAL:  sorry, too many clients already`。给自己留一扇门：`superuser_reserved_connections`（默认 3）；从 PostgreSQL 16 开始，还有 `reserved_connections`，供具有 `pg_use_reserved_connections` 成员资格的角色使用，监控和管理角色正适合放在这里。

<span id="checking-the-defaults-and-reserving-a-slot-for-monitoring" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-28-checking-the-defaults-and-reserving-a-slot-for-monitoring" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：检查默认值，并为监控保留连接槽位
>
> `pg_settings` 保存每项设置的编译时默认值（`boot_val`），以及修改它需要什么条件（`context`；其中 `postmaster` 表示需要重启）。保留槽位是从 `max_connections` 中扣除的，而不是额外增加：默认值为 100、超级用户槽位为 3 时，普通角色只能得到 97 个槽位。为了在应用用完其余槽位时仍让监控保持连接：
>
> ```sql
> alter system set reserved_connections = 5;   -- restart required
> grant pg_use_reserved_connections to monitoring;
> ```
>
> > [!EXAMPLE]- 动手验证 · lab `27-connections`
> >
> > ```sql
> > select lab.prove('max_connections defaults to 100 and needs a restart',
> >   (select boot_val = '100' and context = 'postmaster' from pg_settings where name = 'max_connections'));
> > select lab.prove('superuser_reserved_connections defaults to 3',
> >   (select boot_val from pg_settings where name = 'superuser_reserved_connections') = '3');
> > select lab.prove('reserved_connections (16+) exists, default 0, with the pg_use_reserved_connections role',
> >   (select boot_val from pg_settings where name = 'reserved_connections') = '0'
> >   and exists (select 1 from pg_roles where rolname = 'pg_use_reserved_connections'));
> > ```
> >
> > [完整实验脚本](https://www.flagon.io/books/eight-kilobytes/lab/27-connections.sql)

## 在应用中并在数据库前面使用连接池 {#pool-in-the-app-and-in-front-of-the-database}

始终使用驱动程序或框架提供的连接池；每个请求建立一个连接，就会每次都重现 `pgbench -C` 的缓慢表现。但应用连接池会相乘：连接总数等于进程数乘以池大小，而自动扩缩容会在没有询问数据库的情况下改变进程数：

```text
  40 app pods x pool of 20  =  800 server connections
  ┌───────┐ ┌───────┐ ┌───────┐          ┌───────┐
  │ pod 1 │ │ pod 2 │ │ pod 3 │   ...    │pod 40 │
  │ 20 ▣  │ │ 20 ▣  │ │ 20 ▣  │          │ 20 ▣  │
  └───┬───┘ └───┬───┘ └───┬───┘          └───┬───┘
      └─────────┴────┬────┴──────────────────┘
                     ▼
            ┌──────────────────┐
            │  postgres        │  800 backends, 16 cores,
            │  max_conn = 1000 │  most of them idle or waiting
            └──────────────────┘
```

连接池代理，例如[PgBouncer](/43-glossary/#pgbouncer)，位于两者之间。客户端可以随意向它打开许多连接（代价很低：PgBouncer 是一个事件驱动进程，不会为每个客户端创建一个后端），而它只借出一组小而固定的服务器连接：

```text
  800 cheap client connections from 40 pods
                     ▼
            ┌──────────────────┐
            │    pgbouncer     │   queues clients, lends
            │ pool_size = 40   │   server connections
            └────────┬─────────┘
                     ▼
            ┌──────────────────┐
            │    postgres      │   40 busy backends
            └──────────────────┘
```

两者都使用。应用连接池避免了向连接池代理建立连接的代价；连接池代理则无论 pod 有多少，都限制真正到达数据库的连接数。如果你的托管服务提供承担 PgBouncer 作用的内置连接池代理，就打开它（[托管连接池代理要么固定会话，要么泄漏状态](/34-managed/#a-managed-pooler-either-pins-sessions-or-leaks-state)）。

## 事务池模式会破坏会话状态 {#transaction-pooling-breaks-session-state}

连接池代理必须决定什么时候可以把一个服务器连接交给另一个客户端，而这个决定也决定了应用可以做什么。

| 模式 | 归还服务器连接的时机 | 多路复用程度 | 会话功能 |
|---|---|---|---|
| 会话 | 客户端断开连接时 | 低 | 全部可用 |
| 事务 | 每个事务结束时 | 高 | 许多功能会失效 |
| 语句 | 每条语句之后 | 最高 | 多语句事务会失效 |

<span id="tbl-28-1"></span>

**Table 28-1.** 事务池模式会破坏会话状态


**会话池模式**是安全的，但几乎不能减少连接数。**事务池模式**才是最大的收益：客户端只在事务内占用服务器连接，因此 800 个大多空闲的客户端可以共享几十个连接。**语句池模式**禁止多语句事务，只适合范围很窄的工作负载。

事务池模式的代价是：同一个客户端连续发起的事务可能运行在不同的后端上。任何驻留在**会话**中的东西都会泄漏，或直接消失：

- **`SET`。** 普通 `SET statement_timeout` 会改变当前后端，接下来的客户端会继承这个设置；而你的下一个事务可能落到一个没有该设置的后端上。在事务内部使用 `SET LOCAL`，或者用 `alter role ... set ...` 设置角色默认值。[行级安全](/33-security/#basics-chapter-33)使用的租户变量也要同样小心：`set_config('app.tenant_id', $1, true)` 中的 `true` 表示事务本地。
- **会话咨询锁。** `pg_advisory_lock` 属于后端。使用 `pg_advisory_xact_lock`，它会在事务结束时释放。
- **驱动程序预备语句。** 大多数驱动程序会在协议层、某个后端上准备参数化查询；下一个事务可能落到从未见过这些语句的后端。PgBouncer 1.21 及更高版本会替你重新准备；如果使用较旧的连接池代理，或使用没有这项能力的托管连接池代理，就在驱动程序中关闭预备语句。
- **`LISTEN`。** 通知会发给执行监听的那个后端。把监听器放在专用的直连上。
- **存续时间超出事务的临时表**、`WITH HOLD` 游标、SQL 层面的 `PREPARE`，以及其他任何假定跨事务仍使用“我的连接”的功能。

> [!WARNING] 事务池模式的问题悄无声息
>
> 事务池模式下，会话级 `SET` 不会失败。它会成功，然后悄悄应用到另一个客户端的事务中。用普通 `SET` 设置的租户 ID 可能被下一个请求从同一个后端读到。切换连接池代理到事务模式前，审计每个 `SET`、`set_config(..., false)`、咨询锁和 `LISTEN`。

<span id="a-leaked-set-caught-in-the-act" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-28-a-leaked-set-caught-in-the-act" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：一个泄漏的 SET，当场抓获
>
> 客户端 A 执行普通 `SET statement_timeout = '7s'` 后结束。客户端 B 的下一个事务落到同一个后端，并继承了它从未请求过的 7 秒超时。事务范围版本的行为符合预期：`SET LOCAL` 和 `set_config(..., true)` 会在提交时消失。咨询锁也有同样的区别。用 `pg_advisory_lock` 获取的会话锁，在获取它的事务结束后仍会留在 `pg_locks` 中，由下一个拿到这个后端的客户端持有；`pg_advisory_xact_lock` 则会随事务释放。实验通过 `dblink`，让两个客户端在一个共享的服务器连接上运行。
>
> > [!EXAMPLE]- 动手验证 · lab `27-connections`
> >
> > ```sql
> > -- client A, on the shared server connection
> > select dblink_exec('server', 'set statement_timeout = ''7s''') \g /dev/null
> > -- client B, next transaction on the same backend
> > select lab.prove('a plain SET by one client stays on the backend for the next client',
> >   (select v from dblink('server', 'show statement_timeout') t(v text)) = '7s');
> > select lab.prove('SET LOCAL and set_config(..., true) reset at commit',
> >   (select v from dblink('server', 'show statement_timeout') t(v text)) = '0'
> >   and (select coalesce(v, '') from dblink('server',
> >          $$select current_setting('app.tenant_id', true)$$) t(v text)) = '');
> > select lab.prove('a session advisory lock outlives the transaction; an xact lock does not',
> >   (select count(*) from pg_locks where locktype = 'advisory'
> >      and pid = (select p from dblink('server', 'select pg_backend_pid()') t(p int))
> >      and objid = 1) = 1
> >   and (select count(*) from pg_locks where locktype = 'advisory' and objid = 2
> >           and pid = (select p from dblink('server', 'select pg_backend_pid()') t(p int))) = 0);
> > ```
> >
> > [完整实验脚本](https://www.flagon.io/books/eight-kilobytes/lab/27-connections.sql)

<span id="what-newer-pgbouncer-fixes-and-what-it-still-cant" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-28-what-newer-pgbouncer-fixes-and-what-it-still-cant" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：新版 PgBouncer 修复了什么，以及仍然做不到什么
>
> 截至 2026 年 10 月：
>
> - **协议层预备语句。** PgBouncer 1.21（2023 年 10 月）增加了跟踪机制，会在客户端落到的任意服务器连接上重新准备语句；只要 `max_prepared_statements` 非零就会启用。自 1.24（2025 年 1 月）起默认值为 200，因此默认开启。1.21 之前的通常建议是在驱动程序中关闭预备语句。
> - **已报告的参数。** PgBouncer 1.26（2026 年 9 月）默认跟踪 Postgres 报告给客户端的每个参数，尤其是 PostgreSQL 18+ 中的 `search_path` 和 14+ 中的 `default_transaction_read_only`。在较旧的 PgBouncer 上，或在 PostgreSQL 18 之前（当时它不会报告 `search_path`），客户端的 `SET search_path` 会泄漏给下一个使用该服务器连接的用户。Postgres 不报告的参数，例如 `statement_timeout` 和 `app.tenant_id` 之类的自定义设置，仍然会泄漏；继续使用 `SET LOCAL`。

## 根据核心数而不是用户数确定连接池大小 {#size-the-pool-from-cores-not-users}

连接池要达到最大吞吐量，所需大小比你想象的要小得多。运行中的后端要么占用一个核心，要么在等待磁盘或锁。工作集位于内存中时，超过每核心一个的后端只是在排队等待 CPU。如果查询在等待磁盘，每个核心多放几个连接可以让核心保持忙碌。一个广泛使用的起点来自经验而非理论：

```text
  connections = (cores x 2) + effective number of disks
```

对于使用网络存储的 16 核服务器，大约是 32 到 40 个活动连接。从这里开始测量：在贴近实际的负载下增大连接池，当吞吐量趋于平坦而延迟开始上升时停止。

从另一个方向用 Little 定律检查：忙碌连接数等于吞吐量乘以每个事务持有连接的时间。一个每秒处理 2,000 个请求的应用，每个请求持有连接 5 ms，平均会保持 2,000 x 0.005 s = 10 个忙碌连接。这样的负载只需要十几个到二十几个连接来应对突发，而不是几百个。（上面的 `pgbench` 运行按构造就符合这个定律：每秒 38,500 次查找、每次约 0.1 ms，正好对应它的 4 个客户端。）

Little 定律还说明了什么真正会使连接数膨胀：占着连接却不使用它。一个请求处理程序打开事务，调用支付 API 800 ms，然后提交；它占用连接的时间是 2 ms 数据库工作所需时间的 400 倍。慢速外部调用要放在事务之前或之后，绝不能放在事务内部。

连接池耗尽时，请求会等待。这是正确的：连接池前的短队列，胜过 Postgres 内部的同一条队列。给这个等待设置几秒钟的超时。

> [!TIP] 连接池大小是总数，不是每个进程的数量
>
> 先决定数据库应该拥有多少个服务器连接（比如 40 个），再把它分配给所有会建立连接的对象：应用 pod、工作进程、cron 作业、迁移运行器和监控。给 40 个 pod 各配置一个大小为 20 的连接池，你就会在无意间选择 800 个连接。

## 每个超时默认都是关闭的 {#every-timeout-defaults-to-off}

一个查询会永远运行，一个事务被遗忘，一个空闲连接占着槽位，一个客户端消失。Postgres 为这些情况各有一个超时。它们默认都为 0（禁用），也都可以按角色设置：

- **`statement_timeout`** 会取消运行过久的语句（SQLSTATE `57014`），但连接仍然存活。超过请求截止时间的语句是在浪费工作，并且还占着锁。
- **`lock_timeout`** 限制等待锁的时间；[迁移](/26-migrations/#rule-one-lock_timeout-and-retry)把它列为第一原则。把它设得比 `statement_timeout` 略短，可以把锁堆积变成快速报错。
- **`transaction_timeout`**（PostgreSQL 17+）会结束一个打开时间过长的事务所在的会话，无论事务正在忙还是处于空闲；它能抓住许多小语句被塞进一个超大事务的情况，而其他超时抓不住。
- **`idle_session_timeout`**（PostgreSQL 14+）会结束在事务外空闲的会话。连接池中的连接本来就会空闲，因此只对人工用户和临时工具的角色使用它，或者把它设得比连接池的空闲超时更长。

应用角色的基线设置：

```sql
alter role app set statement_timeout = '5s';
alter role app set lock_timeout = '3s';
alter role app set idle_in_transaction_session_timeout = '30s';
alter role app set transaction_timeout = '5min';   -- PostgreSQL 17+
```

`lock_timeout` 的值低于 `statement_timeout`，而空闲超时又远低于 `transaction_timeout`，因此最具体的超时会先触发，错误信息也会说明出了什么问题。迁移、回填和批处理作业应使用单独的角色，并设置更长的限制（或者在每个事务中用 `set local` 设置），这样应用基线就不会杀掉有意运行很久的作业。

### idle_in_transaction_session_timeout 最重要 {#idle_in_transaction_session_timeout-matters-most}

在所有超时中，这一个能避免最大的损害。它会结束停留在打开事务中的空闲会话。一个被遗留的事务（异常路径跳过了回滚、调试器暂停、脚本等待输入）会无限期持有锁，并阻止清理回收其快照之后产生的任何内容。被遗忘的 `begin` 就是这样变成表膨胀和迁移阻塞的。为每个应用角色设置它；30 秒是很好的默认值，最多几分钟。

它、`transaction_timeout` 和 `idle_session_timeout` 都属于 FATAL：会关闭整个连接，许多驱动程序只会报告连接已中断。把这当成正常事件；好的连接池会丢弃该连接并重新打开一个。

<span id="what-each-timeout-looks-like-when-it-fires" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-28-what-each-timeout-looks-like-when-it-fires" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：每个超时触发时是什么样子
>
> 三个会话，每个对应一种超时：
>
> ```sql
> set statement_timeout = '1s';
> select pg_sleep(2);
>
> set idle_in_transaction_session_timeout = '1s';
> begin; select 1;
> -- (wait two seconds)
> select 2;
>
> set transaction_timeout = '2s';
> begin; select pg_sleep(1); select pg_sleep(1.5);
> ```
>
> ```text
> ERROR:  57014: canceling statement due to statement timeout
> FATAL:  25P03: terminating connection due to idle-in-transaction timeout
> FATAL:  25P04: terminating connection due to transaction timeout
> ```
>
> 最后一种情况下，没有哪条语句运行缓慢，会话也从未空闲；只是总时长太长。`idle_session_timeout` 结束时会产生 `FATAL:  57P05: terminating connection due to idle-session timeout`。SQLSTATE（用 psql 的 `\set VERBOSITY verbose` 显示）就是服务器发送的内容；但服务器杀掉空闲会话后，要等客户端下次与它通信才能交付这个错误，所以许多驱动程序只会看到 SQLSTATE `08006`，即连接已中断。
>
> > [!EXAMPLE]- 动手验证 · lab `27-connections`
> >
> > ```sql
> > -- psql runs from the server (COPY ... FROM PROGRAM) so the lab can read what it printed
> > select lab.prove('idle_in_transaction_session_timeout: FATAL 25P03, terminating connection due to idle-in-transaction timeout',
> >   (select line from fatal where timeout = 'idle_in_transaction_session_timeout')
> >   = 'FATAL:  25P03: terminating connection due to idle-in-transaction timeout');
> > select lab.prove('transaction_timeout (17+): FATAL 25P04, even though no statement was slow and the session was never idle',
> >   (select line from fatal where timeout = 'transaction_timeout')
> >   = 'FATAL:  25P04: terminating connection due to transaction timeout');
> > select lab.prove('idle_session_timeout (14+): FATAL 57P05',
> >   (select line from fatal where timeout = 'idle_session_timeout')
> >   = 'FATAL:  57P05: terminating connection due to idle-session timeout');
> > ```
> >
> > [完整实验脚本](https://www.flagon.io/books/eight-kilobytes/lab/27-connections.sql)

### 一个失联客户端可以占住连接槽位数小时 {#a-dead-client-can-hold-its-slot-for-hours}

如果客户端崩溃或网络断开，后端会等待下一条永远不会到来的查询，同时占着连接槽位，甚至可能占着一个事务。TCP 保活会探测沉默的对端。默认值为 0，意思是“使用操作系统设置”；在 Linux 上，通常要等两个小时才发出第一次探测。把它们设置成几分钟。

<span id="keepalive-settings-and-the-query-whose-client-left" class="pgbasics-deep-dive-anchor" data-td-print-local></span><span id="basics-28-keepalive-settings-and-the-query-whose-client-left" class="pgbasics-deep-dive-anchor"></span>

> [!DETAILS]- 深入阅读：保活设置，以及客户端离开后的查询
>
> ```ini
> tcp_keepalives_idle = 60        # seconds of silence before the first probe
> tcp_keepalives_interval = 10    # seconds between probes
> tcp_keepalives_count = 6        # unanswered probes before giving up
> ```
>
> 反过来，如果后端正在为一个已经离开的客户端运行长查询，则由 `client_connection_check_interval` 处理（PostgreSQL 14+，Linux 和少数其他平台支持）；它会让运行中的查询定期检查客户端是否还在，若客户端已离开就取消查询。也要设置驱动程序的保活，这样应用会发现数据库连接已死亡，而不是一直挂住。

## 连接风暴会自我加剧 {#connection-storms-feed-themselves}

无服务器架构和激进的自动扩缩容会把数量相乘变成突发：几百个实例在几秒内启动，各自打开连接，失败后又同时重试。部署和故障切换也会这样；数据库变慢时同样如此：请求耗时变长，连接池扩大，更多连接到达，数据库变得更慢。

防御措施大致按价值排序：

1. **在前面放一个连接池代理。** 对无服务器架构来说这是必需的；使用平台提供的连接池代理，或自己运行一个，并留意事务模式的语义。
2. **严格限制连接池，并把最小大小设小。** 一个启动时急着打开 20 个连接的池，会把 100 个同时启动的实例变成 2,000 次连接尝试。
3. **使用带指数退避和抖动的重试。** 客户端同步重试，会把短暂过载变成长时间过载。
4. **连接池耗尽时快速失败。** 较短的获取超时会主动丢弃负载，而不是放大负载。
5. **逐步滚动部署**，让连接池分波次重连。

## 从服务器一侧统计连接 {#count-connections-from-the-servers-side}

遇到连接问题时，首先运行这条查询：

```sql
select usename, application_name, state, count(*)
  from pg_stat_activity
 where backend_type = 'client backend'
 group by 1, 2, 3
 order by 4 desc;
```

在每个连接字符串中设置 `application_name`，这样它就能标出连接的所有者。

然后读取各个状态。大量 `idle` 意味着连接池过大。任何 `idle in transaction` 都值得检查（[backend_xmin 会指出阻碍清理的事务](/31-observability/#backend_xmin-names-the-transaction-holding-vacuum-back)会显示持续了多久）。如果 `active` 经常超过核心数的两倍，前面带队列的小连接池很可能会同时改善吞吐量和延迟。

连接池配置是一份计划；`pg_stat_activity` 才是已经发生的事实。

## 本章要点 {#the-short-version}

- 每个连接都是拥有自己内存的操作系统进程：空闲时几 MB，设置较大的 `work_mem` 后在排序中途可能达到一百 MB 甚至更多。连接是稀缺资源，并非免费。
- 把 `max_connections` 保持在一两百左右。达到上限说明应用需要连接池，而不是更大的上限。为管理和监控预留槽位。
- 根据核心数和磁盘确定服务器连接总数（从核心数 x 2 加磁盘数附近开始，再进行测量），并把总数分给所有会建立连接的对象。
- 始终使用应用侧连接池；当许多进程或无服务器函数连接时，再加一个外部连接池代理。
- 事务池模式会破坏会话状态：使用 `SET LOCAL` 或 `set_config(..., true)`、事务范围的咨询锁，并为 `LISTEN` 使用直连。
- 绝不要让事务跨越缓慢的非数据库工作。
- 为每个应用角色设置 `statement_timeout`、`lock_timeout` 和 `idle_in_transaction_session_timeout`，并在 PostgreSQL 17+ 上设置 `transaction_timeout`（起步值分别为 5s、3s、30s 和 5min）。迁移和批处理作业使用单独的角色。
- 通过连接池代理、较小的最小连接池、较短的获取超时，以及带退避和抖动的重试来应对连接风暴。

## 延伸阅读 {#further-reading}

- [How Connections Are Established](https://www.postgresql.org/docs/18/connect-estab.html)，PostgreSQL 文档。关于每个连接一个进程模型的权威说明。
- [Connections and Authentication](https://www.postgresql.org/docs/18/runtime-config-connection.html) 和 [Client Connection Defaults](https://www.postgresql.org/docs/18/runtime-config-client.html)，PostgreSQL 文档。`max_connections`、保活和本章中的所有超时。
- [Analyzing the Limits of Connection Scalability in Postgres](https://techcommunity.microsoft.com/blog/adforpostgresql/analyzing-the-limits-of-connection-scalability-in-postgres/1757266)，Andres Freund，Microsoft，2020 年。空闲连接为何有代价，以及 PostgreSQL 14 修复了什么。
- [PgBouncer features](https://www.pgbouncer.org/features.html) 和 [configuration](https://www.pgbouncer.org/config.html)，PgBouncer 项目。池化模式兼容性表以及 `max_prepared_statements`。
- [PgBouncer changelog](https://www.pgbouncer.org/changelog.html)，PgBouncer 项目。检查你的版本修复了什么；1.21、1.24 和 1.26 分别改变了事务池模式。
- [About Pool Sizing](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing)，Brett Wooldridge，HikariCP wiki。支持小连接池的经典论证。
- [Number Of Database Connections](https://wiki.postgresql.org/wiki/Number_Of_Database_Connections)，PostgreSQL Wiki。核心数乘二这一出发点的来源。
