# 从连接串识别操作落点

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

---

一条连接字符串表达的是客户端的连接意图，不是服务器的自我证明。主机名可能经过 DNS 或 VIP，端口可能属于代理，登录角色还可能在会话内切换。可靠的第一步不是看到提示符就开始执行，而是把“我打算连到哪里”与“服务器说我落在哪里”对上。

本节全部操作属于 `R0·观察`。请使用第 0 章提供的实验凭据，不要把密码写入命令历史、书稿或 Git。`pg36_shop` 尚未创建，因此先用 Pigsty L1 已有的管理数据库观察；将 `<L1_HOST>` 替换为实际域名或 IP：

```bash
export PG36_BOOTSTRAP_URL='postgresql://dbuser_dba@<L1_HOST>:5436/postgres?application_name=pg36-ch01'
psql -X "$PG36_BOOTSTRAP_URL"
```

`-X` 表示暂不读取个人 `psqlrc`，避免本地定制改变示例行为。安全保存凭据、服务文件和连接保护会在 ch02《psql 与可复现工作流》中展开。

## 1.1.1 主机、端口、服务、数据库与角色 {#item-1-1-1}

全书最终要交给应用的是类似下面的 URI。此刻先把它当作待解释的目标，而不是可以立即连接的成品：

```text
postgresql://pg36_app@pg-meta:5433/pg36_shop?application_name=pg36-ch01
             └──角色──┘ └主机─┘└端口┘└─数据库──┘ └────连接参数─────┘
```

| 部分 | 它回答的问题 | 由谁解释 | 不能据此断言什么 |
|---|---|---|---|
| `pg-meta` | 客户端先去哪里建立网络连接？ | 客户端 DNS、`/etc/hosts`、Unix socket 或地址列表 | 它不一定是一台固定主机，也不证明最终 PostgreSQL 实例 |
| `5433` | 目标主机上的哪个 TCP 入口？ | 监听该端口的进程或代理 | 它不一定是 PostgreSQL；在 Pigsty 中通常是 HAProxy 读写服务 |
| `pg36_shop` | 认证成功后进入哪个数据库？ | PostgreSQL | 它不是模式、实例或集群名 |
| `pg36_app` | 以哪个数据库角色发起认证？ | PostgreSQL 认证规则 | 它不必与 Linux 用户同名，也不等于对象所有者 |
| `application_name` | 这条连接在活动视图和日志中叫什么？ | 客户端传入，PostgreSQL 记录 | 它是可伪造标签，不是安全身份 |

URI 支持 `postgresql://` 和 `postgres://` 两种 scheme。用户名、密码或数据库名含有 `@`、`:`、`/`、`?`、`#` 等保留字符时必须进行百分号编码。更重要的是，不要为了省事把密码直接写入可被 shell 历史、进程列表或日志记录的 URI；本章让 `psql` 交互式询问密码。

“服务”在这里是平台语义，而不是 URI 中额外的一段。它通常由“可访问的主机或域名 + 端口 + 路由规则”共同构成。Pigsty 的 `pg-meta:5433` 是读写服务入口；同样的 `pg-meta` 配上 `5432`，通常变成对当前 VIP 所在节点的 PostgreSQL 直连。端口改变，路径与故障语义也随之改变。

连接成功后，先执行一份上下文快照：

```sql
SELECT
    version()                         AS server_version,
    current_database()               AS database_name,
    session_user                     AS session_user,
    current_user                     AS current_user,
    inet_server_addr()                AS server_addr,
    inet_server_port()                AS server_port,
    pg_backend_pid()                  AS backend_pid,
    pg_is_in_recovery()               AS in_recovery;
```

关键判断不是输出长什么样，而是每一列证明了什么：

- `version()` 来自服务端，可以揭示服务器版本与构建信息；它不等于本机 `psql --version`；
- `inet_server_addr()` 和 `inet_server_port()` 是 PostgreSQL 后端接受连接的地址与端口。经过 HAProxy、PgBouncer 后，它们通常显示最后一跳 PostgreSQL 的地址与 `5432`，而不是客户端最初访问的 `5433`；
- 通过 Unix socket 连接时，`inet_server_addr()` 和 `inet_server_port()` 会是 `NULL`，这不是故障；
- `pg_backend_pid()` 是当前 PostgreSQL 后端进程号，只在该实例当前生命周期内有意义；
- `pg_is_in_recovery()` 为 `false` 表示当前实例不在恢复状态，通常是可写主库；为 `true` 表示处于恢复或热备状态。它不单独证明整套高可用系统健康。

客户端意图与服务器证据必须同时保留。只记 URI，会丢失实际落点；只记 SQL 输出，又会丢失客户端究竟通过哪个入口到达。

## 1.1.2 `current_database()`、`current_user` 与 `search_path` {#item-1-1-2}

进入服务器以后，还要确认三个会直接改变 SQL 含义的上下文：当前数据库、当前角色与模式搜索路径。

```sql
SELECT
    current_database()       AS database_name,
    session_user             AS authenticated_as,
    current_user             AS effective_as,
    current_setting('search_path') AS configured_path,
    current_schemas(true)    AS effective_path;
```

`current_database()` 返回当前连接所在数据库。PostgreSQL 的一个普通会话一次只连接一个数据库；`\c` 看似在会话内“切库”，实际是 `psql` 断开后重新建立连接。数据库之间不是类似 MySQL `database.table` 那样可以随意跨库限定访问的命名空间。

`session_user` 是最初通过认证的角色，通常在连接期间保持不变；`current_user` 是当前权限检查使用的有效角色。执行 `SET ROLE` 或进入使用 `SECURITY DEFINER` 的函数时，两者可能不同：

```sql
SELECT session_user, current_user;
-- 只有在当前角色有权切换时才能执行：
SET ROLE pg36_owner;
SELECT session_user, current_user;
RESET ROLE;
```

因此，审计“谁连进来”时看 `session_user`，判断“当前 SQL 以谁的权限运行”时看 `current_user`。两者都不等于操作系统账号。

`search_path` 决定没有写模式限定符的对象名如何解析，也决定未显式指定模式时新对象创建在哪里。假设有效路径是：

```text
pg_catalog, shop
```

那么系统对象优先从 `pg_catalog` 解析，业务对象再从 `shop` 查找。`current_setting('search_path')` 返回配置文本；`current_schemas(true)` 返回去除不存在或不可访问项后的有效路径，并按参数决定是否包含隐含的系统模式。

不要把 `search_path` 当成界面便利设置。若不可信用户可以在搜索路径靠前的模式中创建对象，未限定名称的函数或操作符可能解析到攻击者提供的对象。应用与迁移脚本应采用受控路径，安全敏感 SQL 则显式写出模式名，例如 `pg_catalog.set_config(...)` 或 `shop.orders`。

本书为运行角色约定：

```sql
ALTER ROLE pg36_app IN DATABASE pg36_shop
SET search_path = pg_catalog, shop;
```

这条语句是 `R1·可逆变更`，只对角色 `pg36_app` 连接数据库 `pg36_shop` 时生效。回退方法是：

```sql
ALTER ROLE pg36_app IN DATABASE pg36_shop RESET search_path;
```

执行位置、权限与对象创建将在 1.7 一并处理。

## 1.1.3 实例端点、服务端点与只读端点 {#item-1-1-3}

端点可以指向固定实例，也可以表达一种稳定服务意图。两者都能建立连接，但承诺不同。

| 入口类型 | Pigsty v4.5 默认示例 | 典型路径 | 适合做什么 | 隐含假设 |
|---|---|---|---|---|
| PostgreSQL 实例直连 | `pg-meta-1:5432` | 客户端 → PostgreSQL | 本地管理、精确诊断单一实例 | 实例身份不会自动随故障切换变化 |
| PgBouncer 实例直连 | `pg-meta-1:6432` | 客户端 → PgBouncer → PostgreSQL | 精确访问某实例上的连接池 | 仍绑定固定实例 |
| primary 服务 | `pg-meta:5433` | 客户端 → HAProxy → 主库 PgBouncer → PostgreSQL | 应用读写 | 平台会根据当前角色路由到主库 |
| replica 服务 | `pg-meta:5434` | 客户端 → HAProxy → 备库 PgBouncer → PostgreSQL | 可容忍复制延迟的读取 | 没有合格备库时可能按配置回退 |
| default 服务 | `pg-meta:5436` | 客户端 → HAProxy → 主库 PostgreSQL | 管理、迁移、需要会话语义的直连 | 绕过连接池，但仍跟随主库 |

这些是 Pigsty 的默认配置，不是 PostgreSQL 标准端口；用户可以修改。`5432` 才是 PostgreSQL 常见默认端口，`6432` 是 PgBouncer 常见默认端口。

最容易犯的错误，是把“replica 服务”理解成数据库层面的强制只读。服务名首先表达路由策略，不等同于授权策略。在单节点 L1 中没有专用备库，replica 服务可能没有可用后端，或者按具体配置回退到主库；即使连接到了备库，未来故障切换也可能改变承载实例。应用是否有写权限，仍应由角色授权、事务只读属性与数据库策略共同约束。

每次需要判断读写能力时，至少采集：

```sql
SELECT
    pg_is_in_recovery()                    AS in_recovery,
    current_setting('transaction_read_only')::boolean AS transaction_read_only,
    has_database_privilege(
        current_user,
        current_database(),
        'CREATE'
    )                                     AS can_create_in_database;
```

三个结果分别回答“实例是否在恢复”“当前事务是否只读”“角色是否拥有数据库级 CREATE 权限”，它们不是同一个问题。`has_database_privilege` 也不能穷举写入能力：表级权限、行级安全策略、函数权限和对象所有权仍可能改变结果。

### 一个两端互证练习

分别通过实例直连与 primary 服务建立连接，运行相同快照，并对比客户端入口与后端证据：

```bash
export PG36_INSTANCE_URL='postgresql://dbuser_dba@<INSTANCE_HOST>:5432/postgres?application_name=pg36-ch01-instance'
export PG36_PRIMARY_URL='postgresql://dbuser_dba@<L1_HOST>:5433/postgres?application_name=pg36-ch01-primary'

psql -X "$PG36_INSTANCE_URL" -c \
  "SELECT inet_server_addr(), inet_server_port(), pg_backend_pid(), pg_is_in_recovery();"

psql -X "$PG36_PRIMARY_URL" -c \
  "SELECT inet_server_addr(), inet_server_port(), pg_backend_pid(), pg_is_in_recovery();"
```

在单节点环境中，两次查询可能落到同一个 PostgreSQL 实例，但路径仍不同；在高可用环境中，primary 服务应随主库角色变化，而固定实例端点不会。不要为了让示例输出与书中一致而忽略差异，把实际结果写进环境清单。

### 本节验收

关闭终端前，确认你能回答：

- URI 中哪个字段选择数据库角色，哪个字段选择数据库？
- 为什么访问 `5433` 后，`inet_server_port()` 常常仍返回 `5432`？
- `current_user` 在什么情况下会与 `session_user` 不同？
- replica 服务、恢复状态、事务只读和角色权限为什么是四个不同判断？

若任何一个答案仍依赖“端口名字看起来像……”，重新执行上下文快照，用查询结果作答。

## 参考资料

- [PostgreSQL 18：libpq 连接字符串](https://www.postgresql.org/docs/18/libpq-connect.html#LIBPQ-CONNSTRING)
- [PostgreSQL 18：系统信息函数](https://www.postgresql.org/docs/18/functions-info.html)
- [PostgreSQL 18：模式与搜索路径](https://www.postgresql.org/docs/18/ddl-schemas.html#DDL-SCHEMAS-PATH)
- [Pigsty v4.5：服务与接入](https://pigsty.io/docs/pgsql/service/)

---

[返回本章目录](../) · [下一节：PostgreSQL 对象与术语坐标](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
