Next.js+MongoDB在Vercel动态路由遇504网关超时问题求助
问题描述
使用MongoDB共享集群 + Vercel免费版,本地环境无超时问题,MongoDB连接采用Mongoose复用同一连接;但在Vercel上首次访问SSG生成的课程/班级动态路由时出现504超时,后续路由缓存后加载正常(最多2秒)。
相关代码
mongodb.js
if (!process.env.MONGODB_URI) { throw new Error('Invalid/Missing environment variable: "MONGODB_URI"') } const uri = process.env.MONGODB_URI const options = {} let client let clientPromise if (process.env.NODE_ENV === 'development') { if (!global._mongoClientPromise) { client = new MongoClient(uri, options) global._mongoClientPromise = client.connect() } clientPromise = global._mongoClientPromise } else { client = new MongoClient(uri, options) clientPromise = client.connect() } // SSR export async function getServerSideProps(context: any) { await db() const { params, query, req, res } = context const session = await getSession({ req }) const cookies = parseCookies(context) const userCookie = cookies?.user ? JSON.parse(cookies.user) : session?.user const email = userCookie?.email const { classId, id } = params const clase = await getClassById(classId, id) const user = await updateActualCourseSS(email, id, classId) return { props: { clase, user } } }
getClassById
import connectDB from "../../../config/connectDB" import Course from "../../../models/courseModel" import User from "../../../models/userModel" import Class from "../../../models/classModel" import bcrypt from "bcryptjs" export async function getClassById(id, courseId) { try { const res = await Class.where('id').equals(id).populate({ path: 'course', match: {id: courseId} }).exec() const courseToSend = res.filter(r => r.course != null) const clase = JSON.parse(JSON.stringify(courseToSend[0])) return clase } catch (err) { console.log(err) } }
updateActualCourseSS
export async function updateActualCourseSS(email: string, courseId: string, actualChapter: number) { try { console.log('connected') const user: any | null = await User.findOne({ email: email }).exec() const courseDB: CoursesDB | null = await Course.findOne({ id: courseId}).exec() const index = user?.courses.findIndex((course: CourseUser) => course.course.valueOf() === courseDB?._id.valueOf()) index != null && user != null ? user.courses[index].actualChapter = actualChapter : null user.courses[index].actualTime = 0; await user?.save() return JSON.parse(JSON.stringify(user)); } catch (err) { console.log(err) } }
问题分析与解决方案
核心原因
Vercel免费版Serverless函数冷启动+MongoDB共享集群首次连接延迟,叠加getServerSideProps里的串行数据库操作,导致首次请求触发504超时(Vercel免费版Serverless函数超时阈值为10秒,跨区域连接+冷启动的耗时容易触达上限)。
具体优化措施
- 修复生产环境MongoDB连接复用逻辑
当前生产环境每次请求都会创建新连接,改成和开发环境一致的全局连接复用:
// 修改mongodb.js生产环境代码段 if (process.env.NODE_ENV === 'development') { if (!global._mongoClientPromise) { client = new MongoClient(uri, options) global._mongoClientPromise = client.connect() } clientPromise = global._mongoClientPromise } else { // 生产环境复用全局连接(Vercel同实例生命周期内会保留全局变量) if (!global._mongoClientPromise) { client = new MongoClient(uri, options) global._mongoClientPromise = client.connect() } clientPromise = global._mongoClientPromise }
- 并行化数据库操作
getServerSideProps中getClassById和updateActualCourseSS原本串行执行,改成并行减少总耗时:
export async function getServerSideProps(context: any) { await db() const { params, query, req, res } = context const session = await getSession({ req }) const cookies = parseCookies(context) const userCookie = cookies?.user ? JSON.parse(cookies.user) : session?.user const email = userCookie?.email const { classId, id } = params // 并行执行两个异步操作 const [clase, user] = await Promise.all([ getClassById(classId, id), updateActualCourseSS(email, id, classId) ]) return { props: { clase, user } } }
- 优化数据库查询性能
- 给高频查询字段加索引:
// 在对应Model文件中添加索引 // classModel.js classSchema.index({ id: 1 }); // courseModel.js courseSchema.index({ id: 1 }); // userModel.js userSchema.index({ email: 1 });
- 简化
getClassById查询逻辑,避免内存过滤:
export async function getClassById(id, courseId) { try { const clase = await Class.findOne({ id }) .populate({ path: 'course', match: { id: courseId } }) .exec(); return clase?.course ? JSON.parse(JSON.stringify(clase)) : null; } catch (err) { console.log(err) } }
- 添加超时与容错处理
- 给MongoDB连接添加超时限制:
const options = { serverSelectionTimeoutMS: 5000, // 5秒未连接成功则抛出错误 socketTimeoutMS: 5000 // 读写操作超时时间 };
- 完善
updateActualCourseSS的空值处理:
export async function updateActualCourseSS(email: string, courseId: string, actualChapter: number) { try { if (!email) return null; const user: any | null = await User.findOne({ email }).exec() if (!user) return null; const courseDB: CoursesDB | null = await Course.findOne({ id: courseId}).exec() if (!courseDB) return user; const index = user.courses.findIndex((course: CourseUser) => course.course.valueOf() === courseDB._id.valueOf()) if (index !== -1) { user.courses[index].actualChapter = actualChapter; user.courses[index].actualTime = 0; await user.save(); } return JSON.parse(JSON.stringify(user)); } catch (err) { console.log(err) return null; } }
内容的提问来源于stack exchange,提问作者Mateo Molfino
相关产品推荐
相关产品推荐

