Nuxt 3 博客晚高峰转圈:我用连接复用、慢查询超时和静态降级稳住 SSR
一次晚高峰博客全站转圈,问题出在 MongoDB 连接池耗尽和慢查询拖住 SSR。我用服务端连接复用、查询超时和旧缓存降级,把页面从等待里拉出来。

作者
ZHOUYI
晚上十点,博客后台流量上来,首页和文章页集体转圈。看监控不是 500,而是一堆请求挂住。Nuxt SSR 的 useAsyncData 全在等 MongoDB。再往下翻日志,Node 服务端连接池快被占满:每个页面请求都像临时借一把数据库连接,借不到就排队;几条慢查询又把连接捏着不放。页面不是被错误打倒,是被等待拖死。
我一开始怀疑是文章接口写复杂了,后来发现真正的问题在服务端资源:连接没有稳定复用,查询没有明确超时,页面也没有降级路径。三个问题叠在一起,晚高峰只要有一两个慢查询,整个 SSR 就会跟着变慢。
连接复用:别让 SSR 每次请求都重新握手
最早我为了省事,在接口函数里临时创建 MongoClient。开发时看不出来,晚高峰就会暴露:连接建立有握手成本,连接池也有上限。后来改成服务端模块级单例,让 Nitro 进程内复用同一个 client。热更新时可能残留旧实例,生产环境以官方文档和实际进程模型为准。
let client;
export function getDb() {
if (!client) client = new MongoClient(process.env.MONGO_URL, { maxPoolSize: 20, serverSelectionTimeoutMS: 3000 });
return client.db('blog');
}
export async function getHomePosts() {
return getDb().collection('posts')
.find({ status: 'published' }, { projection: { title: 1, slug: 1, summary: 1 } })
.sort({ publishedAt: -1 })
.limit(10)
.maxTimeMS(800)
.toArray();
}这里 maxPoolSize 不是越大越好。连接数太高会把数据库压住,太小又容易排队。我更愿意先设一个保守值,再观察连接等待、慢查询和数据库 CPU,然后小步调整。
慢查询超时:给页面渲染留预算
- 首页列表只查标题、slug、摘要,不拉正文,减少扫描和返回体积。
- 给常用查询加 maxTimeMS,超过预算就失败,不把 SSR 线程吊死。
- 对失败请求返回缓存或骨架,前端仍可阅读,而不是一直转圈。
- 用 explain 和慢日志确认索引,别靠猜;必要时只保留最近发布的文章列表。
静态降级:数据库慢时,先给可读页面
我在 Redis 里存了首页数据和文章摘要,TTL 不长,但保留上一份旧值。数据库正常就刷新缓存;超时或异常就回退旧值。文章页也做类似处理:新内容可以慢一点,但已发布页面尽量能打开。降级不是摆烂,而是把『完整实时』让位给『基本可用』。
“这次给我最大的提醒:SSR 页面的稳定性不只靠前端组件,还要看服务端连接、查询预算和降级路径。先把连接复用、超时和缓存降级做好,很多晚高峰事故就不会变成全站转圈。”