Skip to content
Published at:

【JS】深入理解Nodejs

Nodejs 是什么

Node.js® 是一个免费、开源、跨平台的 JavaScript 运行时环境

  • JS拓展应用领域:创建服务器、命令行工具、GUI软件和脚本
  • Nodejs把操作系统的能力暴露给了JS

Nodejs 组成部分

Nodejs 依赖于哪些库?

js
import process from "node:process";

// An object listing the version strings of Node.js and its dependencies
// NodeJS版本和它的依赖
console.log(process.versions);

输出:

bash
$ node versions.js
{
  node: '22.10.0',
  acorn: '8.12.1',
  ada: '2.9.0',
  amaro: '0.1.8',
  ares: '1.34.2',
  brotli: '1.1.0',
  cjs_module_lexer: '1.4.1',
  cldr: '45.0',
  icu: '75.1',
  llhttp: '9.2.1',              // http 协议解析
  modules: '127',
  napi: '9',                    // Native 接口
  nbytes: '0.1.1',
  ncrypto: '0.0.1',             // 加密
  nghttp2: '1.63.0',            // http2
  openssl: '3.4.0',
  simdjson: '3.10.0',           // json 解析
  simdutf: '5.5.0',             // utf 编码
  sqlite: '3.46.1',
  tz: '2024a',                  // timezone 时间
  undici: '6.20.0',
  unicode: '15.1',              // unicode 编码
  uv: '1.49.2',                 // libuv
  uvwasi: '0.0.21',
  v8: '12.4.254.21-node.21',    // V8 引擎
  zlib: '1.2.12'                // 压缩
}

核心的依赖库

  • V8:JS V8引擎
  • uv:提供OS层面接口、事件循环、线程池任务处理

命名常用到的关键词:

  • ng:Next Generation简写,下一代
  • simd: Single Instruction/Multiple Data简写,CPU技术,性能优化

Nodejs 核心

  • 阻塞与非阻塞
  • JS单线程
  • 事件循环Event Loop
  • libuv事件循环源代码
  • 工作线程池

阻塞与非阻塞

前提:调用API接口有成本;并不像人感知的很快,调用一下就返回了结果;快和慢的标准应该是由CPU来评价,而不是人

  • Buffer模块: 对应内存;内存分配、读、写
    • 内存分类:栈、堆、内存池
  • File System模块:对应文件系统;CPU操作磁盘(物理),转起来,找inode,找内容块区域
  • Net模块:对应网络协议;发出来速度还行,什么时候收到响应不确定
    • 最早被异步:内核去监听事件,然后通知应用(应用不用等)

典型 PC 上各种操作的大致时间

execute typical instruction1/1,000,000,000 sec = 1 nanosec
fetch from L1 cache memory0.5 nanosec
branch misprediction5 nanosec
fetch from L2 cache memory7 nanosec
Mutex lock/unlock25 nanosec
fetch from main memory100 nanosec
send 2K bytes over 1Gbps network20,000 nanosec
read 1MB sequentially from memory250,000 nanosec
fetch from new disk location (seek)8,000,000 nanosec
read 1MB sequentially from disk20,000,000 nanosec
send packet US to Europe and back150 milliseconds = 150,000,000 nanosec

Latency Numbers Every Programmer Should Know

阻塞与非阻塞示例:

js
import fs from 'node:fs';

// 阻塞
const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
moreWork(); // will run before console.log

// 非阻塞
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
moreWork(); // will run before console.log

JS单线程

setInterval 超时说起:

js
// 100ms interval
setInterval(() => {
  // 模拟interval回调处理耗时任务1000ms
  let start = Date.now();
  while (Date.now() - start < 1000) {}

  console.log("interval date:", new Date());
}, 100);

现象100ms的定时器变成了1000ms的定时器,为什么会这样?

事件循环Event Loop

事件循环流程图

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

阶段概述

  • timers:此阶段执行由setTimeout() 和安排的回调setInterval()
  • pending callbacks:执行推迟到下一次循环迭代的 I/O 回调。
  • idle, prepare:仅在内部使用。
  • poll:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有回调,除了关闭回调、由计时器安排的回调等setImmediate());节点将在适当的时候在此处阻塞。
  • checksetImmediate()此处调用回调。
  • close callbacks:一些关闭回调,例如socket.on('close', ...)

libuv事件循环源代码

代码目录:deps/uv/src/{unix|win}/core.c

c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int can_sleep;

  r = uv__loop_alive(loop);

  // 省略部分代码...

  while (r != 0 && loop->stop_flag == 0) {
    can_sleep =
        uv__queue_empty(&loop->pending_queue) &&
        uv__queue_empty(&loop->idle_handles);

    uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
      timeout = uv__backend_timeout(loop);

    uv__metrics_inc_loop_count(loop);

    uv__io_poll(loop, timeout);

    for (r = 0; r < 8 && !uv__queue_empty(&loop->pending_queue); r++)
      uv__run_pending(loop);

    uv__metrics_update_idle_time(loop);

    uv__run_check(loop);
    uv__run_closing_handles(loop);

    uv__update_time(loop);
    uv__run_timers(loop);

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  // 省略部分代码...

  return r;
}

工作线程池

先看一段简单测试代码:

js
let counter = 0;
while (true) {
  // console --> stdout --> write
  console.log("counter:", counter++);
}

现象:任务管理器里面多核CPU起飞。多线程来完成任务

Process Explorer

可以用Process Explorer管理器查看 进程的状态,比如线程数

UV_THREADPOOL_SIZE 设置threadpool线程数

bash
$ node --help
# 省略...
Environment variables:
UV_THREADPOOL_SIZE            sets the number of threads used in libuv's threadpool

Nodejs线程模型

线程模型:JS单线程 + N个工作线程

  • JS事件循环的单线程:
    • 事件循环、添加任务、回调(定时器、超时、事件完成的通知)
    • 只做接待类的简单任务,回调也简单处理 (比喻成公司前台)
  • N个工作线程:做具体繁重、耗时任务

不要阻塞JS事件循环

阻塞JS事件循环的单线程带来的问题:

  • 定时器失效、timeout超时不准
  • 无法接待更多客户:新的网络连接、UI交互
  • 不要在回调中写耗时任务

Node.js的核心模块阻塞事件循环表:

  • Encryption加密模块:
    • crypto.randomBytes (同步版本)
    • crypto.randomFillSync
    • crypto.pbkdf2Sync
  • Compression压缩模块:
    • zlib.inflateSync
    • zlib.deflateSync
  • File system文件系统模块:
    • Sync后缀结尾的API
  • Child process子进程模块:
    • child_process.spawnSync
    • child_process.execSync
    • child_process.execFileSync

任务切分

求平均数:切分前

js
for (let i = 0; i < n; i++) sum += i;
let avg = sum / n;
console.log('avg: ' + avg);

切分后(不卡JS事件循环的单线程,循环会继续转)

js
function asyncAvg(n, avgCB) {
  // Save ongoing sum in JS closure.
  let sum = 0;
  function help(i, cb) {
    sum += i;
    if (i == n) {
      cb(sum);
      return;
    }

    // "Asynchronous recursion".
    // Schedule next operation asynchronously.
    setImmediate(help.bind(null, i + 1, cb));
  }

  // Start the helper, with CB to call avgCB.
  help(1, function (sum) {
    let avg = sum / n;
    avgCB(avg);
  });
}

asyncAvg(n, function (avg) {
  console.log('avg of 1-n: ' + avg);
});

任务拆分下派

  • 通过开发C++ 插件来使用内置的 Node.js 工作池
  • 任务分子线程、子进程去跑,然后通知而返回结果

为什么异步

异步是编程语言应对CPU多核(并发)能力的一种方案;历史背景:近些年,单核性能提升变慢,开始通过横向加核来增加CPU性能:

  • 语言迭代有快慢(应对底层物理核增加)
  • 老语言有历史包袱(或是先以三方库的形式存在)、新语言可以从新设计
  • 异步发展:网络(内核监听)--> 文件(线程池)--> 代码块(语言支持:加语法糖)--> 成为语言一部分

不同语言方案分类:

  • 手动调度(开线程、线程池、三方库实现协程/异步):C、C++、其它
  • 异步:JS、Swift、C#、Rust
  • 协程:Go、Java

异步总结:

  • 减少开发人员的开发成本(利用CPU多核的能力)
    • 不用关心数据竞争、线程同步及其它问题
    • 不用关心具体的任务在哪个线程去执行
    • 不用自已写任务分配和调度
  • 让写异步代码和写同步代码一样简单(范式、执行流程)

Refs

Updated at: