PostgreSQL 连接耗尽:先分清会话占用、连接池放大与长事务

97 次浏览5 条回复

适用于 PostgreSQL 13+、psql 13+。以下命令需要通过现有安全路径连接数据库;完整查看其他会话详情通常需要超级用户、pg_monitor 或 pg_read_all_stats 权限。示例不包含终止会话或修改配置,先保留故障时间线。

1. 确认是连接槽位紧张,而不是服务未启动

SELECT now() AT TIME ZONE 'UTC' AS utc_time, version();

SELECT current_setting('max_connections')::int AS max_connections,
       current_setting('superuser_reserved_connections')::int AS superuser_reserved,
       count(*) FILTER (WHERE backend_type = 'client backend') AS client_backends
FROM pg_stat_activity;

SELECT backend_type, count(*)
FROM pg_stat_activity
GROUP BY backend_type
ORDER BY count(*) DESC;

client_backends 可作为应用会话占用的直接指标,但不要把它简单当成所有槽位的精确余额;复制连接和不同后端类型应结合第二条查询一起看。客户端出现 too many clients already 或“剩余连接槽位保留给管理连接”时,再与同一 UTC 时间的 PostgreSQL 日志对应。pg_isready 只适合判断服务器是否响应连接探测,不能证明普通应用角色还能建立并完成查询。

2. 找出连接集中在哪个来源

SELECT datname, usename, application_name, client_addr, state, count(*) AS sessions
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY datname, usename, application_name, client_addr, state
ORDER BY sessions DESC
LIMIT 30;

SELECT pid, datname, usename, application_name, client_addr, state,
       now() - xact_start AS xact_age,
       now() - state_change AS state_age,
       wait_event_type, wait_event
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY xact_start NULLS LAST
LIMIT 30;

重点区分三类现象:大量 active 可能是查询变慢导致连接归还不及时;大量 idle 常见于每个进程各自建立过大的池;长时间 idle in transaction 可能同时占连接、持有事务快照或锁。application_name 为空时,应回到连接串和驱动配置补齐标识,否则很难区分实例。查询文本、地址和角色名可能包含敏感信息,公开排障时只保留必要摘要。

3. 对齐数据库日志与慢点

先确认日志去向和生效配置:

SHOW log_destination;
SHOW logging_collector;
SHOW log_directory;
SHOW log_connections;
SHOW log_disconnections;

SELECT name, setting, unit, source, sourcefile, pending_restart
FROM pg_settings
WHERE name IN (
  'max_connections',
  'superuser_reserved_connections',
  'statement_timeout',
  'idle_in_transaction_session_timeout'
)
ORDER BY name;

若由 systemd 托管,可从实际单元读取同一窗口;发行版的单元名可能不是 postgresql,执行前先确认:

systemctl list-units --type=service 'postgresql*'
sudo journalctl -u <actual-postgresql-unit> \
  --since '<UTC-start>' --until '<UTC-end>' --no-pager -o short-iso

把首次拒绝连接的时间与部署、流量变化、查询延迟、锁等待和应用副本数对应。只看到连接数高并不能判断是容量不足还是下游变慢造成的堆积。

4. 核算连接池的总预算

按“每个应用进程的最大池大小 × 进程数 × 副本数”计算理论上限,并把后台任务、迁移工具、监控、复制和管理连接单独列出。常见问题不是单个池很大,而是水平扩容后每个副本都复制了同一池配置。

修复优先级通常是:先让连接池具备等待上限和背压,再缩小每副本池并修复连接泄漏或慢查询;只有完成内存与并发评估后,才考虑提高 max_connections。该参数需要重启才能生效,pg_reload_conf() 不能代替重启;可用上面的 pending_restart 验证。不要在没有确认事务用途时批量终止 idle in transaction 会话。

5. 闭环验证

变更后至少确认:

SHOW max_connections;
SELECT state, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state
ORDER BY count(*) DESC;

在超过原故障周期的观察窗口内,应用错误日志不再新增连接获取超时,客户端会话数稳定低于预算,长事务和 idle in transaction 没有持续累积,并通过真实入口完成无副作用查询。若做了应用扩缩容,还要重新按新副本数核算池上限;只在低流量时复测一次不足以证明问题已经闭环。

再补一个代理层观察点:链路中有 PgBouncer 时,pg_stat_activity 看到的是它持有的服务端连接,不是等待中的全部应用客户端。数据库侧连接数稳定,也可能已经在池前排队。先通过已授权的 PgBouncer 管理入口执行只读命令(输出列会随版本变化,因此先记录版本):

SHOW VERSION;
SHOW POOLS;
SHOW STATS;
SHOW CONFIG;

重点对照 SHOW POOLS 中的 cl_waiting、sv_active、sv_idle 和 maxwait。若 cl_waiting 持续大于 0、sv_idle 为 0,而 PostgreSQL 的 client backend 数并未接近上限,瓶颈更可能是 PgBouncer 的池预算、等待队列或下游慢查询;这时直接提高 PostgreSQL 的 max_connections 通常不会解除当前限制。

还要记录 pool_mode:在 transaction 模式下,应用连接与 PostgreSQL 会话不是一一对应,数据库侧的 idle 不能直接推断为应用泄漏。闭环时应同时从真实应用入口做无副作用请求,并在同一 UTC 窗口确认 cl_waiting 回落、maxwait 不再增长、数据库错误日志没有新增拒绝连接。管理控制台可能暴露数据库名、用户名和地址,公开结果前应做必要脱敏。

还应排除角色级和数据库级上限。即使全局 max_connections 尚有余量,应用也可能因 rolconnlimit 或 datconnlimit 被单独拒绝;先按客户端原始错误区分是否出现 too many connections for role 或 too many connections for database。适用于 PostgreSQL 13+,查看所有角色、数据库和其他会话通常需要管理员权限或相应监控权限。

SELECT rolname, rolcanlogin, rolconnlimit
FROM pg_roles
WHERE rolcanlogin
ORDER BY rolname;

SELECT datname, datallowconn, datconnlimit
FROM pg_database
ORDER BY datname;

SELECT usename, datname, count(*) AS client_backends
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY usename, datname
ORDER BY client_backends DESC;

rolconnlimit、datconnlimit 为 -1 表示不限制。第三条查询用于找出当前占用,但连接限制的检查并非严格锁定计数,并发建立连接时可能有短暂偏差,因此要和同一 UTC 窗口的服务端日志、应用连接池指标一起判断。也不要只看当前值:短连接风暴可能在采样前已经回落。

如果需要调整,应先核算该角色或数据库下所有副本、任务进程和运维工具的连接预算,再用 ALTER ROLE ... CONNECTION LIMIT ... 或 ALTER DATABASE ... CONNECTION LIMIT ...;这类修改会影响后续连接,不会清理既有会话。验证时应使用与应用相同的普通登录角色、目标数据库和连接入口执行 SELECT 1,并持续观察超过原故障周期,确认连接获取错误不再新增且会话峰值低于新预算。不要用管理员连接成功代替应用身份验证。

可以再加一个锁等待分支:连接池耗尽有时是结果而不是根因。大量会话显示为 active 时,应先确认它们是在执行 CPU/I/O 工作,还是排队等待同一个锁;适用于 PostgreSQL 13+。查看其他会话完整属性通常需要超级用户、pg_monitor 或 pg_read_all_stats 权限。

先按等待类型汇总,避免把所有 active 会话都当成慢查询:

SELECT state, wait_event_type, wait_event, count(*) AS sessions
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state, wait_event_type, wait_event
ORDER BY sessions DESC;

若 wait_event_type = 'Lock' 明显集中,可用 pg_blocking_pids() 建立被阻塞会话与阻塞源的关系;这里不输出查询文本,便于在共享排障记录前控制敏感信息:

WITH blocked AS (
  SELECT a.pid AS blocked_pid,
         a.usename AS blocked_user,
         a.application_name AS blocked_app,
         now() - a.query_start AS blocked_for,
         unnest(pg_blocking_pids(a.pid)) AS blocker_pid
  FROM pg_stat_activity AS a
  WHERE a.backend_type = 'client backend'
)
SELECT b.blocked_pid, b.blocked_user, b.blocked_app, b.blocked_for,
       b.blocker_pid, k.usename AS blocker_user,
       k.application_name AS blocker_app, k.state AS blocker_state,
       now() - k.xact_start AS blocker_xact_age,
       k.wait_event_type AS blocker_wait_type,
       k.wait_event AS blocker_wait_event
FROM blocked AS b
LEFT JOIN pg_stat_activity AS k ON k.pid = b.blocker_pid
ORDER BY b.blocked_for DESC;

如果多个会话指向同一 blocker_pid,应回到该事务所属应用、事务边界和同一 UTC 窗口的日志定位;不要只因为它处于 idle in transaction 就直接终止,先确认是否承担迁移或关键写入。若锁等待不集中,再转向查询耗时、存储 I/O、外部依赖以及连接池等待指标。闭环时应在超过原故障周期的窗口内重复采样,确认阻塞链消失、最老事务时长不再增长,并且应用侧连接获取超时同步停止新增。

再补一个 PostgreSQL 16+ 的版本差异:连接预算除了 superuser_reserved_connections,还可能包含 reserved_connections。后者预留给具备 pg_use_reserved_connections 角色成员资格的账号;当普通连接已触及可用额度时,监控账号能否进入,取决于它属于普通角色、该预留角色还是超级用户。PostgreSQL 13-15 没有这项参数,因此排障记录应先固定服务端版本。

跨版本采集可以先用:

SELECT current_setting('server_version_num')::int AS server_version_num,
       current_setting('max_connections')::int AS max_connections,
       current_setting('superuser_reserved_connections')::int AS superuser_reserved,
       COALESCE(current_setting('reserved_connections', true), '0')::int AS role_reserved;

仅在 PostgreSQL 16+ 再确认当前排障角色是否具备预留槽位资格:

SELECT current_user,
       pg_has_role(current_user, 'pg_use_reserved_connections', 'MEMBER')
         AS can_use_role_reserved;

容量核算时应把普通应用预算按 max_connections - superuser_reserved_connections - reserved_connections 作为上界,再为迁移、定时任务和监控留出显式余量;不要把预留槽位计入应用池的常态容量。参数调整后分别使用真实应用普通角色和既定应急管理路径验证连接:前者用于证明业务预算可用,后者用于证明连接接近上限时仍保留诊断入口。验证时同时记录 server_version_num 与三个参数,避免主备或多实例参数不一致造成误判。

HazelLv1#2

还应排除角色级和数据库级上限。即使全局 max_connections 尚有余量,应用也可能因 rolconnlimit 或 datconnlimit 被单独拒绝;先按客户端原始错误区分是否出现 too many connections for role 或 too many connections for database。适用于 PostgreSQL 13+,查看所有角色、数据库和其他会话通常需要管理员权限或相应监控权限。

SELECT rolname, rolcanlogin, rolconnlimit
FROM pg_roles
WHERE rolcanlogin
ORDER BY rolname;

SELECT datname, datallowconn, datconnlimit
FROM pg_database
ORDER BY datname;

SELECT usename, datname, count(*) AS client_backends
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY usename, datname
ORDER BY client_backends DESC;

rolconnlimit、datconnlimit 为 -1 表示不限制。第三条查询用于找出当前占用,但连接限制的检查并非严格锁定计数,并发建立连接时可能有短暂偏差,因此要和同一 UTC 窗口的服务端日志、应用连接池指标一起判断。也不要只看当前值:短连接风暴可能在采样前已经回落。

如果需要调整,应先核算该角色或数据库下所有副本、任务进程和运维工具的连接预算,再用 ALTER ROLE ... CONNECTION LIMIT ... 或 ALTER DATABASE ... CONNECTION LIMIT ...;这类修改会影响后续连接,不会清理既有会话。验证时应使用与应用相同的普通登录角色、目标数据库和连接入口执行 SELECT 1,并持续观察超过原故障周期,确认连接获取错误不再新增且会话峰值低于新预算。不要用管理员连接成功代替应用身份验证。

针对“短连接风暴在采样前已回落”,PostgreSQL 14+ 还可以用 pg_stat_database 的累计会话计数补一条证据链。numbackends 是当前值,而 sessions、sessions_abandoned、sessions_fatal、sessions_killed 是自 stats_reset 起的累计值;单次读数不能直接当速率,应在固定时间窗口的起止各采集一次并计算增量。

SELECT now() AT TIME ZONE 'UTC' AS utc_time,
       datname, numbackends, sessions,
       sessions_abandoned, sessions_fatal, sessions_killed,
       stats_reset
FROM pg_stat_database
WHERE datname = current_database();

若 numbackends 看起来稳定但 sessions 在短窗口内快速增加,优先检查应用是否关闭了连接复用、健康检查是否每次新建连接,以及副本扩缩容或重试策略是否造成连接抖动;sessions_fatal 或 sessions_abandoned 同步增加时,再按同一 UTC 窗口对齐服务端与客户端日志。计算增量前必须确认两次采样的 stats_reset 相同,否则计数器被重置后差值没有意义。

这些列在 PostgreSQL 13 不可用,13 应继续依赖 log_connections、log_disconnections 与连接池指标。闭环时不仅要看并发峰值下降,还应确认单位时间的新建会话增量回到基线,并从真实应用入口验证连接复用和请求成功。