Skip to content

Node.js 架构快速概览

为什么 Node.js 能处理高并发?

这是每个 Node.js 开发者都应该能回答的问题。不是背诵"单线程、非阻塞、事件驱动"这些术语,而是真正理解这些概念背后的架构设计。

本章我们将快速建立 Node.js 的架构认知,为后续深入学习打下基础。

三层架构模型

Node.js 的架构可以简化为三层:

┌─────────────────────────────────────────┐
│         JavaScript 应用代码             │
│    (你写的 app.js、server.js 等)        │
├─────────────────────────────────────────┤
│        Node.js 标准库                   │
│    (fs, http, path, crypto 等模块)      │
├─────────────────────────────────────────┤
│        Node.js 绑定层 (C++)             │
│    (JavaScript 与 C++ 的桥梁)           │
├──────────────────┬──────────────────────┤
│       V8        │        libuv         │
│   (JS 引擎)     │    (异步 I/O 库)      │
└──────────────────┴──────────────────────┘

让我们自上而下理解每一层。

第一层:你的 JavaScript 代码

这是你作为开发者直接编写的代码。你调用 fs.readFile()http.createServer() 这些 API,而不需要关心底层是如何实现的。

第二层:Node.js 标准库

这一层提供了 Node.js 的核心功能模块,如文件系统(fs)、网络(http/net)、路径处理(path)等。这些模块大部分用 JavaScript 编写,部分涉及系统调用的会调用更底层的 C++ 代码。

第三层:绑定层与底层引擎

这是 Node.js 的核心所在:

  • V8 引擎:Google 开发的 JavaScript 引擎,负责将 JavaScript 编译成机器码执行
  • libuv:跨平台的异步 I/O 库,提供事件循环、线程池等能力

V8 引擎:JavaScript 的执行者

V8 是 Chrome 浏览器的 JavaScript 引擎,Node.js 直接使用它来执行 JavaScript 代码。

V8 的核心职责:

  1. 解析 JavaScript:将源代码解析成抽象语法树(AST)
  2. 编译执行:通过 JIT(即时编译)将代码编译成机器码
  3. 内存管理:自动垃圾回收,管理 JavaScript 对象的生命周期

关键认知:V8 只负责 JavaScript 代码的执行,它本身不具备任何 I/O 能力。读文件、发网络请求这些操作,V8 都做不了——这就是 libuv 的职责。

javascript
// V8 负责执行这些纯 JavaScript 代码
const sum = (a, b) => a + b;
const result = sum(1, 2);
console.log(result);

// 但这个涉及 I/O,需要 libuv 配合
const fs = require('fs');
fs.readFile('data.txt', callback); // libuv 处理实际的文件读取

libuv:异步 I/O 的魔法

libuv 是 Node.js 实现高并发的关键。它是一个用 C 语言编写的跨平台异步 I/O 库。

libuv 的核心职责:

  1. 事件循环:Node.js 事件循环的核心实现
  2. 异步 I/O:文件、网络、DNS 等 I/O 操作的异步封装
  3. 线程池:为某些无法异步的操作提供线程池
  4. 跨平台:统一 Windows、Linux、macOS 的差异

思考一下:为什么需要线程池?

因为不是所有 I/O 操作都能在操作系统层面实现真正的异步。比如文件系统操作,在大多数操作系统上是阻塞的。libuv 通过线程池将这些阻塞操作"伪装"成异步——操作在后台线程执行,完成后通知主线程。

javascript
const fs = require('fs');

// 这个读取操作实际上是在 libuv 的线程池中执行的
fs.readFile('large-file.txt', (err, data) => {
  console.log('文件读取完成');
});

// 主线程继续执行,不会阻塞
console.log('主线程继续工作');

// 输出:
// 主线程继续工作
// 文件读取完成

单线程的真相

"Node.js 是单线程的"——这句话既对也不对。

对的部分:你的 JavaScript 代码确实运行在单个线程上。这意味着:

  • 同一时刻只有一段 JavaScript 代码在执行
  • 不需要考虑锁、死锁、竞态条件等多线程问题
  • 编程模型简单,不容易出错

不对的部分:Node.js 作为一个整体并非只有一个线程:

  • libuv 维护着一个线程池(默认 4 个线程)
  • 某些 C++ 扩展可能使用额外的线程
  • Node.js 提供了 Worker Threads 来创建工作线程
javascript
// JavaScript 代码在单线程执行
// 这两个函数不会同时运行
function taskA() {
  console.log('Task A');
}

function taskB() {
  console.log('Task B');
}

taskA();
taskB();
// 一定是先 A 后 B,绝不会交错

单线程的代价

单线程意味着如果某段代码执行时间过长,整个应用都会被阻塞:

javascript
// 危险!这会阻塞整个 Node.js 进程
function heavyComputation() {
  let result = 0;
  for (let i = 0; i < 1e9; i++) {
    result += Math.sqrt(i);
  }
  return result;
}

// 在这个计算期间,服务器无法响应任何请求!
const result = heavyComputation();

这就是为什么 Node.js 强调不要阻塞事件循环。我们将在下一章详细讨论这个话题。

非阻塞 I/O 的本质

现在我们可以理解 Node.js 高并发能力的秘密了:

  1. JavaScript 代码快速执行:V8 将 JavaScript 编译成高效的机器码
  2. I/O 操作异步化:通过 libuv 将 I/O 操作委托给操作系统或线程池
  3. 事件驱动:I/O 完成后通过回调通知主线程
  4. 单线程不阻塞:主线程不等待 I/O,可以继续处理其他请求

让我们用一个 HTTP 服务器的例子来理解:

javascript
const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  // 每个请求进来时
  fs.readFile('data.txt', 'utf8', (err, data) => {
    // 文件读取是异步的,不阻塞主线程
    res.end(data);
  });
  // 这里立即返回,主线程可以处理下一个请求
});

server.listen(3000);

当同时有 1000 个请求进来时:

  1. 主线程快速处理每个请求,将文件读取任务交给 libuv
  2. libuv 的线程池并行处理文件读取
  3. 读取完成后,回调函数被放入事件队列
  4. 主线程从队列中取出回调执行,发送响应

整个过程中,主线程始终在高效运转,不会因为等待某个文件读取而空闲。

与传统模型的对比

传统的 Web 服务器(如 Apache)通常采用多线程模型

传统模型:一个请求 = 一个线程

请求1 → 线程1 → [等待I/O...] → 响应
请求2 → 线程2 → [等待I/O...] → 响应
请求3 → 线程3 → [等待I/O...] → 响应
...

这种模型的问题:

  • 线程创建和销毁有开销
  • 每个线程占用内存(通常 1MB 左右)
  • 大量线程切换带来 CPU 开销

Node.js 的事件驱动模型

Node.js 模型:事件循环处理所有请求

请求1 → 发起I/O → 回调入队
请求2 → 发起I/O → 回调入队
请求3 → 发起I/O → 回调入队

      事件循环

      依次执行回调

这种模型的优势:

  • 单线程,内存开销小
  • 无线程切换开销
  • 适合 I/O 密集型应用

Node.js 的适用场景

适合

  • Web 服务器(API、网站)
  • 实时应用(聊天、游戏)
  • 微服务
  • 命令行工具
  • 构建工具

不太适合

  • CPU 密集型计算(可以用 Worker Threads 解决)
  • 需要复杂多线程同步的场景

本章小结

  • Node.js 架构分三层:JavaScript 代码 → Node.js 标准库 → V8 + libuv
  • V8 负责执行 JavaScript,libuv 负责异步 I/O
  • 单线程指的是 JavaScript 执行在单线程,libuv 有线程池
  • 非阻塞 I/O 是 Node.js 高并发的关键
  • Node.js 适合 I/O 密集型应用,不适合 CPU 密集型

理解了架构,下一章我们将深入探讨异步编程的思维模型。

Node.js 架构快速概览 has loaded