logo
web
  • Home
  • Pricing
logo
web
Copyright © 2025 web. Ltd.
Links
SubscribeManage Subscription
Powered by Postion - Create. Publish. Own it.
Privacy policy•Terms

Postion

从“P2024”到稳定运行:我如何解决 Next.js + Prisma + PgBouncer 在生产环境的连接池噩梦

从“P2024”到稳定运行:我如何解决 Next.js + Prisma + PgBouncer 在生产环境的连接池噩梦

如果你正在使用 Next.js(或任何 Serverless 架构)、Prisma 和 PostgreSQL 构建应用,你很可能在某个深夜,满怀期待地将应用部署到 Vercel 后,看到过这个让你心跳停止的错误: Error: Timed out fetching a new connection from the connection pool. (P2024) 这个错误就像一个幽灵,它在本地开发环境(npm run dev)中从不出现,却在生产环境的流量高峰期(有时甚至只是几个并发用户)将你的应用炸得粉碎。
b
by buoooou
•Jul 27, 2025

如果你正在使用 Next.js(或任何 Serverless 架构)、Prisma 和 PostgreSQL 构建应用,你很可能在某个深夜,满怀期待地将应用部署到 Vercel 后,看到过这个让你心跳停止的错误:

Error: Timed out fetching a new connection from the connection pool. (P2024)

这个错误就像一个幽灵,它在本地开发环境(npm run dev)中从不出现,却在生产环境的流量高峰期(有时甚至只是几个并发用户)将你的应用炸得粉碎。

在过去的几天里,我完整地经历了从遇到这个错误,到层层排查,再到最终彻底解决的全过程。这篇文章就是我的复盘和总结,希望能帮助同样被此问题困扰的你,省下宝贵的时间和头发。

问题症状:冰山一角

一切都始于那个熟悉的 P2024 错误。日志显示,无论是访问普通页面(prisma.post.findFirst()),还是用户通过 Next-Auth 登录(prisma.account.findUnique()),应用都会随机性地因为无法从连接池获取连接而超时。

我的第一反应是:“数据库连接数不够了?” 于是我检查了 PgBouncer 的配置:MAX_CLIENT_CONN=150,DEFAULT_POOL_SIZE=80。同时我的数据库 max_connections 是 100。从数字上看,绰绰有余。

显然,问题比表面看起来要复杂得多。

探寻真相:为什么连接池会耗尽?

经过一系列的调试和研究,我发现导致问题的不是单一原因,而是三个层层叠加的“坑”。

原因一:错误的“单例模式”——Serverless 环境下的头号杀手

为了避免在每次请求中都创建新的 PrismaClient 实例,我们都知道要使用“单例模式”。我最初的代码是这样的:

Generated javascript

// 错误的单例模式
let prisma: PrismaClient
if (process.env.NODE_ENV === "production") {
  prisma = new PrismaClient()
} else {
  if (!global.cachedPrisma) {
    global.cachedPrisma = new PrismaClient()
  }
  prisma = global.cachedPrisma
}
export const db = prisma

这段代码在传统服务器(如 Express)上没有问题。但在 Serverless 环境中,它是完全错误的!

为什么? Serverless 平台(如 Vercel)为处理并发请求,会启动多个独立的函数实例。我这段代码的逻辑是“如果是生产环境,就直接 new PrismaClient()”。这意味着每一个冷启动的函数实例都会创建一个全新的、拥有独立连接池的 PrismaClient。

假设 Prisma 默认的 connection_limit 是 9。当 10 个用户同时访问,触发 10 个函数实例冷启动时,我的应用会瞬间向数据库请求 10 * 9 = 90 个连接!这足以压垮任何配置合理的 PgBouncer。

原因二:连接风暴——即使单例也可能失控

在修复了单例模式后,我观察到情况有所好转,但 P2024 错误依然在流量稍高时出现。为什么?

即便我们确保了全局只有一个 PrismaClient 实例,但这个实例本身也会维护一个内部连接池。默认情况下,这个池的大小是 num_of_cpus * 2 + 1。在 Serverless 环境中,这可能意味着 5 到 10 个连接。

当大量请求涌入,多个“温”函数实例同时向这一个共享的 Prisma 实例请求连接时,仍然可能在短时间内耗尽 PgBouncer 的可用连接。这就像虽然全公司共用一个咖啡机,但早晨 9 点所有人同时去接咖啡,咖啡机还是会不堪重负。

原因三:终极 Boss——Prepared Statements 与 PgBouncer 的兼容性陷阱

这是最隐蔽,也是最终极的问题。在我修复了前两个问题,并且在连接字符串里加上了 connection_limit=5 来限制每个实例的行为后,错误依然偶尔浮现。

我查看 PgBouncer 的日志,发现了大量奇怪的警告:
WARNING ... FIXME: query end, but query_start == 0

这个警告暗示 PgBouncer 对客户端(Prisma)发送的指令感到“困惑”。经过查阅资料,我发现元凶是 Prepared Statements (预编译语句)。

  • Prisma 为了性能,会大量使用预编译语句。

  • PgBouncer 在 transaction(事务)池模式下,对预编译语句的支持存在缺陷。它可能无法在事务结束后正确地释放一个连接,导致这个连接被“卡住”。

  • 随着时间推移,被卡住的连接越来越多,连接池被缓慢耗尽,最终导致新的请求超时。

解决方案:一套组合拳彻底根治

针对以上三个根本原因,我采用了下面这套组合方案,最终让应用在生产环境稳定如飞。

第一步:实现一个真正 Serverless 安全的单例

这是最基础,也是最重要的一步。我将创建 Prisma 实例的代码修改为以下唯一正确的版本:

Generated typescript

// lib/db.ts
import { PrismaClient } from '@prisma/client'

declare global {
  // eslint-disable-next-line no-var
  var prisma: PrismaClient | undefined
}

// 无论在什么环境下,都优先从全局缓存中获取实例
export const db = global.prisma || new PrismaClient()

// 在非生产环境中,将实例挂载到全局对象上,防止热重载导致实例泛滥
if (process.env.NODE_ENV !== 'production') {
  global.prisma = db
}

这个版本确保了在任何情况下,一个函数实例的生命周期内,PrismaClient 只会被创建一次。

第二步:配置“安全带”——优化数据库连接字符串

仅仅修复代码是不够的,我们还需要从配置层面“驯服”Prisma。我修改了 .env 文件中的 DATABASE_URL,为它加上了几个关键参数:

Generated env

DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DB?schema=public&pgbouncer=true&connection_limit=5&timeout=30"
  • pgbouncer=true:明确告知 Prisma 它正通过连接池工作,让它采取更兼容的模式。

  • connection_limit=5:这是安全带! 它强制规定每个 PrismaClient 实例(即每个函数实例)最多只能占用 5 个数据库连接。这能有效防止单个实例的连接风暴耗尽整个池。

  • timeout=30:将连接超时时间从默认的 10 秒延长到 30 秒。这为应用在网络波动或高负载下提供了更多的容错空间。

第三步:解决兼容性——禁用 Prepared Statements

为了解决 Prisma 与 PgBouncer 的根本冲突,我加入了最后一个,也是最关键的参数:

Generated env

DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DB?schema=public&pgbouncer=true&connection_limit=5&timeout=30&pga_prepared_statements=false"
  • pga_prepared_statements=false:这个 Prisma 独有的参数,指示它不要使用会话级别的预编译语句。这彻底避免了与 PgBouncer transaction 模式的冲突,杜绝了连接被“卡住”的现象。

结论

Serverless 数据库连接管理是一个复杂的领域。P2024 错误并非单一问题,而是一系列架构、代码和配置问题的综合体现。

回顾整个过程,解决问题的关键路径是:

  1. 实现真正安全的单例模式,杜绝 PrismaClient 滥建。

  2. 通过 connection_limit 为每个实例戴上“紧箍咒”,防止连接风暴。

  3. 通过 pga_prepared_statements=false 解决深层次的兼容性问题,确保连接被正确释放。

如果你也遇到了同样的问题,希望我的这段踩坑经历能为你提供一张清晰的路线图,让你能直达问题核心,快速恢复服务的稳定。

Comments (0)

Continue Reading

How to Change the Global Background Color in Your Next.js + Tailwind CSS App

When building a modern web application with Next.js and Tailwind CSS, you'll often want to set a custom global background color that aligns with your brand identity. A common mistake is to hardcode the color directly onto the <body> tag, which can break theming capabilities like dark mode.

Published Jul 20, 2025

一文读懂点击劫持(Clickjacking)的危害与修复方案

在网络世界中,一个看不见的威胁可能正潜伏在看似无害的按钮或链接之下。用户的一次无心点击,可能导致账户被盗、信息泄露,甚至造成财产损失。这个“隐形杀手”就是——点击劫持(Clickjacking)。

Published Jul 18, 2025

深入解析 DNS CAA 记录

Published Jul 18, 2025