返回探索
指南VibeFix 编辑部更新于 2026年10月17日

FATAL: too many clients already:vibe 项目的数据库连接池实战

FATAL: too many clients already 不是慢查询、也不是选错了库,而是连接数耗尽。这篇讲池化算账公式、PgBouncer 三种模式决策、Supabase/Neon 自带 pooler 接法、Prisma 的 prepared statement 大坑,以及排错对照表。

夜间高速公路立交桥航拍照片,灯光勾勒出复杂的车流路线,隐喻数据库连接的调度与池化

先划清边界:这篇不讲什么

数据库话题站内已经很挤,先说明分工:

  • 《vibe coding 项目选数据库》讲的是「选哪个库」——Postgres / MySQL / SQLite / Mongo 怎么选;
  • 《vibe 项目的慢查询与索引实战》讲的是「SQL 怎么变快」——explain、索引、N+1;
  • 《vibe 项目的 Serverless 成本优化》讲的是「云账单怎么降」——函数、带宽、调用次数;
  • 《vibe 项目的后台任务与队列》讲的是「异步怎么做」——BullMQ、重试、死信。

本篇只回答一个问题:「连接数不够用了怎么办。」不是 SQL 慢,不是库选错,是 FATAL: too many clients already——应用拿不到连接,请求直接失败。这是 vibe 项目从「能跑」到「能扛」的必经之坑。

一个连接到底有多贵

PostgreSQL 是进程模型:每个连接对应一个后端进程,连上之后常驻内存。一个空闲连接大约吃掉 5–10MB 内存(work_mem、 buffers 分摊),100 个连接就是近 1GB。Supabase 免费档的 max_connections 默认 60 左右,Neon 免费档更低——而你的应用侧呢?Prisma 默认连接池、加上 2 个 serverless 实例、再加一个后台 worker,轻轻松松超过 60。

最要命的是 serverless:Vercel / Cloudflare Workers 每来一个请求都可能新建连接,流量一涨,连接数呈「风暴」式增长。数据库不是被查垮的,是被连垮的。

先算账:你离撞墙还有多远

公式很简单:

所需连接数 ≈ 应用实例数 × 单实例连接池大小 + 后台任务连接数 + 人工运维连接数

举例:Next.js 跑 3 个实例(Prisma 默认池 10)+ 1 个 worker(池 5)+ 你本地 psql 留 2 个 = 3×10 + 5 + 2 = 37。看着离 60 还远?serverless 场景下「实例数」是弹性的,促销一来变成 20 个实例,20×10 = 200,直接撞墙。

折线图:应用实例数增长时,直连的数据库连接数线性上升并撞上 max_connections 红线,而池化后的连接数被钉死在池上限

连接池解决的正是这个问题:把「应用以为自己有多少连接」和「数据库实际有多少连接」解耦。应用侧可以开 200 个逻辑连接,池子只维持 25 个物理连接,复用给真正需要执行的查询。

三层池化架构:你在哪一层动手

层级是什么适合谁
应用内连接池Prisma / Drizzle / SQLAlchemy 自带的池所有人,第一道防线;但管不住多实例
PgBouncer(自建)独立的连接池中间件,跑在应用和 DB 之间自有 VPS / Docker 部署,有运维能力
云厂商 poolerSupabase(Supavisor)、Neon 的池化连接串用 BaaS 的人,零运维,首选

一人团队的推荐顺序:先用云 pooler → 不够再自建 PgBouncer → 应用内池永远开着。三层不是互斥的,是叠加的。

PgBouncer 三种模式:选错等于没池化

这是整篇最重要的决策表,选错模式,池子形同虚设:

模式连接什么时候归还适用场景禁忌
session客户端断开才归还长连接、需要 LISTEN/NOTIFY、 advisory lockserverless 短连接:池化效果几乎为零
transaction每个事务结束就归还Web 应用默认选这个,serverless 必选不能用 prepared statement、不能用 LISTEN/NOTIFY、事务内不能切库
statement每条语句结束就归还极少用,多语句事务直接报错几乎所有 ORM 都不兼容

一句话记忆:vibe 项目的 Web 服务,无脑选 transaction 模式。代价是放弃 prepared statement——而这正是下面 Supabase 那个著名大坑的根源。

实战 A:Supabase / Neon 自带 pooler(零运维路线)

Supabase 的连接串面板里有两条:直连(5432 端口)和 Pooler(6543 端口,Supavisor,transaction 模式)。Neon 同理提供 pooled 连接串。一人团队:应用一律走 pooler 端口,直连端口只留给迁移(prisma migrate)和你本地 psql。

Prisma 用户必看的坑——transaction 模式不支持 prepared statement,Prisma 默认是开启的,必须关掉:

// schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")        // 6543 pooler,用于运行时
  directUrl = env("DIRECT_DATABASE_URL") // 5432 直连,仅用于 migrate
}
// prisma client 初始化
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool, {
  // 关键:transaction 池模式下必须禁用 prepared statements
  // 否则报错:prepared statement "s0" does not exist
});
export const prisma = new PrismaClient({ adapter });

如果你用旧版 prisma-client-js 直连(无 adapter),连接串后面加参数:?pgbouncer=true&connection_limit=5,Prisma 会自动禁用 prepared statement。Drizzle(node-postgres 驱动)同理:new Pool({ connectionString }) 走 6543 即可,node-postgres 默认不用 prepared statement,反而省心。

Neon 用户注意:Neon 的 pooled 连接要求同样逻辑,且免费档的「compute 自动休眠」会导致首个请求慢——那是另一个话题(冷启动),别和连接池混淆。

实战 B:自建 PgBouncer(Docker 一把梭)

用 VPS + Docker Compose 的人,加一个服务就行:

services:
  pgbouncer:
    image: edoburu/pgbouncer:1.22
    restart: unless-stopped
    environment:
      DATABASES_HOST: db
      DATABASES_PORT: 5432
      DATABASES_USER: postgres
      DATABASES_PASSWORD_FILE: /run/secrets/db_password
      DATABASES_DBNAME: appdb
      POOL_MODE: transaction      # 默认就选 transaction
      MAX_CLIENT_CONN: 200        # 应用侧逻辑连接上限
      DEFAULT_POOL_SIZE: 25       # 真实打到 PG 的物理连接
      MIN_POOL_SIZE: 5
      RESERVE_POOL_SIZE: 5        # 突发流量缓冲
      RESERVE_POOL_TIMEOUT: 3
      STATS_USERS: postgres
    secrets: [db_password]
    ports: ["127.0.0.1:6432:5432"]  # 只给本机应用用,不暴露公网
    depends_on:
      db: { condition: service_healthy }

应用的 DATABASE_URL 改成 postgres://postgres:xxx@pgbouncer:5432/appdb(compose 内网走服务名)。参数就调两个:DEFAULT_POOL_SIZE(物理连接数,按 DB 内存定:每连接 10MB,1GB 内存给 25 个留足余量)和 MAX_CLIENT_CONN(逻辑上限,给足 200,别让应用排队)。

验证池子在工作:连上 pgbouncer 的管理库 psql -h 127.0.0.1 -p 6432 pgbouncer,执行 SHOW POOLS;——cl_active 是应用逻辑连接,sv_active 是真实物理连接。压测时看 sv_active 被钉在 25 不动,就说明池化生效了。

应用侧池大小:一句话公式

应用内池(Prisma connection_limit、SQLAlchemy pool_size)也不是越大越好。经验公式:

单实例池大小 = CPU 核数 × 2 + 有效磁盘数(单机一般按 1 算)

2 核 VPS:池大小 5。超过这个数,多出来的连接只是在排队,还占着内存。vibe 项目的典型错误是「默认配置一把梭」——Prisma 默认连接数公式是 num_cpus * 2 + 1,在 8 核开发机上是 17,部署到 1 核小 VPS 上如果还用环境变量覆盖成 20,纯属浪费。显式设置 connection_limit=5,把省下来的连接数留给池子统一调度。

排错手册:4 个报错对号入座

报错根因解法
FATAL: too many clients already物理连接超过 max_connections上 PgBouncer / 云 pooler;检查是否有连接泄漏(只借不还)
prepared statement "s0" does not existtransaction 模式下用了 prepared statementPrisma 加 ?pgbouncer=true 或 adapter 模式;Drizzle 检查是否开了 prepare
server closed the connection unexpectedly云 DB 空闲断开 / pooler 踢掉空闲连接应用池配 idleTimeout 小于云端超时;加连接健康检查
高峰期偶发 timeout 但连接数没满池子排队:逻辑连接在等物理连接看 SHOW POOLS 的 cl_waiting;适当调大 DEFAULT_POOL_SIZE 或优化慢查询(见慢查询指南)

监控:两个 SQL 盯住连接数

-- 当前各状态连接数,一眼看出有没有泄漏
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;

-- 谁占着连接不放(按空闲时长排序,揪出泄漏源)
SELECT pid, usename, application_name, state,
       now() - state_change AS idle_for, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY idle_for DESC LIMIT 10;

idle in transaction 是头号嫌疑犯:事务开了没提交,连接一直占着。ORM 里常见的写法是 prisma.$transaction 里调了外部 API——网络一慢,连接就被钉死。修法:事务里只放 DB 操作,外部调用挪到事务外。

告警阈值:连接数超过 max_connections 的 70% 就告警,别等到 100%。配合状态页指南里的对外沟通,至少做到「先知道、再修」。Uptime 监控只看 HTTP 200 是不够的,加一个定时任务每分钟跑第一个 SQL,超阈值就发通知。

上线前 checklist

  • ☐ 应用走 pooler 端口(6543/6432),直连端口只用于 migrate 和人工运维
  • ☐ transaction 模式 + prepared statement 已禁用(Prisma 必查)
  • ☐ SHOW POOLS 验证 sv_active 被钉在预期值
  • ☐ 单实例 connection_limit 按公式显式设置,不过大
  • ☐ 事务里没有外部 API 调用(防 idle in transaction 泄漏)
  • ☐ 连接数 70% 告警已配
  • ☐ 压测过:模拟 5 倍流量,确认排队可控、无 too many clients

总结一句话:连接数是 vibe 项目最便宜的扩容瓶颈——不是加机器,是加一层池子。公式算清楚、模式选对、prepared statement 关掉,80% 的「数据库扛不住」到此为止。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...
浏览项目广场发布你的项目

相关文章

实拍:数据中心机房内成排的服务器机柜,呼应数据库归档主题
指南
表撑到一亿行之前:vibe 项目的数据归档与冷热分离实战

当只增不减的表撑过内存,备份和查询会一起衰减。vibe 项目归档实战:四个动手信号、三种策略对照(原生分区+pg_partman vs 归档表 vs Parquet 冷导出)、幂等可重入的归档作业状态机、归档后查询路由、回查解冻,以及归档与备份、合规保留期的关系。

数据工作流数据库后端工程
实拍:程序员紧盯显示器上的代码逐行检查
指南
别再 JSON.parse 裸奔:AI 结构化输出的校验与修复实战

prompt 是建议,不是约束。AI 结构化输出可靠性的四层防线:模型原生结构化输出(OpenAI vs Anthropic,官方文档核实)、zod 校验的严格/宽松取舍、把报错原文喂回的修复重试、按成本选的降级兜底。

后端工程项目创作独立开发
实拍:工业控制面板上的红色急停按钮
指南
上线不靠祈祷:vibe 项目的功能开关与应急关停实战

功能开关把部署和发布解耦:代码先上,功能后开。四种开关类型与选型矩阵、一张表的最小实现、兜底铁律(新功能默认关、止血开关冻结)、开关腐烂治理,以及开关与灰度发布如何配合。

部署上线后端工程项目创作