Skip to content

Node.js ​

导航目录 ​

一、基础篇

二、异步核心篇

三、模块与 I/O 篇

四、进程与并发篇

五、工程实践篇


Node.js 核心特性 ​

非阻塞式 I/O 模型 ​

核心概念

Node.js 的非阻塞式 I/O 模型是其高性能的关键,基于事件驱动的异步架构,能够高效处理大量并发连接。

  • 基于事件驱动的异步 I/O 架构
  • 单线程处理高并发连接(避免多线程开销)
  • 适合 I/O 密集型应用:API 服务、实时聊天、数据流处理
  • 示例:同时处理 10,000 个并发连接仅需约 30MB 内存

为什么 Node.js 适合处理高并发 ​

1. 传统的多线程/多进程模型(对比模型)

想象一个银行,每个客户(请求)都需要一个柜员(线程)来专门服务。

  • 工作方式:每当一个新客户到来(一个新请求),银行就开辟一个新的服务窗口,并分配一个专门的柜员(创建一个新线程或从线程池中取一个)。这个柜员会全程服务这位客户,包括处理他的业务(CPU 计算)和等待他填写表格(I/O 等待,如查询数据库、读写文件、调用外部 API)。
  • 问题:当客户在慢吞吞地填表时(I/O 等待),这个柜员只能闲着,什么也做不了,但他却占着一个窗口(占用着系统内存和 CPU 调度资源)。如果同时来了成千上万个客户,就会导致巨大的开销:
    • 内存开销:每个线程都需要分配独立的内存空间(如堆栈)。
    • CPU 上下文切换开销:CPU 需要在成千上万个线程之间不断切换,这个过程本身就很耗时。

2. Node.js 的单线程事件驱动模型(解决方案)

现在,想象一个完全不同的银行,它只有一个"超级柜员"(单线程),但这个柜员效率极高。

  1. 事件循环 (Event Loop):这个超级柜员有一个永不停止的循环,不断检查任务队列里有没有事情要做。
  2. 接收请求:客户(请求)到来时快速接待。若只需简单查询(CPU 计算),当场处理完并返回。
  3. 处理异步 I/O:若需求耗时(如查询数据库),柜员不会傻等,而是说"您先去填个表(发起异步 I/O),填好叫我(注册回调)",然后立刻接待下一位客户。
  4. 回调函数 (Callback):当数据库返回数据,该"完成事件"被放入任务队列,超级柜员在循环中检查到后执行对应的回调函数。

一句话总结

Node.js 用"单线程 + 事件循环 + 异步 I/O",把线程等待 I/O 的时间省下来处理更多请求,从而以极小的资源开销支撑高并发。


适用场景与选型 ​

核心概念

Node.js 并非万能,它在 I/O 密集型场景表现卓越,但在 CPU 密集型场景需要额外手段(子进程 / 工作线程)来规避事件循环阻塞。

适合的场景 ​

场景说明
RESTful API / BFF高并发、I/O 为主,天然契合
实时通信WebSocket、聊天室、协同编辑(长连接)
数据流处理大文件上传下载、日志管道(Stream)
微服务网关请求转发、聚合,I/O 密集
SSR 服务端渲染与前端同构,生态统一

不擅长的场景 ​

场景原因应对方案
大量 CPU 计算单线程会阻塞事件循环worker_threads / child_process
图像/视频转码长时间占用 CPU交给专门服务或子进程
复杂科学计算非其强项调用 C++ 插件或其他语言服务

关键认知

"Node.js 单线程"指的是执行 JavaScript 的主线程是单线程,但底层 I/O 由 libuv 的线程池异步完成。因此 I/O 不阻塞主线程,而 JS 层的密集计算会阻塞。


模块系统:CommonJS vs ES Module ​

核心概念

Node.js 同时支持 CommonJS(CJS) 和 ES Module(ESM) 两套模块系统,理解二者差异是避免踩坑的基础。

基本用法对比 ​

js
// CommonJS:Node.js 传统模块规范
const fs = require("fs");
module.exports = { foo };
exports.bar = bar;

// ES Module:ECMAScript 标准(需 .mjs 或 package.json 声明 "type": "module")
import fs from "fs";
export { foo };
export default bar;

核心差异 ​

维度CommonJSES Module
加载时机运行时同步加载编译时静态分析
加载方式值的拷贝值的引用(动态绑定)
顶层 await不支持支持
Tree-shaking不友好友好(静态结构)
this 指向module.exportsundefined
循环依赖返回未完成的部分导出通过引用绑定处理更优雅

选型建议

新项目优先使用 ESM(面向未来、可 Tree-shaking);维护旧项目或需要动态 require 时用 CJS。两者可通过 import() 动态导入或 createRequire 互操作。


事件循环机制 ​

核心概念

事件循环(Event Loop)是 Node.js 处理异步操作的核心机制,它通过不断循环检查各阶段的任务队列来处理各种异步事件。底层由 libuv 实现。

js
    ┌───────────────────────────┐
 ┌─>│           timers          │ 执行 setTimeout/setInterval 回调
 │  └─────────────┬─────────────┘
 │  ┌─────────────┴─────────────┐
 │  │     pending callbacks     │ 执行系统操作回调(如 TCP 错误)
 │  └─────────────┬─────────────┘
 │  ┌─────────────┴─────────────┐
 │  │       idle, prepare       │ Node 内部使用
 │  └─────────────┬─────────────┘      ┌───────────────┐
 │  ┌─────────────┴─────────────┐      │   I/O 事件:   │
 │  │           poll            │<─────┤ 文件/网络操作 │
 │  └─────────────┬─────────────┘      └───────────────┘
 │  ┌─────────────┴─────────────┐
 │  │           check           │ 执行 setImmediate 回调
 │  └─────────────┬─────────────┘
 │  ┌─────────────┴─────────────┐
 └──┤      close callbacks      │ 关闭事件回调(如 socket.close)
    └───────────────────────────┘

六大阶段职责 ​

阶段职责
timers执行到期的 setTimeout / setInterval 回调
pending callbacks执行延迟到下一循环的 I/O 回调(如某些系统错误)
idle, prepare仅 Node 内部使用
poll检索新的 I/O 事件、执行 I/O 回调(事件循环在此等待)
check执行 setImmediate 回调
close callbacks执行关闭事件回调(如 socket.on('close'))

宏任务、微任务与执行顺序 ​

执行顺序

事件循环的执行顺序决定了不同类型任务的优先级,理解这一点对于编写高性能且行为可预测的 Node.js 应用至关重要。

整体优先级 ​

  1. 同步代码:优先执行所有同步代码
  2. process.nextTick 队列:优先级最高的异步任务
  3. 微任务队列(Promise):优先级高于宏任务
  4. 进入事件循环各阶段:timers → pending → poll → check → close,每个阶段结束后都会清空 nextTick 与微任务队列

process.nextTick 机制 ​

核心概念

process.nextTick 是 Node.js 特有的异步 API,其回调在当前操作完成后、事件循环继续之前执行,优先级高于 Promise 微任务。

js
process.nextTick(() => console.log("NextTick 1"));
Promise.resolve().then(() => console.log("Promise 1"));
console.log("同步代码");

// 输出:
// 同步代码
// NextTick 1
// Promise 1

关键点:

  • 执行时机:当前执行栈结束后立即执行
  • 优先级:高于微任务(Promise)
  • 注意事项:nextTick 队列过长会"饿死"事件循环,阻塞后续阶段

setTimeout vs setImmediate ​

核心概念

setTimeout(fn, 0) 与 setImmediate(fn) 的执行顺序取决于当前所处的事件循环阶段。

主模块中:顺序不确定

js
// 受进程启动耗时影响,输出顺序可能不同
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));

I/O 回调中:顺序确定

js
const fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => console.log("timeout"), 0);
  setImmediate(() => console.log("immediate")); // 总是先执行
});

原因:I/O 回调在 poll 阶段执行,完成后紧接着进入 check 阶段,因此 setImmediate 总是先于下一轮 timers 的 setTimeout 执行。


Node.js vs 浏览器事件循环 ​

特性Node.js浏览器
宏任务队列多个阶段(timers, poll, check 等)单个宏任务队列
微任务执行时机每个阶段之间执行每个宏任务之后执行
setImmediate支持不支持
process.nextTick支持不支持
requestAnimationFrame不支持支持

核心区别

浏览器是"一个宏任务 → 清空微任务 → 渲染"的简单循环;Node.js 是"多阶段循环,每阶段之间清空 nextTick 与微任务"的复杂模型。这也是同一段异步代码在两端输出顺序可能不同的根源。


异步编程演进:回调 → Promise → async/await ​

核心概念

Node.js 的异步编程经历了三代演进:回调函数 → Promise → async/await。每一代都在解决前一代的痛点,最终让异步代码拥有接近同步代码的可读性。

第一代:回调函数(Callback) ​

Node.js 早期约定俗成的 错误优先回调(Error-First Callback) 风格:第一个参数永远是错误对象。

js
const fs = require("fs");

fs.readFile("a.txt", "utf8", (err, data) => {
  if (err) return console.error(err);
  console.log(data);
});

痛点:回调地狱(Callback Hell)——多层嵌套导致代码横向膨胀、难以维护、错误处理分散。

js
fs.readFile("a.txt", "utf8", (err, a) => {
  if (err) return handle(err);
  fs.readFile("b.txt", "utf8", (err, b) => {
    if (err) return handle(err);
    fs.readFile("c.txt", "utf8", (err, c) => {
      if (err) return handle(err);
      console.log(a, b, c); // 层层嵌套,难以阅读
    });
  });
});

第二代:Promise ​

Promise 将回调转化为链式调用,把嵌套结构"拉平",并统一了错误处理入口 .catch()。

js
const fs = require("fs").promises;

fs.readFile("a.txt", "utf8")
  .then((a) => fs.readFile("b.txt", "utf8").then((b) => [a, b]))
  .then(([a, b]) => console.log(a, b))
  .catch((err) => console.error(err)); // 统一错误处理

Promise 的三种状态:pending(进行中)→ fulfilled(已完成)/ rejected(已拒绝),状态一旦改变不可逆。

常用组合方法:

方法说明
Promise.all全部成功才成功,任一失败即失败
Promise.race返回最先完成(成功或失败)的结果
Promise.allSettled等待全部完成,返回每个的状态(不会短路)
Promise.any任一成功即成功,全部失败才失败

第三代:async/await ​

async/await 是基于 Promise 的语法糖,让异步代码写起来像同步代码,配合 try/catch 处理错误。

js
const fs = require("fs").promises;

async function readAll() {
  try {
    const a = await fs.readFile("a.txt", "utf8");
    const b = await fs.readFile("b.txt", "utf8");
    console.log(a, b);
  } catch (err) {
    console.error(err); // 像同步代码一样捕获异常
  }
}
readAll();

注意:串行 vs 并行

连续 await 会串行执行。若任务之间没有依赖,应使用 Promise.all 并行执行以提升性能:

js
// ❌ 串行:耗时 = a + b
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");

// ✅ 并行:耗时 = max(a, b)
const [a, b] = await Promise.all([
  fs.readFile("a.txt", "utf8"),
  fs.readFile("b.txt", "utf8"),
]);

三代对比总结 ​

方案可读性错误处理组合能力
回调差分散、易遗漏弱
Promise中统一 .catch()强
async/await好try/catch 直观强

EventEmitter 事件驱动 ​

核心概念

EventEmitter 是 Node.js 事件驱动架构的基石。核心内置模块(Stream、HTTP、fs 等)几乎都继承自它,通过发布-订阅模式实现模块间的解耦通信。

基本用法 ​

js
const EventEmitter = require("events");

const emitter = new EventEmitter();

// 订阅事件
emitter.on("data", (payload) => {
  console.log("收到数据:", payload);
});

// 发布事件
emitter.emit("data", { id: 1, name: "node" });

常用 API ​

方法说明
on(event, listener)注册监听器(可重复触发)
once(event, listener)只监听一次,触发后自动移除
emit(event, ...args)触发事件并传参
off / removeListener移除指定监听器
removeAllListeners移除某事件(或全部)的监听器
listenerCount(event)获取监听器数量

自定义类继承 EventEmitter ​

实际开发中常通过继承来赋予自定义对象事件能力:

js
const EventEmitter = require("events");

class OrderService extends EventEmitter {
  createOrder(order) {
    // 业务处理...
    this.emit("created", order); // 通知订阅者
  }
}

const service = new OrderService();
service.on("created", (order) => {
  console.log("发送通知邮件:", order.id);
});
service.createOrder({ id: 1001 });

常见问题与注意事项

  • 内存泄漏警告:单个事件监听器超过 10 个会触发警告(MaxListenersExceededWarning),可用 setMaxListeners(n) 调整,但更应排查是否忘记 off。
  • 错误事件:error 事件是特殊事件,若 emit('error') 时没有对应监听器,会直接抛出异常并使进程崩溃。务必监听 error。
  • 同步执行:emit 是同步调用所有监听器,若需异步应在监听器内使用 setImmediate / process.nextTick。

Buffer 与二进制数据 ​

核心概念

Buffer 是 Node.js 用于处理二进制数据的核心类。JavaScript 原生擅长处理字符串,但在文件读写、网络传输、图片/音视频处理等场景需要直接操作字节流,Buffer 就是为此而生。

Buffer 本质是一块固定长度的内存区域,位于 V8 堆外,不受垃圾回收的常规约束。

创建 Buffer ​

js
// 分配 10 字节并清零(安全)
const buf1 = Buffer.alloc(10);

// 从字符串创建
const buf2 = Buffer.from("hello", "utf8");

// 从数组创建
const buf3 = Buffer.from([0x68, 0x69]); // "hi"

安全提示

避免使用已废弃的 new Buffer() 与 Buffer.allocUnsafe()(后者不会清零内存,可能残留敏感数据)。优先使用 Buffer.alloc / Buffer.from。

常用操作 ​

js
const buf = Buffer.from("hello");

buf.toString("utf8"); // "hello",转回字符串
buf.length; // 5,字节长度
buf.toString("hex"); // "68656c6c6f",十六进制
buf.toString("base64"); // base64 编码

// 拼接多个 Buffer
const merged = Buffer.concat([Buffer.from("a"), Buffer.from("b")]);

Stream 流与背压 ​

核心概念

Stream(流)用于分块处理数据,无需一次性把全部数据读入内存。处理大文件、网络传输时,流能显著降低内存占用,是 Node.js 高效 I/O 的关键。

四种流类型 ​

类型说明示例
Readable(可读)数据来源fs.createReadStream
Writable(可写)数据目的地fs.createWriteStream
Duplex(双工)可读又可写TCP Socket
Transform(转换)读写并转换数据zlib.createGzip

使用 pipe 处理大文件 ​

对比一次性读取,流式拷贝内存占用几乎恒定:

js
const fs = require("fs");

// ❌ 一次性读入内存,大文件会 OOM
fs.readFile("big.mp4", (err, data) => {
  fs.writeFile("copy.mp4", data, () => {});
});

// ✅ 流式处理,内存占用恒定
fs.createReadStream("big.mp4").pipe(fs.createWriteStream("copy.mp4"));

背压(Backpressure) ​

什么是背压

当可读流生产数据的速度 > 可写流消费的速度时,数据会在缓冲区堆积,可能耗尽内存。这就是背压问题。

pipe() 会自动处理背压:当写入端缓冲区满时,它会暂停读取端,待缓冲区排空后再恢复。手动处理时可监听 drain 事件:

js
const canContinue = writable.write(chunk);
if (!canContinue) {
  readable.pause(); // 暂停读取
  writable.once("drain", () => readable.resume()); // 排空后恢复
}

推荐使用 stream.pipeline,它能自动处理背压与错误、清理资源:

js
const { pipeline } = require("stream");
const fs = require("fs");
const zlib = require("zlib");

pipeline(
  fs.createReadStream("input.txt"),
  zlib.createGzip(),
  fs.createWriteStream("input.txt.gz"),
  (err) => {
    if (err) console.error("处理失败", err);
    else console.log("压缩完成");
  }
);

常用内置模块速览 ​

核心概念

Node.js 提供了丰富的内置模块,无需安装即可使用。熟悉高频模块能大幅提升开发效率。

模块用途常用 API
fs文件系统操作readFile / writeFile / createReadStream
path跨平台路径处理join / resolve / basename / extname
http创建 HTTP 服务与客户端createServer / request
urlURL 解析new URL() / URLSearchParams
os操作系统信息cpus / totalmem / platform
crypto加密、哈希、随机数createHash / randomBytes
util实用工具promisify / inspect
events事件驱动EventEmitter
stream流处理pipeline / Readable / Writable

path:跨平台路径处理 ​

为什么必须用 path

不同操作系统路径分隔符不同(Windows 用 \,Unix 用 /)。手动拼接字符串会导致跨平台 bug,务必使用 path 模块。

js
const path = require("path");

path.join("/foo", "bar", "baz"); // /foo/bar/baz
path.resolve("src", "index.js"); // 转为绝对路径
path.extname("app.config.js"); // ".js"
path.basename("/a/b/c.txt"); // "c.txt"
__dirname; // 当前文件所在目录(CommonJS)
__filename; // 当前文件绝对路径(CommonJS)

util.promisify:回调转 Promise ​

将传统的错误优先回调 API 转换为 Promise,便于 async/await:

js
const util = require("util");
const fs = require("fs");

const readFile = util.promisify(fs.readFile);
const data = await readFile("a.txt", "utf8");

进程与线程深度解析 ​

核心概念

Node.js 主线程是单线程的(执行 JS 代码),但底层通过 libuv 线程池、多进程、worker_threads 实现真正的并行。理解进程与线程的区别,是设计高并发架构的前提。

进程 vs 线程 ​

对比项进程(Process)线程(Thread)
资源独立的内存空间共享所属进程的内存
通信IPC(较重,需序列化)共享内存(较轻,需加锁)
开销创建/切换开销大创建/切换开销小
隔离性强,一个崩溃不影响其他弱,一个崩溃可能拖垮整个进程
适用CPU 隔离、多核利用(Cluster)CPU 密集计算(worker_threads)

Node.js 的并发模型 ​

  • 单线程执行 JS:所有 JS 代码在主线程运行,避免了线程安全问题。
  • libuv 线程池:文件 I/O、DNS、crypto 等由后台线程池(默认 4 个)处理,完成后回调入队。
  • 多进程(Cluster):利用多核 CPU,多个进程监听同一端口。
  • worker_threads:在同一进程内创建工作线程,适合 CPU 密集任务。

子进程管理 ​

核心概念

child_process 模块允许 Node.js 创建子进程,用于执行系统命令、运行其他脚本或分担 CPU 密集任务。

四种创建方式 ​

方法特点适用场景
exec启动 shell 执行命令,结果缓存到回调简单命令、输出量小
execFile直接执行可执行文件,不经过 shell(更安全)执行指定程序
spawn流式返回输出,不缓存输出量大、长时间运行
fork专为创建 Node 子进程设计,内置 IPC 通道Node 进程间通信

fork 与 IPC 通信 ​

fork 会在父子进程间建立 IPC 通道,可通过 send / on('message') 双向通信:

js
// parent.js
const { fork } = require("child_process");
const child = fork("./child.js");

child.send({ task: "compute", n: 40 });
child.on("message", (result) => {
  console.log("子进程返回:", result);
});
js
// child.js
process.on("message", (msg) => {
  if (msg.task === "compute") {
    const result = fibonacci(msg.n);
    process.send({ result });
  }
});

function fibonacci(n) {
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

应用场景

将 CPU 密集计算(如大数据处理、图像转换)fork 到子进程,可避免阻塞主进程的事件循环,保证服务持续响应。


Cluster 集群实战 ​

核心概念

Node.js 单进程无法利用多核 CPU。cluster 模块通过创建多个共享同一端口的工作进程,充分利用多核性能,是生产环境的标配。

工作原理 ​

                    ┌─────────────┐
                    │  Master 主进程 │  (负责调度,不处理请求)
                    └──────┬──────┘
          ┌────────────────┼────────────────┐
    ┌─────▼─────┐    ┌─────▼─────┐    ┌─────▼─────┐
    │ Worker 1  │    │ Worker 2  │    │ Worker N  │  (共享同一端口)
    └───────────┘    └───────────┘    └───────────┘

主进程负责创建并管理工作进程,请求由操作系统或主进程分发给各工作进程处理。

基本用法 ​

js
const cluster = require("cluster");
const http = require("http");
const os = require("os");

if (cluster.isPrimary) {
  const cpuCount = os.cpus().length;
  console.log(`主进程启动,创建 ${cpuCount} 个工作进程`);

  for (let i = 0; i < cpuCount; i++) {
    cluster.fork();
  }

  // 工作进程退出时自动重启,保证高可用
  cluster.on("exit", (worker) => {
    console.log(`工作进程 ${worker.process.pid} 退出,重启中...`);
    cluster.fork();
  });
} else {
  http
    .createServer((req, res) => {
      res.end(`响应来自工作进程 ${process.pid}`);
    })
    .listen(3000);
}

Cluster vs PM2

生产环境更推荐使用 PM2 进程管理器(pm2 start app.js -i max),它封装了 cluster 并提供负载均衡、日志管理、自动重启、零停机重载等能力,无需手写集群代码。


worker_threads 工作线程 ​

核心概念

worker_threads 在同一进程内创建线程,共享内存(通过 SharedArrayBuffer),比多进程更轻量。它是解决 CPU 密集型任务阻塞事件循环的首选方案。

Cluster vs worker_threads ​

对比项Cluster(多进程)worker_threads(多线程)
隔离级别进程级,内存独立线程级,可共享内存
开销较大较小
通信IPC,需序列化消息传递 + 共享内存
适用多核扩展 Web 服务CPU 密集计算

基本用法 ​

js
// main.js
const { Worker } = require("worker_threads");

const worker = new Worker("./worker.js", {
  workerData: { n: 40 },
});

worker.on("message", (result) => {
  console.log("计算结果:", result);
});
worker.on("error", (err) => console.error(err));
js
// worker.js
const { parentPort, workerData } = require("worker_threads");

function fibonacci(n) {
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

const result = fibonacci(workerData.n);
parentPort.postMessage(result); // 计算完成后回传结果

选型建议

  • I/O 密集 → 直接用 Node 异步 API 即可,无需额外线程。
  • CPU 密集(加密、压缩、图像处理、大数据计算)→ 用 worker_threads。
  • 多核扩展 Web 服务 → 用 cluster 或 PM2。

Node.js 框架对比 ​

核心概念

Node.js 生态中主流的 Web 框架各有侧重。理解它们的设计哲学,有助于根据项目规模与团队习惯做出合理选型。

主流框架对比 ​

框架特点适用场景
Express老牌、生态成熟、中间件丰富通用 Web 服务、快速开发
Koa洋葱模型、基于 async/await、轻量追求优雅异步流程控制
Egg.js基于 Koa、约定优于配置、企业级大型企业应用
NestJSTypeScript、模块化、依赖注入、类 Angular大型可维护的后端架构
Fastify高性能、低开销、内置 schema 校验高吞吐 API 服务

Koa 洋葱模型 ​

核心思想

Koa 中间件的执行呈"洋葱圈"结构:请求由外向内穿过每一层中间件到达核心,响应再由内向外返回。await next() 是进入下一层的关键。

        ┌─────────────────────────┐
        │  中间件 1 (前)           │
        │   ┌───────────────────┐ │
        │   │  中间件 2 (前)     │ │
        │   │   ┌─────────────┐ │ │
   请求 ──▶ │  核心处理    │ │ │──▶ 响应
        │   │   └─────────────┘ │ │
        │   │  中间件 2 (后)     │ │
        │   └───────────────────┘ │
        │  中间件 1 (后)           │
        └─────────────────────────┘
js
const Koa = require("koa");
const app = new Koa();

app.use(async (ctx, next) => {
  console.log("1 前");
  await next(); // 进入下一层
  console.log("1 后");
});

app.use(async (ctx, next) => {
  console.log("2 前");
  ctx.body = "hello";
  console.log("2 后");
});

app.listen(3000);
// 输出顺序:1 前 → 2 前 → 2 后 → 1 后

Koa vs Express ​

对比项ExpressKoa
异步处理回调为主async/await 原生支持
中间件模型线性执行洋葱模型(可控制前后)
体积较大(内置路由等)精简(核心极小,按需引入)
错误处理需 next(err)try/catch 更直观

优雅退出与错误处理 ​

核心概念

生产环境的 Node 服务必须处理好进程退出与异常捕获,否则可能导致请求丢失、资源泄漏、进程静默崩溃。

优雅退出(Graceful Shutdown) ​

收到退出信号时,应停止接收新请求、处理完存量请求、关闭数据库连接后再退出:

js
const server = app.listen(3000);

function gracefulShutdown() {
  console.log("收到退出信号,开始优雅关闭...");
  server.close(() => {
    console.log("HTTP 服务已关闭");
    // 关闭数据库连接、清理资源
    db.close(() => process.exit(0));
  });

  // 兜底:10 秒内未关闭则强制退出
  setTimeout(() => {
    console.error("强制退出");
    process.exit(1);
  }, 10000);
}

process.on("SIGTERM", gracefulShutdown); // kill 信号
process.on("SIGINT", gracefulShutdown); // Ctrl+C

全局异常兜底 ​

js
// 未捕获的同步异常
process.on("uncaughtException", (err) => {
  console.error("未捕获异常:", err);
  // 记录日志后退出,交由进程管理器重启
  process.exit(1);
});

// 未处理的 Promise 拒绝
process.on("unhandledRejection", (reason) => {
  console.error("未处理的 Promise 拒绝:", reason);
});

重要原则

uncaughtException 触发后进程已处于不确定状态,不应继续运行。正确做法是记录日志 → 优雅关闭 → 退出,再由 PM2/Cluster 拉起新进程,而非"捕获后继续跑"。


性能优化实践 ​

核心概念

Node.js 性能优化围绕不阻塞事件循环与充分利用系统资源两大主线展开。

优化建议清单 ​

  • 避免阻塞事件循环:不要在主线程做大量同步计算(如 JSON.parse 超大对象、同步加密),CPU 密集任务交给 worker_threads。
  • 善用流处理:大文件、大响应体使用 Stream,避免一次性载入内存。
  • 多核扩展:使用 cluster / PM2 充分利用多核 CPU。
  • 合理缓存:热点数据用 Redis / 内存缓存,减少重复计算与 I/O。
  • 连接池:数据库、HTTP 客户端使用连接池,避免频繁建连开销。
  • 减少同步 API:优先使用异步版本(fs.readFile 而非 fs.readFileSync)。
  • 开启 gzip 压缩:减小传输体积。
  • 监控与诊断:使用 --prof、clinic.js、0x 等工具定位性能瓶颈。

定位阻塞

可通过监控事件循环延迟(perf_hooks 的 monitorEventLoopDelay)来发现是否存在长任务阻塞事件循环。