异常监控与日志管理
在 Node.js 中,由于单线程的特性,一个未捕获的异常可能会导致整个进程崩溃,所有用户的请求都会受到影响。因此,异常处理机制和日志收集是高级后端面试的必考题。
异常捕获机制与兜底策略
🔥 面试题:Node.js 中如何捕获异常?如果发生未捕获的异常怎么处理?
1.1 常见的异常捕获方式
- 同步代码:使用
try...catch。 - 异步回调 (Callback):遵循 Node.js 的 Error-First 规范,即回调函数的第一个参数是
error。fs.readFile("file.txt", (err, data) => {
if (err) return console.error("出错了", err);
}); - Promise / Async/Await:使用
.catch()或在async函数中使用try...catch。
1.2 全局兜底:未捕获异常的处理
如果代码中漏写了 try...catch 或者 .catch(),异常就会冒泡到全局。Node.js 提供了两个全局事件来进行兜底捕获:
uncaughtException:捕获未处理的同步异常。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?
- 性能问题:在写入终端或文件时,
console.log有时是同步阻塞的(尤其是当目标是文件且磁盘 I/O 慢时),会严重拖慢事件循环。 - 缺乏级别管理:无法区分
INFO、WARN、ERROR、DEBUG。线上无法做到动态调整日志级别。 - 不易于检索和分析:纯文本输出很难被日志分析系统(如 ELK)格式化提取。
现代日志框架推荐
在 Node.js 中,最主流的两个日志库是 Winston 和 Pino。
- Winston:老牌经典,生态极好。支持多 Transport(比如同时输出到控制台、写入文件、发送到日志服务器)。
- Pino:主打极速 (Super fast)。通过异步日志和极其优化的序列化机制,性能远超 Winston,是现代高并发框架(如 Fastify)的标配。
大厂日志规范与落地方案
面试官非常看重你是否具备生产环境的日志素养。大厂日志规范通常包含:
- 结构化日志 (JSON 格式):日志必须是 JSON 格式的。这样 Logstash 或 Filebeat 在采集日志时,可以直接将
level、timestamp、userId、traceId提取为独立字段,存入 Elasticsearch 中供 Kibana 检索。 - 全链路追踪 (TraceId):在微服务架构中,一个请求可能穿过 5 个服务。必须在 Nginx 或网关层生成一个唯一的
traceId,Node.js 接收后,所有相关的日志都要带上这个traceId,方便排查完整链路。 - 日志滚动切分 (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 物理机部署极度依赖周边生态:
PM2 (生产环境必备)
- 崩溃自动重启:结合前面的
uncaughtException退出,PM2 发现进程死了会立刻拉起。 - Cluster 负载均衡:一行命令利用多核 CPU。
- 平滑重启 (0 秒停机):
pm2 reload。 - 日志接管:收集所有 stdout/stderr 输出。
- 崩溃自动重启:结合前面的
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);
});