Skip to main content

异常监控与日志管理

在 Node.js 中,由于单线程的特性,一个未捕获的异常可能会导致整个进程崩溃,所有用户的请求都会受到影响。因此,异常处理机制日志收集是高级后端面试的必考题。


异常捕获机制与兜底策略

🔥 面试题:Node.js 中如何捕获异常?如果发生未捕获的异常怎么处理?

1.1 常见的异常捕获方式

  1. 同步代码:使用 try...catch
  2. 异步回调 (Callback):遵循 Node.js 的 Error-First 规范,即回调函数的第一个参数是 error
    fs.readFile("file.txt", (err, data) => {
    if (err) return console.error("出错了", err);
    });
  3. Promise / Async/Await:使用 .catch() 或在 async 函数中使用 try...catch

1.2 全局兜底:未捕获异常的处理

如果代码中漏写了 try...catch 或者 .catch(),异常就会冒泡到全局。Node.js 提供了两个全局事件来进行兜底捕获:

  1. uncaughtException:捕获未处理的同步异常。
  2. unhandledRejection:捕获 Promise 中抛出且未被处理的异常。
process.on("uncaughtException", (err) => {
console.error("致命错误: 未捕获的同步异常", err);
// 最佳实践:记录日志后,主动退出进程,由 PM2 重启
process.exit(1);
});

process.on("unhandledRejection", (reason, promise) => {
console.error("警告: 未处理的 Promise 拒绝", reason);
// Node 15+ 之后,unhandledRejection 默认会导致进程崩溃
});

🔥 高级连环问:为什么 uncaughtException 发生后,最佳实践是强制退出进程(process.exit(1)),而不是当做没发生继续运行?

:因为当抛出 uncaughtException 时,意味着应用已经处于不可预测的状态。例如:某个全局对象可能被污染、某个数据库连接可能没有释放、某个内存泄漏正在发生。如果不退出进程,接下来的请求可能会得到错误的数据,或者导致更严重的内存溢出。 正确的做法是:“让它崩溃” (Let it crash),记录日志后退出,依靠进程守护工具(如 PM2 / K8s)自动拉起一个干净的新实例。


日志管理最佳实践 (Logging)

🔥 面试题:生产环境能直接用 console.log 吗?为什么?大厂是怎么做日志管理的?

为什么抛弃 console.log

  1. 性能问题:在写入终端或文件时,console.log 有时是同步阻塞的(尤其是当目标是文件且磁盘 I/O 慢时),会严重拖慢事件循环。
  2. 缺乏级别管理:无法区分 INFOWARNERRORDEBUG。线上无法做到动态调整日志级别。
  3. 不易于检索和分析:纯文本输出很难被日志分析系统(如 ELK)格式化提取。

现代日志框架推荐

在 Node.js 中,最主流的两个日志库是 WinstonPino

  • Winston:老牌经典,生态极好。支持多 Transport(比如同时输出到控制台、写入文件、发送到日志服务器)。
  • Pino:主打极速 (Super fast)。通过异步日志和极其优化的序列化机制,性能远超 Winston,是现代高并发框架(如 Fastify)的标配。

大厂日志规范与落地方案

面试官非常看重你是否具备生产环境的日志素养。大厂日志规范通常包含:

  1. 结构化日志 (JSON 格式):日志必须是 JSON 格式的。这样 Logstash 或 Filebeat 在采集日志时,可以直接将 leveltimestampuserIdtraceId 提取为独立字段,存入 Elasticsearch 中供 Kibana 检索。
  2. 全链路追踪 (TraceId):在微服务架构中,一个请求可能穿过 5 个服务。必须在 Nginx 或网关层生成一个唯一的 traceId,Node.js 接收后,所有相关的日志都要带上这个 traceId,方便排查完整链路。
  3. 日志滚动切分 (Log Rotation):不能把所有日志写在一个 app.log 里,否则文件会大到几个 GB 导致磁盘撑爆。需要按天或按大小切分(如 winston-daily-rotate-file),并定期清理 30 天前的旧日志。
// Pino 日志示例
const pino = require("pino");
const logger = pino({
level: "info",
// 生产环境输出纯 JSON,开发环境可配置 pino-pretty 进行高亮美化
});

logger.info({ userId: 123, action: "login" }, "用户登录成功");
// 输出: {"level":30,"time":1630000000000,"userId":123,"action":"login","msg":"用户登录成功"}

进程守护与周边生态

除了依靠 K8s 等云原生编排工具,传统的 Node.js 物理机部署极度依赖周边生态:

  1. PM2 (生产环境必备)

    • 崩溃自动重启:结合前面的 uncaughtException 退出,PM2 发现进程死了会立刻拉起。
    • Cluster 负载均衡:一行命令利用多核 CPU。
    • 平滑重启 (0 秒停机)pm2 reload
    • 日志接管:收集所有 stdout/stderr 输出。
  2. Nodemon / ts-node-dev (开发环境利器)

    • 监听文件变化,自动重启 Node 服务,极大提升开发效率。

优雅退出 (Graceful Shutdown)

🔥 面试题:当运维执行 pm2 reload 或者 K8s 升级版本时,如何保证正在处理的请求不被强制中断?

当进程守护工具准备杀掉 Node.js 进程时,会向它发送 SIGTERM 信号。我们需要监听这个信号,实现优雅退出:

const server = app.listen(3000);

process.on("SIGTERM", () => {
console.log("收到关闭信号,准备优雅退出...");

// 1. 停止接收新的网络请求
server.close(() => {
console.log("所有旧请求已处理完毕");

// 2. 清理资源:断开数据库连接,释放 Redis 等
db.disconnect();

// 3. 安全退出进程
process.exit(0);
});

// 4. 兜底策略:如果 10 秒内旧请求还没处理完(可能卡死了),强制退出
setTimeout(() => {
console.error("强制退出");
process.exit(1);
}, 10000);
});