用低 maxconnections 的 PostgreSQL 容器复现连接耗尽,练习区分泄漏与池配置问题

演练目标

环境准备

安装 Docker,本机 5433 端口空闲。实验使用独立容器,max_connections 故意设得很小。

docker run -d --name lab-pg \
  -e POSTGRES_PASSWORD=labpass \
  -e POSTGRES_DB=lab \
  -p 5433:5432 \
  postgres:16 -c max_connections=20 -c superuser_reserved_connections=3
until docker exec lab-pg pg_isready -U postgres >/dev/null 2>&1; do sleep 2; done
docker exec -i lab-pg psql -U postgres -d lab <<'SQL'
CREATE USER app WITH PASSWORD 'apppass';
GRANT CONNECT ON DATABASE lab TO app;
SQL

创建非超级用户 app 很关键,否则它会占用超级用户保留连接,实验现象不准确。

注入故障

在容器内启动 20 个长期持有连接的客户端,每个执行 pg_sleep(600):

docker exec -d lab-pg bash -c \
  'for i in $(seq 1 20); do PGPASSWORD=apppass psql -U app -d lab -c "SELECT pg_sleep(600)" >/dev/null 2>&1 & done; wait'
sleep 3
docker exec lab-pg psql -U app -d lab -c 'SELECT 1'

普通用户 app 的新连接应报 FATAL: sorry, too many clients already。20 个并发会话中只有约 17 个能建立,因为 3 个连接槽位保留给超级用户。

观察现象

docker exec lab-pg psql -U postgres -d lab \
  -c "SELECT count(*), state FROM pg_stat_activity GROUP BY state ORDER BY 2;"
docker exec lab-pg psql -U postgres -d lab -c "SHOW max_connections;"
docker exec lab-pg psql -U postgres -d lab -c "SHOW superuser_reserved_connections;"
docker exec lab-pg psql -U postgres -d lab \
  -c "SELECT usename, application_name, client_addr, state, count(*)
      FROM pg_stat_activity GROUP BY 1,2,3,4 ORDER BY 5 DESC;"
docker exec lab-pg psql -U app -d lab -c 'SELECT 1'

预期 pg_stat_activity 中 app 的 active 连接接近上限,postgres 仍能新建连接;普通用户的新连接被拒绝。连接状态以 active 为主,因为每个会话都在执行 pg_sleep。

判断与定位

  1. 先区分连接耗尽和连接拒绝:连接耗尽报 too many clients already;服务未监听报 connection refused;认证失败报 password authentication failed。
  2. 按用户、application_name、client_addr 聚合连接,判断是单个实例池过大、连接泄漏,还是多个应用叠加超过上限。
  3. 查看 state:idle in transaction 长期占用连接通常意味着事务未提交;active 过多要看慢查询和阻塞。
  4. 容易误判:超级用户仍能连接,会让人误以为数据库“时好时坏”。这是 superuser_reserved_connections 在起作用,不代表普通应用连接充足。另一个误判是直接调大 max_connections;每个后端都占内存,盲目调大可能把问题变成内存不足。
  5. 还要检查操作系统文件描述符和 ulimit -n,连接数上去后可能先在 OS 层失败。

修复

先终止实验连接,验证普通用户恢复:

docker exec lab-pg psql -U postgres -d lab \
  -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = 'app';"
docker exec lab-pg psql -U app -d lab -c 'SELECT 1'

生产环境应使用 PgBouncer 等连接池,按应用实例数计算池上限,设置连接最大生命周期,修复连接泄漏,并配置 idle_in_transaction_session_timeout。只有在评估内存和连接来源后才考虑提高 max_connections。

清理与回滚

docker exec lab-pg psql -U postgres -d lab \
  -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = 'app';" \
  2>/dev/null || true
docker rm -f lab-pg

容器删除后所有连接和数据消失。若容器仍在运行但连接打满,也可以直接用 docker restart lab-pg 强制断开所有会话,但生产环境不要这样做。

复盘

延伸阅读

读完自测

  1. 为什么用超级用户测试时可能看不到“too many clients”?
  2. idle in transaction 连接过多通常说明什么?
  3. 提高 max_connections 前需要评估什么?
答案
  1. superuser_reserved_connections 为超级用户保留了专用槽位,普通用户已被拒绝时超级用户仍能连接。
  2. 事务开启后长时间未提交或未回滚,连接一直被占用,常见于应用忘记提交或外部调用放在事务内。
  3. 每个后端的内存开销、CPU 并发能力、连接池配置和操作系统文件描述符上限。