# 连接与会话候选规则

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

---

应用拿到一个 PostgreSQL connection 时，业务代码通常把它当作“数据库”。实际上，它是一组会改变 SQL 含义和失败方式的上下文：host/service 把流量送到某个 instance，database 决定 catalog 边界，role 决定权限，GUC 决定名称解析、时间展示与超时，连接池还可能让同一个 server connection 被多个 client request 复用。

因此，连接规约不能只检查 TCP 和认证成功。它必须同时约束**目标、身份、语义、预算与归因**。

## 6.2.1 连接上下文、超时与 `application_name` {#item-6-2-1}

本书把连接上下文拆成两层：

```text
连接前声明
  service / host / port / dbname / user
  connect_timeout / application_name / TLS policy

连接后验证
  current_database()
  session_user / current_user
  pg_is_in_recovery()
  current_setting(...)
  schema/model version
```

第一层表达意图，第二层证明意图落在了正确对象上。只做其中一层都不够：连接字符串写对了仍可能遇到 DNS、service 或 failover 配置错误；连接后只看 `SELECT 1` 又无法知道自己是谁、在哪个库、是否落到只读副本。

### 用 service name 固定目标身份

libpq service file 把一组连接参数绑定到一个稳定名称：

```ini
[pg36-admin]
host=<L1_HOST>
port=5436
dbname=pg36_shop
user=dbuser_dba
application_name=pg36-ch06
connect_timeout=5
options=-c statement_timeout=30s -c lock_timeout=5s
```

客户端以 `service=pg36-admin` 或 `PGSERVICE=pg36-admin` 连接。用户级 service file 默认为 `~/.pg_service.conf`，也可以用绝对路径 `PGSERVICEFILE` 指定；直接连接字符串中的同名参数又会覆盖 service file 值。因此 service 是集中声明，不是不可绕过的安全边界，gate 仍须检查运行事实。

自动化入口使用：

```bash
export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin

psql -X -w "service=$PGSERVICE application_name=pg36-ch06-review"
```

`-X` 不读取用户 `psqlrc`，避免本地宏、变量或 `SET` 改变脚本；`-w` 禁止在无人值守任务里退回交互密码提示。第 2 章的 [service file 示例](/labs/ch02/pg_service.conf.example) 没有 secret，密码应来自 mode `0600` 的 passfile 或组织 secret provider。Unix 上权限过宽的 password file 会被 libpq 忽略；这是一项客户端保护，不等于凭据已经完成轮换、审计和最小授权。

### 四类超时不是同一个旋钮

| 预算 | 控制的阶段 | 典型失败 |
|---|---|---|
| `connect_timeout` | 建立连接 | 网络、DNS、endpoint 不可达 |
| `statement_timeout` | 单条 statement 执行 | 查询/写入超过请求预算 |
| `lock_timeout` | 等待任意单次 lock acquisition | DDL/DML 被冲突锁长期阻塞 |
| `idle_in_transaction_session_timeout` | transaction 中无客户端活动 | 遗忘事务长期持锁或 snapshot |

本章实验值为 5 秒连接、30 秒 statement、5 秒 lock、60 秒 idle-in-transaction。它们是 L1 教学 workload 的默认，不是生产通用答案。在线请求、报表、批处理、migration 应分别从 SLO、锁风险和恢复方式推导预算。

`statement_timeout=0` 和 `lock_timeout=0` 表示禁用超时，不表示“立刻超时”。`lock_timeout` 若等于或大于 `statement_timeout`，通常没有独立触发机会。超时也不是资源隔离：30 秒内仍可能消耗大量 CPU/I/O，后续章节还要加入并发、连接数、work memory 和 workload routing。

### `application_name` 是归因键，不是认证身份

`application_name` 会出现在 `pg_stat_activity`，配置允许时也可进入日志。命名至少区分：

```text
product / component / workload / environment
```

实验脚本还加入 action，例如 `pg36-ch06-all`。生产 trace 应在应用侧把 request/trace ID 与 backend PID、`backend_start`、transaction/query start 和 dashboard 时间窗关联；不要把每个 request ID 都塞进一个高基数、长度受限的 `application_name`。

客户端可以自行声明这个值，所以它不能替代 `session_user`、证书身份或审计主体。它的作用是把等待、日志和指标归到正确 workload/owner；安全判断必须使用服务器验证的身份。

由此形成两条规则：

- `SAFE-CONN-001`：写入前验证 database、effective role、primary、`search_path` 和模型版本；
- `DEFAULT-SESS-001`：每类 workload 声明可归因 application name 与连接/statement/lock/idle transaction 预算。

## 6.2.2 时区、编码、`search_path` 与会话状态 {#item-6-2-2}

SQL 文本相同，不保证会话语义相同。下面这些 session state 都可能改变结果：

- `client_encoding` 决定客户端字节怎样转换为数据库字符；
- `TimeZone` 改变 `timestamptz` 的输入解释和输出展示；
- `DateStyle` 影响含歧义的日期文本；
- `search_path` 决定未限定 table、function、type 与 operator 名称解析；
- transaction isolation/read-only/deferrable 改变并发观察；
- role 与 row security 设置改变可见对象和数据；
- timeout、planner GUC 与 locale/collation 影响失败或计划选择。

本章默认固定：

```sql
SET client_encoding = 'UTF8';
SET TimeZone = 'UTC';
SET search_path = pg_catalog, shop;
SET statement_timeout = '30s';
SET lock_timeout = '5s';
SET idle_in_transaction_session_timeout = '60s';
```

UTC 是存储/接口默认，不妨碍 UI 按用户时区展示；UTF-8 是跨系统文本默认，不替代 collation 设计。若业务输入使用本地时区，接口必须同时携带 zone/offset 并测试 DST 重叠与跳跃，不能依赖 application server 的系统时区。

### `search_path` 是信任边界

`search_path` 不只用于缩短表名。PostgreSQL 也按它解析 function、type 和 operator；把某个可被不受信用户 `CREATE` 的 schema 放入 path，就等于信任该用户可以影响未限定名称的解析。

对 application session，本书采用：

```text
pg_catalog, shop
```

同时从 `public` 撤销 PUBLIC CREATE。需要注意版本和升级历史：PostgreSQL 15 新建数据库的默认权限与从 PostgreSQL 14 或更早升级的数据库可能不同，不能靠“大版本默认应该安全”代替 catalog 检查：

```sql
SELECT has_schema_privilege('public', 'CREATE');
```

对 `SECURITY DEFINER` function 要更严格：只保留可信 schema，把 `pg_temp` 放到最后或明确排除不可信解析路径，敏感对象使用 schema-qualified name，并在创建 function 的同一事务中 `REVOKE ALL ... FROM PUBLIC` 后按需 `GRANT EXECUTE`。这是 `SAFE-DEFR-004` 的安全边界，第 4 章已有 catalog 反证。

### 会话默认与每次请求声明

配置可以有多个层次：

```text
postgresql.conf / ALTER SYSTEM
  < ALTER DATABASE / ALTER ROLE
  < ALTER ROLE ... IN DATABASE
  < startup options / connection parameters
  < session SET
  < transaction SET LOCAL
```

具体生效值应由 `current_setting()` 或 `pg_settings` 观察，不应只查看某一层配置文件。对稳定的 database/role 默认，可由 Pigsty `pg_databases.parameters` 或版本化 SQL 声明；对单次事务预算，优先在 transaction 开始后 `SET LOCAL`，让它随 commit/rollback 自动恢复。

在 PgBouncer transaction pooling 下，client session 与 PostgreSQL backend 不是永久一一对应。应用不能假设上一请求的 `SET`、临时对象、prepared statement 或 session lock 会在下一事务仍然存在，也不能让状态泄漏给后续请求。需要 session affinity 的 workload 应选择 session pooling 或 direct service，并把理由写进 waiver；普通 OLTP 则应把事务所需状态显式放进 startup/role default 或每个 transaction。

因此 `DEFAULT-CONT-002` 的真正要求不是“所有地方硬编码同一串 SET”，而是：

1. 定义 canonical session profile；
2. 选择一个可重复应用的层次；
3. 在取得连接后验证关键语义；
4. 对 pool reuse 不做隐式假设；
5. 在 evidence 中保存实际值。

## 6.2.3 用错误连接案例验证规则价值 {#item-6-2-3}

只有正例的 gate 可能永远绿色，即使检查本身已经失效。本章提供两个故意失败的 fixture：

### 错误会话

[`wrong-session.sql`](/labs/ch06/wrong-session.sql) 主动设置：

```sql
SET TimeZone = 'Asia/Shanghai';
SET search_path = public;
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
```

然后要求 baseline 以自定义 SQLSTATE `P0601` 拒绝。gate 不是笼统检查“命令失败”，而是同时断言：

```text
psql exit = 3
stderr 中恰好一个 ERROR: P0601
```

若脚本因为语法错误、认证失败或其他 SQLSTATE 退出，negative gate 仍失败；否则一个与规则无关的故障也会被误报为“安全控制成功”。

### 错误目标

[`session-profile.sql`](/labs/ch06/session-profile.sql) 默认期待 `pg36_shop`。negative action 将 expected database 改为一个确定不存在于合同中的名字：

```bash
psql ... \
  --set=expected_db=definitely_not_pg36_shop \
  --set=VERBOSITY=sqlstate \
  --file=session-profile.sql
```

[`context.sql`](/labs/ch06/context.sql) 在任何业务读取前拒绝，预期 exit 3、SQLSTATE `P0001`。这证明 target guard 确实参与路径，而不是写在文件里却从未被调用。

### 正确会话

`quality-gate.sh live` 运行相同 profile，要求输出：

```text
status=ok
database=pg36_shop
effective_role=pg36_owner
application_name=pg36-ch06-<action>
client_encoding=UTF8
timezone=UTC
search_path=pg_catalog, shop
statement_timeout=30s
lock_timeout=5s
idle_in_transaction_session_timeout=1min
```

注意输出格式可能把 `60s` 规范化为 `1min`；验收 SQL 用 `::interval` 比较语义，不比较展示文本。类似地，host、PID、server version 和 timestamp 是本次证据，不应做 golden value。

### 仍然没有证明什么

这个 gate 证明检查时刻的 PostgreSQL session 与 ch04-v1 模型符合预期，但没有证明：

- TLS、证书和 HBA 满足生产安全策略；
- 所有应用连接都使用同一个 profile；
- HAProxy endpoint 在 failover 后仍按预期路由；
- passfile/secret provider 的生命周期与轮换正确；
- 30 秒预算适合真实 SLO；
- PgBouncer reset 与 driver 行为没有其他差异。

这些边界必须明确写出，否则一个绿色实验会被错误扩大为生产认证。正确做法是把相邻控制接入同一证据链，而不是让单个脚本背负它无法观察的结论。

### 运行本节验证

静态检查不连接数据库：

```bash
cd static/labs/ch06
./quality-gate.sh static
```

在已经确认的 ch04-v1 L1 上运行 session 正反例：

```bash
export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin
export PG36_EVIDENCE_DIR="$PWD/evidence/ch06/session-$(date -u +%Y%m%dT%H%M%SZ)"

./quality-gate.sh live
./quality-gate.sh negative
```

检查 `session-profile.txt`、`negative-summary.txt` 和各自 stderr。只有正确上下文通过、两个错误上下文按精确原因失败，连接规约才同时拥有正向和负向证据。

## 参考资料

- [PostgreSQL 18：The Connection Service File](https://www.postgresql.org/docs/18/libpq-pgservice.html)
- [PostgreSQL 18：The Password File](https://www.postgresql.org/docs/18/libpq-pgpass.html)
- [PostgreSQL 18：Database Connection Control Functions](https://www.postgresql.org/docs/18/libpq-connect.html)
- [PostgreSQL 18：Client Connection Defaults](https://www.postgresql.org/docs/18/runtime-config-client.html)
- [PostgreSQL 18：Statement Behavior](https://www.postgresql.org/docs/18/runtime-config-client.html#RUNTIME-CONFIG-CLIENT-STATEMENT)
- [PostgreSQL 18：Schemas and `search_path`](https://www.postgresql.org/docs/18/ddl-schemas.html)
- [PostgreSQL 18：Reporting and Logging](https://www.postgresql.org/docs/18/runtime-config-logging.html)

---

[上一节：规约不是口号](../01/) · [返回本章目录](../) · [下一节：模式与 DDL 候选规则](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
