在EC2上使用Morgan自定义Logger出现EMFILE错误如何解决?
问题分析与修复方案
问题根源
你遇到的EMFILE错误是因为系统允许同时打开的文件描述符数量有限。你的代码虽然缓存了日志流,但这些流会一直处于打开状态(因为持续追加日志),finish事件只有在主动调用stream.end()时才会触发,不会自动关闭。随着用户量增加或运行时间变长,打开的文件数会超过系统上限,最终触发错误。
修复方案
方案一:用LRU缓存限制流数量(推荐)
使用lru-cache包管理日志流,设置最大缓存数和闲置超时,自动淘汰并关闭长时间未使用的流,避免耗尽文件描述符。
- 安装依赖:
npm install lru-cache
- 修改流管理代码:
import LRU from 'lru-cache'; import fs from 'fs'; import path from 'path'; import fsExtra from 'fs-extra'; // 配置LRU缓存:最多保存100个流,闲置5分钟自动关闭 const logStreams = new LRU<string, fs.WriteStream>({ max: 100, ttl: 5 * 60 * 1000, // 5分钟超时 dispose: (stream, filePath) => { // 淘汰时主动关闭流 stream.end(() => { console.log(`Closed idle log stream: ${filePath}`); }); }, }); function getLogStream(filePath: string) { let stream = logStreams.get(filePath); if (!stream) { stream = fs.createWriteStream(filePath, { flags: 'a' }); logStreams.set(filePath, stream); } // 更新访问时间,避免被提前淘汰 logStreams.get(filePath); return stream; } // 保留你的认证中间件和morgan配置...
方案二:用专业日志轮转库
使用rotating-file-stream库自动管理日志文件和流,它会按规则分割日志并自动处理流的生命周期,无需手动维护。
- 安装依赖:
npm install rotating-file-stream
- 重构日志流逻辑:
import rfs from 'rotating-file-stream'; import path from 'path'; import fsExtra from 'fs-extra'; function getLogStream(userId: string) { const logDir = path.resolve(__dirname, '../logs', userId); fsExtra.ensureDirSync(logDir); // 按日期自动轮转日志,每天生成一个新文件 return rfs.createStream(`${new Date().toISOString().slice(0, 10)}.log`, { interval: '1d', // 每日轮转 path: logDir, flags: 'a', immutable: false, }); } // 修改日志中间件 app.use((req: any, res, next) => { const userId = req.user.id; const accessLogStream = getLogStream(userId); morgan(':date :method :url :status :res[content-length] - :response-time ms ', { stream: accessLogStream, })(req, res, next); });
方案三:手动定时清理闲置流(无第三方依赖)
如果不想用外部包,可以自己写定时任务,定期关闭长时间未使用的流。
import fs from 'fs'; // 存储流和最后使用时间 const logStreams: { [key: string]: { stream: fs.WriteStream; lastUsed: number } } = {}; // 每10分钟检查一次,关闭30分钟未使用的流 setInterval(() => { const now = Date.now(); for (const [filePath, entry] of Object.entries(logStreams)) { if (now - entry.lastUsed > 30 * 60 * 1000) { entry.stream.end(() => { console.log(`Closed idle stream: ${filePath}`); delete logStreams[filePath]; }); } } }, 10 * 60 * 1000); function getLogStream(filePath: string) { let entry = logStreams[filePath]; if (!entry) { const stream = fs.createWriteStream(filePath, { flags: 'a' }); entry = { stream, lastUsed: Date.now() }; logStreams[filePath] = entry; } else { // 更新最后使用时间 entry.lastUsed = Date.now(); } return entry.stream; }
额外优化建议
- 预定义morgan格式,避免每次请求创建新实例:
const morganLogger = morgan(':date :method :url :status :res[content-length] - :response-time ms '); // 中间件里直接调用 morganLogger(req, res, next); - 可以临时提高系统文件描述符限制(仅临时生效):
ulimit -n 10240 - 增加流创建失败的错误捕获,避免崩溃:
try { stream = fs.createWriteStream(filePath, { flags: 'a' }); } catch (err) { console.error(`Failed to create log stream: ${err}`); // 降级到默认日志流或控制台 return process.stdout; }
内容的提问来源于stack exchange,提问作者Guest
相关产品推荐
相关产品推荐

