Postgres 高负载下连接池耗尽

Postgres 在流量上来时报 'remaining connection slots reserved'。通过调整池大小、上 PgBouncer、清理长 idle 连接来修。

应用 200 RPS 时稳稳的,流量翻倍后开始报 FATAL: remaining connection slots are reserved for non-replication superuser connectionssorry, too many clients already。Postgres 撞到了 max_connections。新请求失败或挂等空 slot。已有请求等到的连接是死的,也会卡。修复方向:池子大小合理化、Postgres 前面放 PgBouncer 走 transaction 模式、清掉 idle-in-transaction 连接、读流量分流到副本。

常见原因

按踩坑频率排序。

1. 每个应用实例自己开大池子

10 个应用实例、每个池子 20 = 200 连接。Postgres 默认 max_connections 是 100。所有实例同时起压就溢出。

怎么判断:对比 SELECT count(*) FROM pg_stat_activitySHOW max_connections。接近 max 就是要溢出。

2. 漏提交事务导致连接泄漏

代码开了事务、抛异常、没 commit 也没 rollback。连接永远停在 idle in transaction,把池子占住。

怎么判断SELECT state, count(*) FROM pg_stat_activity GROUP BY stateidle in transaction 数量高就是泄漏。

3. 长查询占着连接

报表查询跑 5 分钟,占着一个连接。20 个这种查询就把池子塞满。

怎么判断SELECT pid, query_start, query FROM pg_stat_activity WHERE state = 'active' ORDER BY query_start LIMIT 10。跑超过 30 秒的查询就是长尾。

4. Postgres 前面没 PgBouncer

应用池子直连 Postgres。每个应用实例都占真实 Postgres 连接,没复用。

怎么判断:连接来源是应用服务器,不是 PgBouncer 主机。

5. Serverless 函数每次冷启动开新连接

Vercel / Lambda 没接连接池时,每次冷启动开一条新连接。突发流量 = 突发新连接。

怎么判断pg_stat_activity 里很多短命连接,来自不同源端口。

6. 读流量都打到主库

所有读查询都走主。本来可以走副本,把主的连接腾给写。

怎么判断pg_stat_activity 里 80% 是读重查询,副本利用率几乎为零。

动手前先确认

  • 确认症状是 too many clients 还是 remaining connection slots reserved
  • 记下当前 max_connections 和实际占用数。
  • 找最近一次可能改了池子大小或查询模式的应用发布。
  • 看 Postgres 本体是不是健康:CPU、内存、复制延迟。
  • 计划安全重启:连接 drain 策略 + 短停机窗口。

需要收集的信息

  • SHOW max_connectionsSHOW shared_buffers、Postgres 类型(RDS、Cloud SQL、Supabase、自建)。
  • SELECT state, count(*) FROM pg_stat_activity GROUP BY state
  • 应用端池子配置:池大小、idle 超时、最大寿命。
  • PgBouncer 配置(如果有)。
  • 过去一小时的慢查询日志。

分步修复

Step 1:池子大小合理化

公式:应用实例数 * 每实例池大小 <= 0.7 * max_connections

// pg-pool 配置
import { Pool } from 'pg';

const pool = new Pool({
  max: 10,                  // 每个应用实例
  idleTimeoutMillis: 30000, // 30 秒释放 idle
  connectionTimeoutMillis: 2000, // 饱和时快速失败
});

10 个应用实例 + max_connections = 100,池子设 7 留余量。

Step 2:Postgres 前面放 PgBouncer

# pgbouncer.ini
[databases]
mydb = host=postgres-primary port=5432 dbname=app

[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
server_idle_timeout = 600

应用连 PgBouncer 的 6432。PgBouncer 持有 25 条真实 Postgres 连接,把 1000 个应用连接复用上去。

transaction 模式最常用——每个事务结束就释放连接。除非需要 session 级特性,否则别用 session 模式。

Step 3:清掉 idle-in-transaction 连接

-- 看一下作恶的
SELECT pid, now() - state_change AS idle_for, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND state_change < now() - interval '5 minutes'
ORDER BY state_change;

-- 干掉
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND state_change < now() - interval '5 minutes';

服务端加保护:

ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
SELECT pg_reload_conf();

Step 4:加 statement 超时

-- 按数据库
ALTER DATABASE app SET statement_timeout = '30s';

-- 按角色(报表)
ALTER ROLE reporting SET statement_timeout = '5min';

应用代码里按会话:

await pool.query("SET statement_timeout = '10s'");

Step 5:读分流到副本

import { Pool } from 'pg';

const primary = new Pool({ host: 'postgres-primary' });
const replica = new Pool({ host: 'postgres-replica' });

function getPool(query: string) {
  if (/^\s*(SELECT|EXPLAIN)\b/i.test(query) && !/FOR UPDATE/i.test(query)) {
    return replica;
  }
  return primary;
}

不明确时显式打标。读走副本,主就能腾出连接给写。

Step 6:Serverless 用合适的驱动

Vercel / Lambda:

// 用支持 HTTP / 边缘连接池的驱动
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);

// 或 Supabase
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(url, key);

这些驱动避开 serverless 场景下的原生 Postgres 连接。

Step 7:监控和告警

-- 每 30 秒跑一次的指标查询
SELECT
  count(*) FILTER (WHERE state = 'active') AS active,
  count(*) FILTER (WHERE state = 'idle') AS idle,
  count(*) FILTER (WHERE state = 'idle in transaction') AS idle_tx,
  count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting
FROM pg_stat_activity;

总连接到 max_connections 的 80% 告警,idle_in_transaction 超过 5 也告警。

验证

  • 跑 2 倍峰值的压力测试;pg_stat_activity 计数应该稳在 max_connections 的 70% 以下。
  • PgBouncer 日志显示客户端连接 vs 服务端连接的高复用比。
  • 正常流量 10 分钟后,idle-in-transaction 数应该接近 0。
  • 副本接到了相当份额的读流量(通过 pg_stat_statements 验证)。

长期预防

  • 所有应用实例的池子配置标准化,把推导过程写文档。
  • 生产环境默认上 PgBouncer(或者 Supavisor / RDS Proxy 这种等价方案)。
  • 数据库层面就把 idle_in_transaction_session_timeoutstatement_timeout 设好默认值。
  • 上线第一天就准备只读副本,报表和分析全走它。
  • 每月复盘连接池告警,把最常触发的收紧。

容易踩的坑

  • max_connections 提到 500 来”解决”问题——内存代价不小(每连接约 10 MB)。
  • PgBouncer 用 session 模式,其实 transaction 就够——放弃了大部分复用收益。
  • statement_timeout 太激进,把合法的长迁移给打断了。
  • 杀连接但不找泄漏源——还会复发。

FAQ

每个应用实例池子设多少? 5 到 10 起步。多数应用比想象中需要的少。有压测证据再加。

PgBouncer 跟 prepared statements 能配合吗? transaction 模式下 PgBouncer 1.21+ 可以。老版本有限制。

serverless 要不要做连接池? 用 serverless 友好的驱动(Neon、Supabase、RDS Proxy),不要在 serverless 里跑原生 Postgres 连接池。

相关阅读

标签: #后端 #排查 #postgres