凌晨告警里,“应用连不上数据库”通常会被直接翻译成“PostgreSQL 挂了”。但这个现象至少对应两类完全不同的故障:数据库主机真的无法响应,或者应用连接池里保存了一条早已失效的 TCP 连接。两者需要不同的处理方式,混淆它们,很容易浪费事故发生后的关键十分钟。
两种故障,分界线是“新连接能否建立”
真正的 PostgreSQL 卡死发生在服务端或更底层。此时无论从哪个应用、连接池或网络路径发起请求,新会话都会超时或被拒绝;即使登录数据库主机并执行一个全新的 psql,也无法正常连接。
这类故障通常要向操作系统层排查:
- 内存耗尽,内核正在频繁回收页面或触发 OOM。
- 存储卷无法完成 I/O,请求长时间停在不可中断状态。
- CPU 被占满,PostgreSQL 连接受理进程迟迟得不到调度。
失活连接则不同。PostgreSQL 本身仍然健康,新连接可以立即建立,只有连接池中事故发生前就存在的连接失败。常见错误包括 connection reset by peer、broken pipe,以及驱动报告的“连接已经关闭”。
这种连接可能被负载均衡器、NAT 网关、服务网格或 Kubernetes 网络变化悄悄丢弃。如果中间设备没有让 FIN 或 RST 抵达通信双方,应用和 PostgreSQL 都可能继续认为连接存在,直到下一次真正读写 socket 时才发现问题。
因此,最重要的判断问题不是“应用能不能访问数据库”,而是:现在创建一条全新的连接,能不能访问数据库?
一套可以直接执行的五分钟检查
下面的脚本假设已经安装 PostgreSQL 客户端。运行前设置 PGHOST、PGPORT、PGUSER 和 PGDATABASE;不要把密码直接写进脚本,可使用 PGPASSWORD、.pgpass 或现有密钥注入机制。
#!/usr/bin/env bash
set -u
: "${PGHOST:?set PGHOST}"
: "${PGUSER:?set PGUSER}"
: "${PGDATABASE:?set PGDATABASE}"
export PGPORT="${PGPORT:-5432}"
export PGCONNECT_TIMEOUT="${PGCONNECT_TIMEOUT:-5}"
psql -X -v ON_ERROR_STOP=1 \
-c "select now() AS checked_at, pg_backend_pid() AS backend_pid;"
尽量先在数据库主机上执行,并让 PGHOST 指向本机 Unix socket 或回环地址。然后再从应用所在网络执行一次。结果可以这样解释:
- 数据库主机上的全新连接也超时或拒绝:优先按服务端卡死处理,检查 CPU、内存和磁盘 I/O。
- 主机直连成功,但应用网络失败:检查安全组、路由、DNS、代理和中间网络设备。
- 新连接全部成功,只有连接池中的旧连接报错:高度符合失活连接特征,应淘汰旧连接并检查池化与 keepalive 配置。
确认服务器可连接后,可以查看 PostgreSQL 是否仍把相关后端视为正常空闲连接:
SELECT
pid,
usename,
application_name,
client_addr,
state,
backend_start,
state_change,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY state_change;
如果应用已经报告连接失败,而对应后端仍以 idle 状态存在,说明 PostgreSQL 没有收到连接关闭信号。这是网络路径或客户端连接已静默失效的重要证据。
同时对齐事故时间,搜索两端日志:
rg -i "reset by peer|broken pipe|connection.*closed|timeout" /var/log/postgresql/
rg -i "reset by peer|broken pipe|connection.*closed|timeout" /var/log/myapp/
真正的服务端卡死更常表现为连接超时和日志空白;失活连接通常在应用重新使用 socket 时快速返回明确错误。不过错误文本取决于驱动,不能只凭一条日志定性,仍要以全新连接测试为核心证据。
为什么 PostgreSQL 不会立即发现死连接
这不是 PostgreSQL 特有的缺陷,而是 TCP 的行为边界。连接在中间路径上被静默删除后,如果两端都不发送数据,它们就没有机会知道状态已经变化。TCP keepalive 通过周期性探测缩短发现时间,连接池健康检查则在应用层阻止坏连接被借给业务请求。
可以在 postgresql.conf 中显式设置服务端 keepalive。下面是一组实践起点,不是适用于所有环境的固定答案;具体值必须短于网络路径中最小的空闲连接超时,并结合连接规模评估探测开销:
# postgresql.conf
tcp_keepalives_idle = 60
tcp_keepalives_interval = 15
tcp_keepalives_count = 4
# PostgreSQL 14+;长查询期间定期检查客户端是否仍连接
client_connection_check_interval = '30s'
修改后需要按配置项要求重新加载或重启,并验证实际值:
psql -X -c "SELECT name, setting, unit FROM pg_settings WHERE name IN ('tcp_keepalives_idle', 'tcp_keepalives_interval', 'tcp_keepalives_count', 'client_connection_check_interval');"
client_connection_check_interval 默认关闭,主要价值是让服务器在长查询执行期间更早发现客户端已经消失。它不能替代 TCP keepalive 或连接池校验。
连接池和网络超时必须配套
使用 HikariCP 时,可以这样实践:
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.connection-timeout=5000
spring.datasource.hikari.validation-timeout=3000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.keepalive-time=120000
这里的 keepalive-time 是 120 秒,必须小于 max-lifetime,也应小于应用到 PostgreSQL 路径上最短的空闲超时。不要盲目照抄数值:如果某个 NAT、负载均衡器或代理在 60 秒后清理连接,那么 120 秒探测仍然太慢。
使用 PgBouncer 时,应分别考虑它到 PostgreSQL 的服务端连接和应用到 PgBouncer 的客户端连接:
[pgbouncer]
server_idle_timeout = 600
client_idle_timeout = 300
server_connect_timeout = 5
server_login_retry = 5
server_idle_timeout 与 client_idle_timeout 控制的是不同方向。启用客户端空闲超时还会改变应用长期持有空闲连接的行为,部署前应确认驱动具备自动重连或连接替换能力。
statement_timeout 和 idle_in_transaction_session_timeout 不解决这个问题。它们用于限制慢 SQL,以及清理长时间持有事务的客户端;一条已经在网络层静默死亡的连接,不会因为配置这两个参数而被及时识别。
上线前把诊断顺序写进值班手册
建议把以下顺序固定下来:
- 从数据库主机创建全新连接,设置明确的连接超时。
- 从应用网络路径创建全新连接,区分服务端故障与网络路径故障。
- 判断失败是否只影响事故前建立的池化连接。
- 对齐 PostgreSQL、驱动和代理日志中的 reset、broken pipe 与 timeout。
- 查询
pg_stat_activity,确认服务端是否仍把相关后端视为idle。 - 只有全新直连也失败时,才立即进入主机 CPU、内存和存储 I/O 排查。
修复失活连接不能只改一个参数。可靠方案需要让服务端 TCP keepalive、连接池健康检查、连接最大寿命以及网络设备空闲超时形成一致关系。目标不是保证连接永远不失效,而是让系统在业务请求拿到坏连接之前识别并替换它。