连接管理:连接池、PgBouncer 与优雅关闭
本教程共 54 篇 · 第 39 篇 · 更新于 2026-08-11 · 约 7 分钟阅读
本节目标:搞懂连接池怎么工作、怎么调,学会用 PgBouncer 扛高并发,并让应用优雅退出。
数据库能同时接受的连接数有限,每条连接都要占内存。应用要长期稳定运行,第一课就是管好连接。
连接池:v7 由驱动管理
Prisma Client(Prisma 客户端)不会为每条查询单独开连接,而是维护一个连接池(connection pool):查询来了从池里取空闲连接,用完归还。
v7 的连接池由数据库驱动管理。你在构造函数里传入的驱动程序适配器(driver adapter,简称驱动适配器),比如 @prisma/adapter-pg,内部自带连接池。这意味着池的配置不再是 v6 的连接字符串参数,而是适配器构造函数的选项。两者默认值差异很大:
| 行为 | v6 URL 参数 | v6 默认值 | v7 pg 配置字段 | v7 默认值 |
|---|---|---|---|---|
| 池大小 | connection_limit | CPU 数×2+1 | max | 10 |
| 取连接超时 | pool_timeout | 10 秒 | connectionTimeoutMillis | 0(不超时) |
| 建连超时 | connect_timeout | 5 秒 | connectionTimeoutMillis | 0(不超时) |
| 空闲回收 | max_idle_connection_lifetime | 300 秒 | idleTimeoutMillis | 10 秒 |
v6 的池大小随机器 CPU 数变化,v7 固定 10;v6 建连 5 秒超时,v7 默认永不超时。注意 v7 的空闲回收反而变快了:10 秒不用就回收,v6 是 300 秒。想保留 v6 行为,把 v6 默认值手动传给适配器:
连接池在 PrismaClient 第一次连接时创建。触发时机有两种:显式调用 $connect(),或者执行第一条查询(内部自动调用)。懒连接(lazy connection)意味着应用启动时并不连数据库,第一个请求会多等一次建连的时间。对首请求延迟敏感的服务,可以在启动阶段主动 await prisma.$connect()。
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../generated/prisma/client";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
max: 10,
connectionTimeoutMillis: 5_000, // 建连 5 秒超时,对齐 v6
idleTimeoutMillis: 300_000, // 空闲 300 秒回收,对齐 v6
});
export const prisma = new PrismaClient({ adapter });
Notev7 里连接字符串上的 connection_limit、pool_timeout 参数已经失效。调池子只能在适配器构造时传选项。
池大小怎么定?原则是「池大小 × 实例数 < 数据库连接上限」。托管 PostgreSQL 上限常见 100,一台单实例应用配 10 就是合理起点;数据库还有别的客户端(迁移、后台任务、监控)也要占连接,记得留余量。
长驻进程与无服务器环境的策略不同。 长驻服务(传统服务器、Docker)全局只保留一个 PrismaClient 实例并复用,别在每次请求里 new。无服务器环境每个函数实例自带一个连接池,并发一高就很容易打满数据库,这时要么把池调小,要么上外部连接池。热重载开发环境下,把实例挂到 globalThis 上可以防止模块刷新时反复创建新实例。
池耗尽:P2024 与指数退避重试
并发请求太多,或查询在池里排队太久,会抛 P2024:Timed out fetching a new connection from the connection pool。意思是池里没有空闲连接,等待也超时了。
常见诱因:每次请求都 new PrismaClient()(每个实例有独立连接池);Promise.all 一次并发几百条查询;无服务器(serverless)环境并发函数太多,把数据库连接上限打满。
对策有三条:应用全局只保留一个 PrismaClient 实例;池大小乘以实例数要小于数据库连接上限;对瞬态错误做指数退避重试:
async function withRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (e) {
const code = (e as { code?: string }).code;
// 只重试瞬态错误:连不上、池耗尽、事务冲突
if (!["P1001", "P2024", "P2034"].includes(code ?? "") || attempt === retries) {
throw e;
}
await new Promise((r) => setTimeout(r, 100 * 2 ** attempt)); // 100ms、200ms、400ms
}
}
throw new Error("unreachable");
}
PgBouncer:外部连接池
托管 PostgreSQL 的连接上限常见 100 条。应用实例一多,尤其是无服务器场景,很容易打满。PgBouncer 是轻量连接池,挡在应用和数据库之间,用少量数据库连接服务大量客户端。
接入步骤:
- PgBouncer 必须运行在事务模式(Transaction mode)。
- 应用侧连接字符串加
?pgbouncer=true(PgBouncer 1.21.0 及以上官方建议不加)。 - PgBouncer 的 max_prepared_statements 配成大于 0,预编译语句才能复用。
# .env:应用走 PgBouncer,CLI 直连数据库
DATABASE_URL="postgresql://user:***@pgbouncer-host:6432/mydb?pgbouncer=true"
DIRECT_URL="postgresql://user:***@db-host:5432/mydb"
迁移命令必须走直连。Prisma Migrate 的 Schema 引擎按单连接设计,不支持 PgBouncer 连接池,强行走会报「prepared statement already exists」。v7 里直连地址配在 prisma.config.ts:
// prisma.config.ts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
datasource: {
url: env("DIRECT_URL"),
},
});
运行时 Prisma Client 仍用带 pgbouncer=true 的池化地址。两条路分工:应用走池,CLI 走直连。
多连接模式:多租户与读写分离
一个应用连多个库,常见两种。多租户:每个租户一个数据库,给每个租户建一个带各自 URL 的适配器与 PrismaClient。读写分离:主库写、从库读,分别建客户端(v7.1 起官方推荐直接用读取副本扩展,见下一章)。实例要按需创建并缓存复用,否则每个实例一个连接池,数据库吃不消:
const clients = new Map<string, PrismaClient>();
export function getTenantClient(tenantUrl: string): PrismaClient {
let client = clients.get(tenantUrl);
if (!client) {
client = new PrismaClient({
adapter: new PrismaPg({ connectionString: tenantUrl }),
});
clients.set(tenantUrl, client);
}
return client;
}
Tip每个 PrismaClient 实例各有一个连接池。实例数 × 池大小要低于数据库连接上限,否则池调得再合理也会耗尽。
优雅关闭
进程收到 SIGTERM 或 SIGINT 退出时,应该先让连接池关闭,再退出进程。否则数据库侧会残留孤儿连接:
process.on("SIGTERM", async () => {
await prisma.$disconnect();
process.exit(0);
});
Prisma 还提供 beforeExit 钩子:应用被外部信号触发退出时,先执行钩子里的代码再断开。适合在退出前写一条下线日志之类的收尾:
prisma.$on("beforeExit", async () => {
await prisma.auditLog.create({ data: { message: "server shutting down" } });
});
Note官方文档仍记载 beforeExit 钩子,但 v7(无二进制引擎)下是否触发存疑,生产环境建议优先用 SIGTERM 显式断开。
长驻服务不需要每次请求后 $disconnect(),连接要复用。只有临时脚本、以及 Cloudflare Workers 这类释放临时客户端的场景,才值得显式断开。
NoteMongoDB 的连接池由 MongoDB 驱动内部管理,v7 不支持 MongoDB,相关场景留在 v6.19。
小结
连接管理的核心就三件事:池大小和超时按数据库容量调;实例全局复用、按需创建;退出时优雅断开。生产环境再叠一层 PgBouncer 或托管池子,连接问题基本可控。
参考来源
- Prisma 官方文档:Connection pool / Connection management / Database connections
- Prisma 官方文档:Configure Prisma Client with PgBouncer
- Mapagam:Managing Database Connections
- HireNodeJS:Prisma ORM for Node.js: The Complete Production Guide 2026