跳至内容

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_usersearch_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 服务、恢复状态、事务只读和角色权限为什么是四个不同判断?

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

参考资料


返回本章目录 · 下一节:PostgreSQL 对象与术语坐标 · 查看全书目录 · 查看索引中心

最后更新于