1. zeit/micro 里 Mongoose 连 Mongo 为什么总在冷启动翻车
如果你正在用 zeit/micro 写轻量接口,又想让数据落到 MongoDB,大概率会搜到「zeit micro mongoose mongo 连接复用」这类关键词。micro 本身极简,一个module.exports = (req, res) => {}就能跑起来,但它没有内置数据库生命周期管理。Serverless 环境下每次冷启动都会重新执行模块顶层代码,如果你把mongoose.connect()直接写在文件顶部,就会出现两个典型问题:一是每次请求都新建连接,连接数暴涨;二是函数实例被回收后连接悬空,下一次请求拿到的是失效句柄,报MongooseError: Operation buffering timed out或者Cannot use a closed connection。
我试过在本地micro-dev跑得好好的,一部署到 Serverless 平台就间歇性 500,日志里全是buffering timed out after 10000ms。根因不是 Mongoose 本身,而是连接没有做「实例级复用 + 就绪等待」。micro 的请求处理函数是 async 的,但模块加载阶段是同步的,mongoose.connect()返回 Promise,你不 await 它,第一个请求进来时连接还没建立,Mongoose 默认会缓冲操作,缓冲超时就抛错。
这篇要解决的就是这条链路:用 Mongoose 在 zeit/micro 里建立可复用的 Mongo 连接,处理冷启动、连接超时、读写验证。适合已经在写 micro 接口、准备接 MongoDB、或者被 Serverless 连接问题卡住的开发者。下面从连接封装开始,一步步给出可复制的配置和验证请求。
2. TaoToken 前置:给 micro 项目准备模型调用与 Key 管理
在写数据库层之前,先把项目里可能用到的模型调用能力准备好。很多 micro 服务除了读写 Mongo,还会调用大模型做日志分类、内容摘要。TaoToken 提供统一的 API 入口,兼容常见模型调用格式,适合放在 Serverless 函数里做轻量推理。
你需要先拿到 API Key。打开 https://taotoken.net/api-keys ,登录后创建一个 Key,复制保存。这个 Key 就是后面所有请求的凭证,不要写死在代码里,放到环境变量。
模型调用入口是 https://taotoken.net/api ,它兼容 OpenAI 风格的/v1/chat/completions。在 micro 里你可以直接用node-fetch或内置https模块请求。如果你要做长期编码或 Agent 类任务,可以看 Coding Plan:https://taotoken.net/coding-plan ,它更适合持续性的代码生成场景。想先验证模型是否通,用模型对话页面:https://taotoken.net/chat ,发一条消息看返回。
接入文档在 https://taotoken.net/doc ,里面有 Base URL、鉴权头、请求体格式的完整说明。控制台在 https://taotoken.net/console ,可以查看用量和调用记录。Claude Code 相关接入参考 https://taotoken.net/claude-code 。
这里的关键是:micro 函数是无状态的,Key 必须通过环境变量注入,不能硬编码。本地开发用.env,部署时在平台的环境变量面板里配置。下面第三节会给出完整的连接封装和配置片段。
3. 可复制配置:Mongoose 连接封装与 micro 路由
先装依赖。micro 生态的核心包是micro、micro-dev、microrouter,数据库用mongoose。
npm install micro micro-dev microrouter mongoose --savepackage.json的 scripts 建议这样写,本地开发用micro-dev带热重载,生产用micro:
{ "name": "micro-mongo", "version": "1.0.0", "main": "index.js", "scripts": { "dev": "micro-dev -p 5670", "start": "micro -p 5500" }, "dependencies": { "micro": "^9.3.4", "microrouter": "^3.1.3", "mongoose": "^7.6.3" }, "devDependencies": { "micro-dev": "^3.0.0" } }连接封装单独放db.js。核心是「缓存连接 Promise」,而不是缓存连接对象。这样并发请求进来时,大家 await 同一个 Promise,不会重复建连。
// db.js const mongoose = require('mongoose'); const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/micro_log'; let cached = global.__mongooseCache; if (!cached) { cached = global.__mongooseCache = { conn: null, promise: null }; } async function connectWithDB() { if (cached.conn) { return cached.conn; } if (!cached.promise) { const opts = { serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, maxPoolSize: 10, bufferCommands: false }; cached.promise = mongoose.connect(MONGODB_URI, opts).then((m) => m); } cached.conn = await cached.promise; return cached.conn; } module.exports = connectWithDB;注意bufferCommands: false,这样连接没就绪时操作会立刻报错,而不是缓冲 10 秒再超时,排障更快。maxPoolSize控制单实例连接数,Serverless 下别设太大。
模型定义放model/log.js,用mongoose.models.Log做存在性判断,避免热重载时重复注册模型报OverwriteModelError:
// model/log.js const mongoose = require('mongoose'); const LogSchema = new mongoose.Schema({ name: { type: String, required: 'Please fill name', trim: true }, time: { type: String }, type: { type: String }, status: { type: String }, tag: { type: String }, handler: { type: String, default: 'SCM' }, description: { type: String, default: 'SystemLog' } }, { timestamps: true }); module.exports = mongoose.models.Log || mongoose.model('Log', LogSchema);micro 入口index.js,用 microrouter 组织路由,每个 handler 先 await 连接再操作:
// index.js const { send, json } = require('micro'); const { router, get, post } = require('microrouter'); const connectWithDB = require('./db'); const Log = require('./model/log'); const notfound = (req, res) => send(res, 404, { msg: 'Not found route' }); const create = async (req, res) => { await connectWithDB(); const data = await json(req); const log = new Log(data); await log.save(); send(res, 200, { msg: 'create-ok', id: log._id }); }; const findDatas = async (req, res) => { await connectWithDB(); const datas = await Log.find({}).limit(50).lean(); send(res, 200, datas); }; const findDatasByTime = async (req, res) => { await connectWithDB(); const data = await json(req); const p = { time: { $gte: data.startTime, $lt: data.endTime } }; const datas = await Log.find(p).lean(); send(res, 200, datas); }; module.exports = router( get('/loglist', findDatas), post('/logcreate', create), post('/loglist', findDatasByTime), get('/*', notfound) );环境变量在本地用.env配合micro-dev加载,或者直接export MONGODB_URI=...。部署时在平台环境变量里配置MONGODB_URI,格式是mongodb+srv://user:pass@host/dbname或标准连接串。
4. 验证请求:本地与部署环境的读写链路确认
先本地起服务:
npm run dev默认监听 5670。用 curl 验证写入:
curl -X POST http://localhost:5670/logcreate \ -H "Content-Type: application/json" \ -d '{"name":"alarm-001","time":"2024-06-01T10:00:00Z","type":"cpu","status":"open","tag":"prod"}'预期返回:
{"msg":"create-ok","id":"665f1a2b3c4d5e6f7a8b9c0d"}再验证读取:
curl http://localhost:5670/loglist预期返回一个数组,包含刚写入的记录。如果返回[],说明写入到了不同数据库或集合,检查MONGODB_URI里的 dbname。
按时间范围查询:
curl -X POST http://localhost:5670/loglist \ -H "Content-Type: application/json" \ -d '{"startTime":"2024-06-01T00:00:00Z","endTime":"2024-06-02T00:00:00Z"}'部署后把域名替换掉本地地址,重复上面三条请求。Serverless 平台第一次请求会触发冷启动,可能慢 1-2 秒,第二次开始走复用连接,响应明显变快。你可以连续发 10 次/loglist,观察日志里connectWithDB是否只打印一次连接信息。如果每次都打印,说明缓存没生效,检查global.__mongooseCache是否被平台重置,或者模块是否被重复加载。
验证模型调用链路时,用环境变量里的 Key 请求:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}'返回里有choices[0].message.content就说明 Key 和网络都通。这一步和 Mongo 无关,但能确认你的 micro 函数在部署环境里能正常发出外部请求。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
报错一:MongooseError: Operation buffering timed out after 10000ms这是连接没就绪就执行查询。检查两点:connectWithDB()是否在每个 handler 里 await 了;bufferCommands是否设成了false。如果设了 false 还报这个错,说明连接根本没建立,看MONGODB_URI是否正确、Mongo 是否允许当前 IP 访问。
报错二:MongoServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017本地没起 Mongo,或者连接串指向了 localhost 但部署环境里没有本地 Mongo。部署时必须用云数据库连接串,通过环境变量注入。
报错三:OverwriteModelError: Cannot overwrite 'Log' model once compiled.热重载时重复注册模型。用mongoose.models.Log || mongoose.model('Log', LogSchema)解决,已经在第 3 节代码里处理了。
报错四:模型调用返回 401{"error":{"message":"Invalid API key","type":"invalid_request_error"}}。检查Authorization头是否是Bearer <Key>,Key 是否有多余空格,环境变量是否真的注入到了运行环境。在 TaoToken 控制台 https://taotoken.net/console 确认 Key 状态。
报错五:local proxy failed或连接超时这类错误通常出现在函数运行环境无法直连外部服务时。检查平台是否限制了出站网络,或者连接串里的 host 是否可达。TaoToken 的 API 入口是标准 HTTPS,正常环境可直接请求。
报错六:Cannot read properties of undefined (reading 'choices')模型返回体结构和你解析的字段不匹配。先打印完整响应体,确认choices存在。常见原因是请求被拦截返回了 HTML 错误页,或者用了错误的 endpoint。确认请求的是https://taotoken.net/api/v1/chat/completions。
报错七:OAuth 相关错误如果你在接入 Claude Code 或类似工具时遇到 OAuth 报错,检查回调地址和 token 是否过期。Claude Code 接入参考 https://taotoken.net/claude-code ,按文档里的步骤重新授权。
排查顺序建议:先确认 Mongo 连接串和网络,再确认 Mongoose 缓存逻辑,最后确认模型调用的 Key 和 endpoint。每一步都用最小请求验证,不要一次改多个地方。
6. 把连接复用和 Key 管理固定成项目模板
micro + Mongoose 这套组合的关键就三件事:连接 Promise 缓存、bufferCommands: false、每个 handler await 连接。把db.js和model/log.js抽成模板,新项目直接复制,能省掉大量冷启动排障时间。
模型调用这边,Key 走环境变量,Base URL 用 https://taotoken.net/api ,需要长期跑编码任务就上 Coding Plan,临时验证用模型对话页面。接入文档在 https://taotoken.net/doc ,API Key 在 https://taotoken.net/api-keys 创建。
最后留一个实用技巧:在connectWithDB里加一行连接事件监听,mongoose.connection.on('error', console.error),部署后日志里能第一时间看到连接断开,比等请求超时再查快得多。