1.1 从连接串识别操作落点
一条连接字符串表达的是客户端的连接意图,不是服务器的自我证明。主机名可能经过 DNS 或 VIP,端口可能属于代理,登录角色还可能在会话内切换。可靠的第一步不是看到提示符就开始执行,而是把“我打算连到哪里”与“服务器说我落在哪里”对上。
本节全部操作属于 R0·观察。请使用第 0 章提供的实验凭据,不要把密码写入命令历史、书稿或 Git。pg36_shop 尚未创建,因此先用 Pigsty L1 已有的管理数据库观察;将 <L1_HOST> 替换为实际域名或 IP:
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 主机、端口、服务、数据库与角色
全书最终要交给应用的是类似下面的 URI。此刻先把它当作待解释的目标,而不是可以立即连接的成品:
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 直连。端口改变,路径与故障语义也随之改变。
连接成功后,先执行一份上下文快照:
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
进入服务器以后,还要确认三个会直接改变 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 的函数时,两者可能不同:
SELECT session_user, current_user;
-- 只有在当前角色有权切换时才能执行:
SET ROLE pg36_owner;
SELECT session_user, current_user;
RESET ROLE;因此,审计“谁连进来”时看 session_user,判断“当前 SQL 以谁的权限运行”时看 current_user。两者都不等于操作系统账号。
search_path 决定没有写模式限定符的对象名如何解析,也决定未显式指定模式时新对象创建在哪里。假设有效路径是:
pg_catalog, shop那么系统对象优先从 pg_catalog 解析,业务对象再从 shop 查找。current_setting('search_path') 返回配置文本;current_schemas(true) 返回去除不存在或不可访问项后的有效路径,并按参数决定是否包含隐含的系统模式。
不要把 search_path 当成界面便利设置。若不可信用户可以在搜索路径靠前的模式中创建对象,未限定名称的函数或操作符可能解析到攻击者提供的对象。应用与迁移脚本应采用受控路径,安全敏感 SQL 则显式写出模式名,例如 pg_catalog.set_config(...) 或 shop.orders。
本书为运行角色约定:
ALTER ROLE pg36_app IN DATABASE pg36_shop
SET search_path = pg_catalog, shop;这条语句是 R1·可逆变更,只对角色 pg36_app 连接数据库 pg36_shop 时生效。回退方法是:
ALTER ROLE pg36_app IN DATABASE pg36_shop RESET search_path;执行位置、权限与对象创建将在 1.7 一并处理。
1.1.3 实例端点、服务端点与只读端点
端点可以指向固定实例,也可以表达一种稳定服务意图。两者都能建立连接,但承诺不同。
| 入口类型 | Pigsty v4.4 默认示例 | 典型路径 | 适合做什么 | 隐含假设 |
|---|---|---|---|---|
| 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 服务可能没有可用后端,或者按具体配置回退到主库;即使连接到了备库,未来故障切换也可能改变承载实例。应用是否有写权限,仍应由角色授权、事务只读属性与数据库策略共同约束。
每次需要判断读写能力时,至少采集:
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 服务建立连接,运行相同快照,并对比客户端入口与后端证据:
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 服务、恢复状态、事务只读和角色权限为什么是四个不同判断?
若任何一个答案仍依赖“端口名字看起来像……”,重新执行上下文快照,用查询结果作答。