平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“PostgreSQ数据库连接失败报错的问题排查与实践修复教学”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。
1. 先看懂这条报错到底在说什么
connection to server at "1", port 5432 failed 这行字,几乎每个用 pgsql 的人都会在不同阶段遇到一次。它的信息量其实比大多数人以为的要大得多: at "1" 告诉你客户端解析到的目标主机字符串就是字面上的 1 , port 5432 告诉你端口用的是默认值, failed 后面紧跟的那半截 "postgres" P... 才是真正的病根所在。我第一次遇到这个报错时,第一反应是去看防火墙,结果折腾了半小时才发现是连接串里一个环境变量没被替换,host 真的被写成了 1 。这篇文章就想把这类排错的完整链路讲透:从报错文本怎么读、六层关卡怎么逐层验证,到容器、认证算法、连接数这些高频现场,再到我踩过的那几个反直觉的坑。不管你是刚装完数据库在本地试连的初学者,还是在维护一套多环境、带连接池的生产系统的运维,都能在下面的排查路径里找到对应的那一段。
1.1 报错文本的逐段拆解
落到代码里,把这条报错按标点切开,其实是 libpq 在"握手失败"这一层的标准模板: connection to server at "。前半句是客户端视角的连接目标,后半句才是服务端或网络给出的具体原因。很多人只盯着 failed 三个字慌,却忽略了前面引号里的内容—— 引号里的 host 字符串是客户端亲眼看到的,不是服务端告诉它的 。当它显示为 "1" 这种明显不像主机名的东西时,基本能够先排除网络和服务端设置,直接怀疑客户端侧的设置来源:环境变量、设置文件、连接串拼接模板、容器编排里的占位符。
再看 port 5432 。如果这里不是你预期的端口,比如你明明把库改到了 5433 或者容器映射成了 15432,那问题同样在客户端侧,而不是服务端在拒你。我见过一个典型情况:docker-compose 里写的是 15432:5432 ,应用设置里也跟着写了 15432,但本地命令行调试时用了默认 psql -h 127.0.0.1 ,于是端口对不上,报错却长得几乎一样。
1.2 被截断的那个 "P" 到底可能是什么
原始报错尾部只留下了 "postgres" P ,这个 P 大概率是两个高频短语的开头: Password authentication failed for user "postgres" ,或者 Permission denied 。前者属于认证层失败,说明网络是通的、端口是通的、pg_hba.conf 也放行了,仅仅是你给的用户名密码组合不对,或者密码的存储算法和客户端的应答算法对不上;后者更像是系统层的问题,比如 Unix socket 目录权限、证书文件读不到、备份文件没权限这类。
区分方法很直接:把完整报错抓全。命令行场景下别只截最后一行,让 psql 把 FATAL 行完整打出来;应用场景下把驱动抛出的原始异常完整打日志,不带任何 str(e)[:50] 之类的截断。 报错被截断是排障成本的最大来源之一 实际处理时,,我后来在所有项目的数据库初始化代码里都把连接异常完整打印同时加上目标 host/port/dbname,就为了不再靠猜。
1.3 从客户端到 postgres 进程要过六道关卡
结合项目来看,一条连接从发起走到真正执行 SQL,中间至少要穿过六道关卡,任何一道没过,最后呈现给用户的都是类似 connection failed 的壳子。第一道是地址解析,域名或 IP 能不能变成可达地址;第二道是网络可达,路由、安全组、防火墙放不放行;第三道是端口,postgres 进程到底有没有在那个地址和端口上 listen;第四道是 pg_hba.conf 的规则匹配,客户端来源有没有被允许;第五道是认证方式与凭据,密码算法对不对、密码对不对;第六道是数据库与角色本身,库存不存在、角色有没有 CONNECT 权限、连接数有没有超。
这六层是由外向内逐层收敛的, 排查顺序必须从外往里,不要跳 。我见过有人一上来就改 pg_hba.conf ,加了 host all all 0.0.0.0/0 scram-sha-256 ,结果真正的原因是安全组没开,改完还是连不上,反而把权限放宽了,这是典型的跳层排错。
| 关卡 | 排查对象 | 典型报错片段 | 验证手段 |
|---|---|---|---|
| 1 地址解析 | host 字符串、DNS、hosts | could not translate host name | getent hosts 、 nslookup |
| 2 网络可达 | 路由、安全组、防火墙 | Connection timed out | nc -vz 、 ping |
| 3 端口 | listen_addresses、port | Connection refused | ss -lntp 、 pg_isready |
| 4 规则匹配 | pg_hba.conf | no pg_hba.conf entry for host | pg_hba_file_rules 视图 |
| 5 认证 | 密码算法、密码值 | password authentication failed | 重设密码、 password_encryption |
| 6 库与角色 | 库存在性、权限、连接数 | database does not exist 、 too many clients | pg_stat_activity 、 \l 、 \du |
2. 逐层排查:从网络层到认证层的完整路径
2.1 第一关:客户端看到的 host 到底是不是你想的
先解决 "1" 这个怪东西。绝大多数情况下它不是笔误,而是 变量替换失败后的残留 。比如连接串模板写成 postgresql://postgres:${DB_PASS}@${DB_HOST}:5432/${DB_NAME} ,而 DB_HOST 在某个环境里没注入,某些 shell 会把未定义变量展开成空串,于是地址变成 @:5432 ;但如果模板写成了 host='$DB_HOST' 又叠加了一层引号转义,或者用某个模板引擎把 $DB_HOST 整个吃掉只剩了个索引数字,就会出现这种看着像数字的 host。
排查动作很轻松,三步:第一步,在报错的那台机器上直接把最后生效的连接参数打印出来(这里说的是打印参数本身,不是打印密码);第二步,用 env | grep -i pg 看有没有 PGHOST 、 PGPORT 、 PGDATABASE 、 PGUSER 之类环境变量在悄悄覆盖你的显式参数;第三步,确认没有 ~/.pg_service.conf 、 ~/.pgpass 、 PGSERVICEFILE 这些隐藏设置在生效。
# 看环境变量有没有偷偷覆盖连接参数
env | grep -i '^PG'
# 显式确认最终使用的 host 字符串(不打印密码)
psql "host=1 port=5432 dbname=postgres user=postgres connect_timeout=5" -c 'select 1'
psql 的参数优先级是"显式参数 > 环境变量 > 默认值",但连接 URI 形式的优先级规则略有不同,URI 里出现的项优先生效。当你混用 -h 和环境变量时,最容易出这种鬼。我的做法是 在脚本里永远用完整的连接串形式,不用分散的短参数 ,一眼就能看出目标是什么。
另外一个常被忽略的点是 localhost 的解析。很多系统上 localhost 会先解析到 ::1 (IPv6),如果服务端 listen_addresses 只写了 '127.0.0.1' ,就会出现"ping 得通、telnet 不通"的怪现象。遇到 Connection refused 而服务又明明在跑,一定要检查是不是 IPv4/IPv6 二选一的问题。
2.2 第二关:网络通不通,别用 ping 判断
ping 通不通和 5432 通不通是两件独立的事。ICMP 被禁而 TCP 放行是常态,反过来也常用。真正有意义的验证是直接对目标端口 做 TCP 探测。
# 目标端口连通性
nc -vz 10.0.0.12 5432
# 没有 nc 的环境用 bash 原生探活
timeout 3 bash -c 'cat < /dev/null > /dev/tcp/10.0.0.12/5432' && echo open || echo closed
如果 nc 显示 Connection refused ,说明包到了目标主机,但目标端口没有,问题在服务端;如果显示 timed out ,说明包在中途被丢,问题在路由、安全组或主机防火墙;如果显示 No route to host ,那基本是网络层不通,先找网络同学,别在数据库上耗时间。
注意: Connection refused 和 timeout 是两个完全不同的信号,前者说明"目标在线但不打算理你",后者说明"信号发出去没回音"。把这两个混为一谈是多花半天时间的经典原因。
理解这一步时,在自建环境里,firewalld、ufw、nftables 都可能拦住 5432。放行时请只针对业务网段开放,不要图省事全开。云上环境还要额外确认安全组是双向生效的:入方向放行了不代表出方向没问题(虽然默认多数是全放行,但收紧过的安全组会翻车)。
2.3 第三关:postgres 到底在哪个地址
从实现思路看,这一层是新手翻车率最高的地方。PostgreSQL 默认设置很保守: listen_addresses = 'localhost' ,只回环地址。这意味着同机的 psql 能连,别的机器一个都连不上。
# 看谁在听 5432
ss -lntp | grep 5432
# 期望看到 0.0.0.0:5432 或 *:5432;如果只看到 127.0.0.1:5432 就说明只了回环
# 更语义化的探活
pg_isready -h 10.0.0.12 -p 5432 -U postgres -d postgres
改法是在 postgresql.conf 里把 listen_addresses 调成 '*' 或具体的网卡地址:
listen_addresses = '*'
port = 5432
注意: listen_addresses 属于需重启(restart)才生效的参数, pg_ctl reload 是不够的。这一点很多人记混,改完 reload 之后发现 ss 输出没变,就以为改错了文件。改完能够用 SELECT name, setting, pending_restart FROM pg_settings WHERE name = 'listen_addresses'; 确认是不是处于"待重启"状态。
还有一个隐蔽的坑:设置文件根本不是你改的那一个。用 SHOW config_file; 和 SHOW hba_file; 确认服务端实际加载的路径,这在多实例、容器、包管理器安装(设置分散在 /etc/postgresql/<版本>/main/ 下)的场景里极其常用。我见过有人改了 /var/lib/pgsql/data/postgresql.conf ,实际跑的是另一个实例的设置目录,白白折腾一晚上。
SHOW config_file;
SHOW hba_file;
SHOW data_directory;
SELECT name, setting, sourcefile, sourceline
FROM pg_settings
WHERE name IN ('listen_addresses', 'port', 'max_connections', 'password_encryption');
sourcefile 和 sourceline 这两列是我最喜欢的调试助手,它直接告诉你"当前值是从哪个文件的第几行读到的",比在多个设置文件之间翻找高效得多。
2.4 第四关:pg_hba.conf 的匹配规则不生效的三个原因
结合项目来看,网络通了、端口听了,下一个拦路虎就是 pg_hba.conf 。这个文件最容易出的问题不是"写错了",而是"写了但没生效"。
第一个原因是 没重载 。 pg_hba.conf 属于 reload 级参数,改完必须 pg_ctl reload 或者 SELECT pg_reload_conf(); 。不重载的话,新连接仍然按旧规则匹配,你会觉得规则写了跟没写一样。
第二个原因是 匹配顺序 。 pg_hba.conf 是从上往下一行行匹配的, 第一条匹配上的规则立即生效,后面的一律不看 。所以如果文件顶部有一条 host all all 127.0.0.1/32 reject 或者一条更宽的 host all all 0.0.0.0/0 md5 ,你在下面新加的更精确的规则永远轮不到。正确姿势是把精确规则放前面、宽泛规则放后面,或者直接清理掉历史遗留的条目。
第三个原因最容易忽略: 字段语义搞错 落到代码里,。这五个字段依次是连接类型、数据库、用户、地址、认证方法,其中用户字段写的是 角色名 从实现思路看,,不是数据库名;地址字段如果是主机名,会触发反向 DNS 解析,解析慢或失败时会表现为连接超时,看起来像网络问题。生产上建议地址一律写 CIDR,别写主机名。
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host all postgres 10.0.0.0/24 scram-sha-256
host all app_rw 10.0.0.0/24 scram-sha-256
host all all 0.0.0.0/0 reject
从实现思路看,PG 10 之后有一个很实用的视图能够直接审阅规则,不用去猜文件在哪:
SELECT line_number, type, database, user_name, address, auth_method, error
FROM pg_hba_file_rules
ORDER BY line_number;
error 列不为空的行就是语法有问题或无法解析的条目, line_number 直接对应文件行号。这条 SQL 我基本是遇到 pg_hba 相关报错时的第一条命令。
2.5 第五关:密码认证为什么明明对了还是失败
排除掉前面四关,剩下的就是 "postgres" P... 中最可能的那种: password authentication failed for user "postgres" 。这里有个高频误区—— "我记得密码是这个"和"服务端存的密码验证器能匹配我的输入"是两件事 。
PG 10 之后引入 scram-sha-256 ,14 版本开始成为默认。如果服务端把密码存成了 SCRAM 验证器,而客户端是 libpq 9.x 这种老版本驱动,它只会发 MD5 或明文密码响应,服务端无法完成挑战应答,报错可能就表现为认证失败而非明确的不兼容提示。反过来,服务端 pg_hba.conf 写 md5 但库里存的是 SCRAM 验证器,PostgreSQL 会做个兼容处理,但如果 password_encryption 是 scram-sha-256 而客户端库太老,一样会翻车。
排查顺序:
-- 1. 客户端与服务端版本
SELECT version();
-- 2. 当前密码存储算法
SHOW password_encryption;
-- 3. 角色是否存在、验证器是什么前缀
SELECT rolname, rolcanlogin, left(rolpassword, 14) AS verifier_prefix
FROM pg_authid WHERE rolname = 'postgres';
rolpassword 以 SCRAM-SHA-256$ 开头说明是 SCRAM 验证器,以 md5 开头说明是老的 MD5 验证器。两者不能混用,如果确认是算法不匹配,处理方式是 重设一次密码 ,让它按当前的 password_encryption 重新生成验证器:
SET password_encryption = 'scram-sha-256';
ALTER ROLE postgres WITH PASSWORD 'your-new-strong-password';
注意: ALTER ROLE ... PASSWORD 只有在角色拥有 LOGIN 属性时才有实际意义;另外改密码后已有连接不会掉,但新连接必须用新密码。改完不要忘记同步所有依赖它的设置文件与密钥管理系统,否则第二天定时任务会集体失败。
结合项目来看,还有一个容易被忽视的细节:密码里包含 @ 、 / 、 : 、 # 、 ? 这些字符时,在 URI 形式连接串里必须做百分号编码,否则解析会错位,把密码的一部分当成主机名或端口。这就是为什么有人用 psql "postgresql://user:p@[email protected]:5432/db" 时,客户端会去连一个叫 [email protected] 的怪主机。改成键值对形式 host=... password=... 能够规避,或者老老实实编码。
2.6 第六关:连接数、库存在性与角色权限
前五关都过了,还可能卡在最后一步。三个高频原因:
连接数打满 。报错是 FATAL: sorry, too many clients already 或 remaining connection s are reserved 。检查方式:
SELECT count(*) AS total,
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_tx
FROM pg_stat_activity;
idle in transaction 数量高是应用层没提交事务的典型信号,这类连接会一直占着槽位不放,比普通 idle 更危险。
库或角色不存在 。报错会是 database "xxx" does not exist 或 role "xxx" does not exist ,但有些驱动会把它包装成笼统的连接失败,日志里才看得到细节。
角色没有 CONNECT 权限 。PG 默认 PUBLIC 对新建库有 CONNECT 权限,但如果被人回收过,就会报 permission denied for database 。这类问题在接手别人维护的实例时特别常用,因为权限被收紧过但没有文档。
3. 典型场景复现与修复实操
3.1 场景 A:容器里跑库,宿主机就是连不上
这是 connection failed 出现频率最高的场景之一。很多人用官方镜像起了一个实例,映射了端口,宿主机 psql 却报连接拒绝。原因通常有三层,得一层层剥。
第一层是端口映射方向搞反。 -p 5432:5432 是"宿主端口:容器端口",写反了就等于没映射。第二层是容器内的 listen_addresses 。官方镜像默认是 '*' ,但如果你基于某些精简基础镜像自己打包,很可能沿用了只 localhost 的默认设置,此时容器内 psql 能连、外部一律拒绝。第三层是 pg_hba.conf ,容器内的地址段是 Docker 网桥,如果规则只放行了 127.0.0.1/32 ,从宿主机过来的连接源地址是网桥网关,一样匹配不上。
docker run -d --name demo-pg \
-e POSTGRES_PASSWORD='change-me' \
-p 127.0.0.1:5432:5432 \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# 进容器里确认地址与规则
docker exec demo-pg bash -lc "psql -U postgres -c 'show listen_addresses;'"
docker exec demo-pg bash -lc "psql -U postgres -c \"select line_number,type,database,user_name,address,auth_method from pg_hba_file_rules;\""
我个人的习惯是: 映射端口时显式写 127.0.0.1:5432:5432 而不是 5432:5432 。前者只对本机暴露,避免把库裸奔到局域网;调试期再改成具体网卡地址,上线前改回来。这个习惯帮我躲过好几次"临时开放忘记回收"的事故。
结合项目来看,还有一个容器场景特有的坑:容器重启后 pg_hba.conf 被镜像原始内容覆盖。如果你是在运行中的容器里直接 vi 改的文件,而不是借助挂载设置或初始化脚本注入,那么容器一重建,改动全部消失,你会看到"昨天明明好了,今天又挂了"。正确做法是把设置文件和初始化 SQL 挂载进去,或者自己构建镜像。
3.2 场景 B:改完设置不生效的排查套路
"改了不生效"是排障里最磨人的一类。我的固定动作是四步:
- 确认改的是服务端实际加载的文件:
SHOW config_file;/SHOW hba_file; - 确认改动是否需重启:
SELECT name, setting, pending_restart FROM pg_settings WHERE pending_restart; - 触发重载同时检查日志:
SELECT pg_reload_conf();然后看日志有没有received SIGHUP - 用视图复核当前生效内容:
pg_settings的sourcefile/sourceline,pg_hba_file_rules的完整行
SELECT pg_reload_conf();
SELECT name, setting, pending_restart
FROM pg_settings
WHERE pending_restart IS TRUE; -- 有结果就说明必须重启,reload 不够
日志路径也要先搞清楚, SHOW log_directory; 看一眼,或者直接看 SHOW logging_collector; 。容器环境直接 docker logs 更快。日志里 LOG: received SIGHUP, reloading configuration files 这行出现,才说明重载真的发生了;如果重载发生了但行为没变,那就是规则逻辑问题,不是生效问题。
3.3 场景 C:连接串写法的那些坑
在这个场景下,连接串的两种写法(URI 和键值对)各有雷区。URI 形式的雷在特殊字符编码和查询参数,键值对形式的雷在引号和空格。
# 键值对形式:整体必须被引号包住,否则空格会把参数切断
psql "host=10.0.0.12 port=5432 dbname=appdb user=app_rw connect_timeout=5"
# URI 形式:密码含特殊字符必须百分号编码
psql "postgresql://app_rw:p%[email protected]:5432/appdb?connect_timeout=5&sslmode=require"
几个实用参数值得加进模板:
| 参数 | 作用 | 建议值 |
|---|---|---|
connect_timeout | 建立连接的超时秒数 | 3~5,别用默认无限等 |
sslmode | 是否强制加密 | 跨网段用 require 或 verify-full |
application_name | 在 pg_stat_activity 里标识来源 | 写成服务名-环境 |
options | 附加会话参数 | -c statement_timeout=30s |
application_name 这个参数价值被严重低估。一个实例上挂着七八个服务时, pg_stat_activity 里全显示成驱动默认值,根本分不清是谁在占用连接。统一规范写上 application_name ,事后查问题时能省太多时间。
3.4 场景 D:客户端驱动与认证算法版本不匹配
这个场景的迷惑性在于:命令行用最新 psql 连得好好的,应用里就是连不上。原因多半是应用用的驱动版本太老,或者打包时带了一个陈旧的 libpq 动态库。
在这个场景下,Python 生态里用 psycopg 时,建议直接上 psycopg 3,它兼容 SCRAM 完整流程且不需额外依赖:
import psycopg
conninfo = (
"host=10.0.0.12 port=5432 dbname=appdb user=app_rw "
"password=change-me connect_timeout=5 application_name=order-svc-prod"
)
with psycopg.connect(conninfo) as conn:
with conn.cursor() as cur:
cur.execute("select current_user, inet_server_addr(), version()")
print(cur.fetchone())
实际处理时,若你还在用老的同步驱动,务必确认它背后的 libpq 版本兼容 SCRAM。判断方法很土但有效:命令行用同一台机器上的 psql 连一次,能连上说明网络和认证链路没问题,问题只可能在应用侧的驱动或参数,此时把驱动升级的成本远低于去改服务端认证算法降级。 别为了迁就一个老旧驱动把服务端的安全性降下来 ,这是我很坚持的一条线。
3.5 参数计算:max_connections 该开多大
落到代码里,连接数打满时,最省事的做法是把 max_connections 调大,但这会带来内存压力。每个后端进程有基础开销,再加上排序、哈希等操作可能用到的 work_mem ,粗暴上调很容易把机器打爆。
粗算逻辑是这样的:假设单连接基础内存约 5~10 MB, work_mem 设为 4 MB,一个复杂查询最多可能用掉几个 work_mem (排序、哈希连接、哈希聚合各自独立计数)。保守估算按每连接 10 MB + 2×work_mem:
单连接估算 = 10 MB + 2 × 4 MB = 18 MB
200 连接估算 = 200 × 18 MB ≈ 3.6 GB
再加上共享缓冲 shared_buffers (通常物理内存的 25%)、操作系统缓存、其他进程开销。如果机器是 16 GB 内存, shared_buffers 4 GB,留给连接的预算大概 8~9 GB,200 连接是比较稳的;如果非要开到 500,那就得把 work_mem 降到 1~2 MB,同时接受复杂查询走磁盘的风险。
-- 看当前连接使用情况与上限
SHOW max_connections;
SHOW superuser_reserved_connections;
SELECT usename, application_name, state, count(*)
FROM pg_stat_activity
GROUP BY 1, 2, 3
ORDER BY 4 DESC;
结合项目来看,真要支撑高同时发,正确方向不是猛加 max_connections ,而是 在前面加一层连接池 在这个场景下,,把几百个应用连接收敛到几十个数据库连接上。这个改动对吞吐的提升往往比调参数明显得多。
4. 常用问题速查表与排障脚本
4.1 报错片段与病根的对照表
| 报错片段 | 最可能的原因 | 第一动作 |
|---|---|---|
could not translate host name "1" | host 变量未替换、模板占位符残留 | 打印最后连接参数,查环境变量 |
Connection refused | 未、端口不对、进程未起 | ss -lntp | grep 5432 |
Connection timed out | 安全组、防火墙、路由 | nc -vz 探测,找网络侧 |
no pg_hba.conf entry for host | 规则缺条目或匹配顺序被前面的 reject 拦掉 | 查 pg_hba_file_rules |
password authentication failed | 密码错、算法不匹配、验证器类型不符 | 查 pg_authid 前缀,重设密码 |
database "x" does not exist | 库名拼错、连了错误的实例 | \l 列出所有库确认 |
too many clients already | 连接泄漏、池设置不当 | 查 pg_stat_activity 状态分布 |
no encryption / SSL 相关 | 客户端要求加密,服务端未开 | 查 ssl 参数与证书 |
server closed the connection unexpectedly | 服务端崩溃、被 OOM 杀掉 | 查服务端日志与系统日志 |
这张表我建议打印出来贴显示器边上,前三个月能省下大量搜索时间。
4.2 一份能够照着跑的排查脚本
实际处理时,把上面几层串成一条直线,从客户端侧一路打到服务端,中间每一层都留下可读的输出:
#!/usr/bin/env bash
set -u
HOST="${1:?usage: pgcheck.sh[port] [user] [db]}"
PORT="${2:-5432}"
USER_NAME="${3:-postgres}"
DBNAME="${4:-postgres}"
echo "== 1. 地址解析 =="
getent hosts "$HOST" || echo "解析失败:检查 DNS 或 /etc/hosts"
echo "== 2. 端口连通 =="
nc -vz -w 3 "$HOST" "$PORT" 2>&1
echo "== 3. 服务就绪探测 =="
pg_isready -h "$HOST" -p "$PORT" -U "$USER_NAME" -d "$DBNAME" -t 5
echo "== 4. 实际连接(带超时) =="
psql "host=$HOST port=$PORT dbname=$DBNAME user=$USER_NAME connect_timeout=5" \
-c 'select current_user, inet_server_addr(), inet_server_port(), version();'
服务端侧再配一段"体检 SQL":
-- 关键参数与生效来源
SELECT name, setting, sourcefile, sourceline, pending_restart
FROM pg_settings
WHERE name IN ('listen_addresses','port','max_connections','password_encryption','ssl');
-- 规则审阅
SELECT line_number, type, database, user_name, address, auth_method, error
FROM pg_hba_file_rules ORDER BY line_number;
-- 连接占用画像
SELECT application_name, state, count(*)
FROM pg_stat_activity GROUP BY 1,2 ORDER BY 3 DESC;
-- 长时间未提交事务
SELECT pid, application_name, state, now()-state_change AS stuck_for
FROM pg_stat_activity
WHERE state = 'idle in transaction' AND now()-state_change > interval '5 minutes';
从实现思路看,最后那条查询我希望你每次上线前都跑一遍。 idle in transaction 超过五分钟的连接,基本都是代码里 begin 之后忘了 commit 或者异常分支没回滚,它们会长时间持有锁、占着连接槽,是高同时发下最隐蔽的杀手。
4.3 日志里到底该看什么
从实现思路看,服务端日志的信息密度远高于客户端报错。关键行大致长这样: FATAL: password authentication failed for user "postgres" 前面会带来源 IP 和端口, DETAIL: Connection matched pg_hba.conf line 93: "host all all 0.0.0.0/0 scram-sha-256" 这行尤其宝贵——它直接告诉你是 哪一行规则匹配上了 。有了这行,前面说的"匹配顺序"问题基本一眼就能定位。
落到代码里,若你发现日志里压根没有连接记录,那说明连接根本没到服务端,问题一定在网络层或端口层,不用再看认证设置了。这个判断能砍掉一半的排查动作。
5. 踩过的坑与长期预防
5.1 三个反直觉的现场
第一个: 改对了设置,但实例根本不是那个实例 从实现思路看,。一台机器上跑了两套库,一套是包管理器装的、一套是自己编译的, psql 默认连的是 5432,你改的却是 5433 那套的 pg_hba.conf 。判断办法是连上去先执行 SHOW data_directory; ,看清楚自己到底在和谁说话。
第二个: localhost 走了 Unix socket。在 Linux 上, psql -h localhost 和 psql (不带 -h )走的是两条路:前者走 TCP,后者走 Unix domain socket。而 pg_hba.conf 里 local 类型和 host 类型是两组独立规则。所以会出现"不带参数能连、带 -h localhost 就连不上"的情况。搞清楚这点能避免很多自我怀疑。
第三个: postmaster.pid 残留导致启动失败,进而表现为连接拒绝。上一次非正常关闭后,数据目录里留着 pid 文件,新进程启动时认为已有实例在跑,直接退出。此时日志里会有明确提示,但如果你只看客户端报错,会一直以为是网络问题。处理方式是确认没有残留进程后清理 pid 文件,再启动。 永远先看服务端日志,再看客户端报错 ,这条经验价值极高。
5.2 把连接设置当成代码来管
理解这一步时,我后来在所有项目里都强制一条规矩:连接参数不允许散落在各个服务的设置文件里,统一从一个设置源注入,同时且在启动时打印一份脱敏后的连接摘要。
[db] host=10.0.0.12 port=5432 dbname=appdb user=app_rw sslmode=require
connect_timeout=5 application_name=order-svc-prod
实际处理时,这份摘要包含除密码外的所有关键字段,出问题时第一眼就能看出 host 是不是 "1" 这种残值。同时把所有环境的连接串做成模板,用同一套替换逻辑生成,避免"测试环境能跑、生产环境漏了个变量"这种经典事故。设置写错这件事,靠人仔细是防不住的,只能靠机制。
还有个小习惯: 给每个服务建独立的数据库角色,不要所有服务共用一个高权限角色 。这么做的直接好处是排障时 pg_stat_activity 里的 usename 能直接指向服务,间接好处是权限边界清晰,出问题时影响面可控。
5.3 坚控该盯哪几个指标
结合项目来看,连接类问题的特点是"平时没事,一出就是全站",所以坚控要提前布。我一般盯四个指标:当前连接数占总上限的比例(超过 80% 告警)、 idle in transaction 的会话数与最长持续时间、单位时间内的认证失败次数(突增往往意味着设置变更或密码轮换出错)、以及新建连接的成功率。
在这个场景下,认证失败次数这个指标特别值得单独拉出来,因为它的突增几乎总能对应到一次设置发布或密码轮换,是最灵敏的"变更探测器"。至于连接数,建议按 application_name 分维度看,总量正常但某个服务单独飙升,说明问题在那个服务的连接管理上,不在数据库本身。
从实现思路看,最后分享一个我在实际排查中形成的小习惯:遇到 connection failed 这类报错,先在纸上按"地址—网络——规则—认证—库/权限"六层画一条线,每验证一层就在旁边打个勾。看起来笨,但能有效防止在两个层之间反复横跳。真正拖慢排障的从来不是问题难,而是 没有固定顺序 。
从实现思路看,总的来说,PostgreSQ连接失败适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。
