You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在EC2上使用Morgan自定义Logger出现EMFILE错误如何解决?

问题分析与修复方案

问题根源

你遇到的EMFILE错误是因为系统允许同时打开的文件描述符数量有限。你的代码虽然缓存了日志流,但这些流会一直处于打开状态(因为持续追加日志),finish事件只有在主动调用stream.end()时才会触发,不会自动关闭。随着用户量增加或运行时间变长,打开的文件数会超过系统上限,最终触发错误。

修复方案

方案一:用LRU缓存限制流数量(推荐)

使用lru-cache包管理日志流,设置最大缓存数和闲置超时,自动淘汰并关闭长时间未使用的流,避免耗尽文件描述符。

  1. 安装依赖:
npm install lru-cache
  1. 修改流管理代码:
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库自动管理日志文件和流,它会按规则分割日志并自动处理流的生命周期,无需手动维护。

  1. 安装依赖:
npm install rotating-file-stream
  1. 重构日志流逻辑:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 21:56:12