Node.js 服务端基础

共 20 题
📑 题目列表 20 题
#
★★★

1. Node.js 事件循环的六个阶段(timers/pending/poll/check/close)与浏览器事件循环的差异?setImmediate 与 setTimeout(0) 的执行顺序为何不确定?

Node.js 事件循环的六个阶段(timers/pending/poll/check/close)是什么?与浏览器事件循环有何差异?setImmediate 与 setTimeout(0) 的执行顺序为何不确定?

  • Node 事件循环的阶段模型(timers → pending → poll → check → close)
  • 与浏览器事件循环(任务/微任务 + 渲染)的差异
  • setImmediate 与 setTimeout(0) 顺序不确定的机制原因(阶段时序)

Node.js 事件循环分六个阶段:timers(执行 setTimeout/setInterval 到期回调)、pending(处理系统事件如 TCP 错误)、idle/prepare(内部)、poll(取新 I/O 事件、执行 I/O 回调;无任务时阻塞等待)、check(执行 setImmediate 回调)、close(close 事件回调),每阶段间执行"微任务(process.nextTick 优先、Promise 队列)";核心是"阶段轮转 + 微任务插队";浏览器事件循环是"任务队列(宏任务)+ 微任务队列"模型(每轮取一个宏任务 → 清空微任务 → 渲染检查),没有 poll 阶段的"I/O 轮询"概念(浏览器 I/O 是任务投递),且浏览器有渲染帧与 rAF 阶段、无 process.nextTick。

setImmediate 与 setTimeout(0) 顺序不确定的原因:setTimeout(0) 的回调进 timers 阶段、setImmediate 回调进 check 阶段,两者分属相邻阶段;在"主模块(非 I/O)"中执行时,事件循环从 timers 开始:若 setTimeout(0) 的定时器已到期则先执行 timers(setTimeout 先),若到期检查时未到期(或计时器阈值偏差),则先经历 poll 到 check(setImmediate 先)——取决于"定时器到期时刻与进入 timers 阶段的先后",受启动开销、计时器精度影响,故不确定;但在 I/O 回调(poll 阶段执行)中注册两者时,poll 之后必到 check(setImmediate 先于下一轮 timers)——顺序确定(setImmediate 先)。工程启示:需要确定顺序时用 process.nextTick(本阶段后立即)或显式编排,不依赖二者相对顺序。

本题考察事件循环的机制精度:六阶段模型、阶段间微任务、与浏览器的差异、setImmediate/setTimeout 的不确定性来源。回答应说明阶段流转与"定时器到期时序"这一不确定性根源。

#
★★★

2. Node.js 单线程为什么能支撑高并发?libuv 线程池处理哪些任务(fs/dns/crypto)?

Node.js 单线程为什么能支撑高并发?libuv 线程池处理哪些任务?

  • 事件驱动 + 非阻塞 I/O 的并发模型(I/O 不占线程)
  • libuv 线程池(UV_THREADPOOL_SIZE)承担的任务(fs、dns、crypto)
  • 单线程的代价(CPU 密集阻塞)与多进程/线程的补充

Node.js 单线程能支撑高并发的原理是"事件驱动 + 非阻塞 I/O":主线程只执行 JS 代码与事件分发,I/O 操作(网络、文件、数据库)发起后不等待结果(注册回调即返回),由底层(libuv)异步完成,完成事件进入事件循环队列,主线程空闲时处理——"高并发"的实质是"大量并发 I/O 等待不占用 JS 线程"(一个线程服务海量连接,因为连接等待期不占线程),与传统"一连接一线程"模型的区别在于线程数不随连接数增长(无线程切换与内存开销)。

libuv 线程池:非所有异步都靠内核事件(epoll 等),部分"无内核异步接口"的任务由线程池(默认 4 线程,UV_THREADPOOL_SIZE 调整)执行:fs 操作(文件读写、目录操作)、DNS 解析(dns.lookup)、crypto 的 CPU 密集操作(哈希、加解密、pbkdf2 等,如 crypto.randomBytes、crypto.scrypt)、zlib(压缩)、部分 child_process 的 stdio 处理;线程池是"隐藏的并行",任务排队执行(默认容量),线程池任务阻塞会影响其他池内任务(排队延迟)。单线程的代价:CPU 密集任务(大计算、正则、JSON 大对象)阻塞主线程(事件循环停滞,其他请求无响应)——治理:拆小任务(分片/异步)、CPU 密集任务移到 worker_threads(多线程并行)或子进程、或外部服务(队列/函数计算);组合模型:主线程做调度与轻量逻辑、worker 承担计算、线程池承担 I/O 类阻塞任务。

本题考察 Node 并发模型的核心:非阻塞 I/O 让等待不占线程、线程池承担"无异步内核"任务、CPU 密集是单线程软肋。回答应说明模型、线程池任务清单与代价治理。

#
★★★

3. Koa 洋葱模型中间件的实现原理,compose 函数如何串联 async 中间件并保证 next 后置逻辑的执行顺序?

Koa 洋葱模型中间件的实现原理是什么?compose 函数如何串联 async 中间件并保证 next 后置逻辑执行顺序?

  • 洋葱模型的执行轨迹(请求进、响应出,前后置逻辑对称)
  • compose 的实现(递归/迭代串联 Promise、next 语义)
  • 后置逻辑顺序保证(Promise 链与 async 的 await 语义)

Koa 洋葱模型的本质:"中间件按注册顺序执行,每个中间件可在 next() 前后各有一段逻辑——next() 前的代码在请求处理链的前半段执行,next() 后的代码在响应阶段(下游处理完返回后)执行",形成"请求进入时正向经过各中间件、响应返回时反向经过"的洋葱切面,典型应用:日志(请求前计时、响应后记录耗时)、错误处理(try/catch 包 next 捕获下游异常)、权限与响应包装。compose 是实现核心:把中间件数组串联为"嵌套的 Promise 链"——compose 返回的 dispatch(i) 执行第 i 个中间件,并把"调用下一个中间件"的函数(即 dispatch(i+1))作为 next 传入;中间件内部 await next() 会等待"下游所有中间件及其后置逻辑"完成;实现上通过"索引守卫"(i 越界时返回 Promise.resolve())终止链,且每个中间件只执行一次(索引递增防止循环调用)。

顺序保证机制:中间件是 async 函数,next() 返回 Promise——await next() 把"当前中间件的后续代码"挂到该 Promise 的 then 上,因此"下游完成 → 上游后置逻辑继续"由 Promise 的 await 语义天然保证(后进先出的后置顺序);错误传播:任一中间件 reject,向上游的 try/catch 冒泡(未被捕获则到最外层兜底);工程要点:中间件必须 await next()(不 await 会提前返回,破坏洋葱与错误传播)、next 只能调用一次(多次调用会重复执行下游——compose 用索引防重入)、中间件顺序敏感(错误处理要放最外层、响应包装在合适位置)、以及 async 中间件的异常需 catch(防未处理 rejection 崩溃进程)。

本题考察 Koa 核心机制:洋葱模型的执行轨迹、compose 的 Promise 链实现与 await 语义保证的顺序。回答应说明轨迹、实现要点(dispatch/索引守卫)与工程纪律。

#
★★★

4. Node.js 事件循环的六个阶段与 process.nextTick/setImmediate 的执行时机?

Node.js 事件循环的六个阶段与 process.nextTick、setImmediate 的执行时机是什么?

  • 六阶段模型的阶段职责与轮转
  • process.nextTick 的"当前阶段后立即"语义(微任务队列优先)
  • setImmediate 的 check 阶段时机与 nextTick 的对比

Node.js 事件循环六阶段:timers → pending → idle/prepare → poll → check → close,循环轮转;每个阶段处理自己的队列,阶段切换时执行微任务;process.nextTick 不是事件循环阶段,而是"微任务"(nextTick 队列),其时机是"当前操作完成后、事件循环进入下一阶段前"——优先级高于 Promise 微任务(process.nextTick 队列先于 Promise 队列清空),因此"同步代码结束后立即执行"(常被称作"插队");典型用途:在"当前函数返回前"安排回调(避免同步递归爆栈、在 I/O 回调后立即执行清理),滥用会导致"饿死事件循环"(不断 nextTick 递归,poll 阶段永远轮不到)。

setImmediate 的时机:注册的回调在"check 阶段"执行——紧跟在 poll 阶段之后(I/O 回调执行完的下一阶段),与"宏任务"语义不同(它不按时间而是按阶段位置);对比:process.nextTick(当前阶段尾、微任务、优先于 Promise)vs setImmediate(check 阶段、宏任务)vs setTimeout(timers 阶段、宏任务、按时长);经典顺序(I/O 回调内):nextTick → Promise → setImmediate(同轮)→ setTimeout(可能下一轮);工程选型:需要"当前调用栈结束后立即"用 nextTick(但要防饿死)、需要"下一轮 I/O 之后"用 setImmediate(比 setTimeout(0) 语义更贴近"尽快但按阶段")、常规延时用 setTimeout;理解时机差异是"时序敏感代码"(缓存刷新、资源清理、状态同步)正确性的前提。

本题考察 Node 时序 API 的精确语义:阶段模型、nextTick 的微任务插队、setImmediate 的 check 阶段。回答应对比三者时机与优先级、给出选型与防饿死注意点。

#
★★

5. Node.js 的模块系统,CommonJS 与 ESM 的互操作?

Node.js 的模块系统中 CommonJS 与 ESM 如何互操作?

  • 双模块系统的加载模型(require/import、同步/异步)与判定(.mjs/.cjs/type)
  • CJS → ESM(default 与命名导出)与 ESM → CJS(require 限制)的互操作规则
  • 互操作陷阱(双实例、动态 require、循环依赖)与治理

Node.js 双模块系统并存:CJS(require/module.exports,同步加载、运行时动态)与 ESM(import/export,静态分析、异步加载、顶层 await),模块类型由扩展名(.mjs/.cjs)或 package.json 的 type 字段判定;互操作规则:一是"ESM 导入 CJS"——import cjs from 'cjs-module' 时 CJS 的 module.exports 作为 default 导出,且 Node 用 cjs-module-lexer 静态分析 CJS 源码提取"可检测的命名导出"(module.exports.foo = 或 exports.foo 形式可被命名导入,动态赋值(Object.assign、条件导出)检测不到,只能 default);二是"CJS 导入 ESM"——require() 加载 ESM:Node 22+ 支持 require(ESM)(异步图已就绪时同步返回),但更早版本不允许(ERR_REQUIRE_ESM),需动态 import()(异步);ESM 的具名导出对 require 不可用(只能 default);三是"双向引用"——CJS 与 ESM 互相引用时保持各自加载语义(ESM 图异步、CJS 同步),循环依赖的处理规则不同(CJS 部分导出、ESM 绑定)。

互操作陷阱与治理:一是"双实例"——同一包被 require 与 import 分别实例化(dual package hazard):统一入口格式或发布时单格式(或双入口共享核心实现);二是"动态 require"——ESM 无 require 变量:需用 createRequire(import.meta.url) 创建(node 兼容层)、或显式 import() 动态导入(异步语义变化);三是"命名导出不可靠"——依赖 cjs-module-lexer 检测,复杂 CJS 模式的命名导入会 undefined:用 default 导入后解构(包一层)或升级库为 ESM;四是"判定混乱"——type 字段与扩展名不一致导致"同代码不同解析":统一命名规范(新代码 .mjs 或 type: module);五是"工具链"——打包器(转译)与 Node 原生解析的互操作语义可能不同,需按"Node 原生行为"验证。

本题考察双模块系统的互操作规则:判定、双向加载语义、导出可见性(lexer 检测)与双实例治理。回答应说明规则细节与陷阱治理。

#
★★

6. Node.js 的优雅停机(graceful shutdown),SIGTERM/SIGINT 处理、server.close 等待进行中请求、连接超时强杀与 Kubernetes 滚动更新的配合?

Node.js 的优雅停机如何实现?SIGTERM/SIGINT 处理、server.close 等待、超时强杀与 Kubernetes 滚动更新如何配合?

  • 信号处理(SIGTERM/SIGINT)与停机流程编排
  • server.close 的"停止接收新连接、等待存量"语义与超时强杀
  • 与 Kubernetes 滚动更新(terminationGracePeriod、探针)的配合

优雅停机目标:"不中断进行中的请求、不留半截状态、及时退出"。流程编排:监听 SIGTERM(K8s 滚动更新/平台下发)与 SIGINT(Ctrl+C)→ 进入停机流程:1)停止接收新请求(server.close():不再接受新连接,存量连接完成;HTTP keep-alive 场景配合 server.closeIdleConnections() 关闭空闲连接);2)等待进行中请求完成(close 回调触发,或设置"最大等待时间");3)清理资源(数据库连接、定时器、消息队列消费者、文件句柄);4)超时强杀——等待超时(如 30s)后 process.exit(1) 强制退出(防"永远等不完":挂起请求、永不关闭的连接),可用 setTimeout + unref + 标志位;5)退出码(成功 0、超时/异常非 0)。

与 Kubernetes 滚动更新的配合:K8s 停止 Pod 时先发 SIGTERM,并在 terminationGracePeriodSeconds(默认 30s)后 SIGKILL 强杀——Node 的停机流程必须"在宽限期内完成"(总等待 ≤ grace period 减去探针/清理余量),否则被 SIGKILL(请求被硬断);配合"就绪探针(readinessProbe)":停机时先让 Pod 从 Service 摘除(状态 NotReady),新请求不再路由到该 Pod(滚动更新中"先摘再杀"的时序),之后才真正处理存量;还有"preStop hook"(可自定义摘除与等待);工程实践:停机逻辑与"连接追踪"(记录活跃请求/连接,close 后等待清单清空)、监控(停机耗时、被强杀计数)、幂等(重复 SIGTERM 的二次处理、部署时多副本滚动避免全停窗口)。

本题考察生产环境的生命周期管理:信号 → 停止接收 → 等待存量 → 超时强杀 → 与 K8s 宽限期/探针配合。回答应说明停机编排与 K8s 机制的时序配合。

#
★★

7. Node.js Stream 的四种类型与 pipe 的背压(backpressure)机制?大文件处理为何必须用流?

Node.js Stream 的四种类型与 pipe 的背压(backpressure)机制是什么?大文件处理为何必须用流?

  • 四种流(Readable/Writable/Duplex/Transform)的语义
  • pipe 的背压机制(highWaterMark、暂停/恢复、drain)
  • 大文件用流的本质(内存恒定、边读边写)

Stream 四类型:Readable(可读:产生数据,如 fs.createReadStream、process.stdin)、Writable(可写:消费数据,如 fs.createWriteStream、process.stdout)、Duplex(双工:可读可写,如 net.Socket)、Transform(变换:读写间的转换,如 zlib、crypto 加密流),核心是"数据以块(chunk)流动,内存占用与文件大小无关";pipe 是流的管道连接:readable.pipe(writable) 自动管理"读 → 写"流程并处理背压——背压机制:每个流有 highWaterMark(缓冲阈值,如 64KB):可写流缓冲区超过阈值时 push()/write() 返回 false("慢点"信号),可读流据此暂停数据读取(pause),写端 drain 事件触发(缓冲排空)时恢复(resume)——"生产速度 > 消费速度"时通过暂停/恢复调节,内存不会无限增长。

大文件为何必须用流:一次性读取(fs.readFile)会把整个文件载入内存(GB 级文件 → OOM 或 GC 压力),且"读完再写"需要双份内存;流式处理"边读边写"(管道直通,内存只存当前 chunk 与缓冲窗口),任意大文件内存占用恒定(≈ highWaterMark 级别);应用场景:文件复制/上传/下载(流式转发,避免整文件缓冲)、日志处理(逐行/分块)、压缩(Transform 链:read → gzip → write);工程注意:pipe 的默认行为(错误不自动传播——需 error 处理或 stream/promises 的 pipeline(自动清理与传播错误,推荐))、背压失效场景(未 pipe 的手工 write 忽略返回值、Transform 内部缓冲)、背压信号在跨进程(子进程 stdio)与网络(socket)同样重要(TCP 流量控制类似);pipeline(node:stream)是现代推荐(统一错误与关闭处理)。

本题考察流的模型与背压机制:四类型语义、pipe 的暂停/恢复调节、大文件内存恒定的本质。回答应说明机制与 pipeline 推荐、背压失效风险。

#
★★

8. cluster 模块与 pm2 的多进程模型,端口共享(SO_REUSEPORT/句柄传递)与优雅重启如何实现?

cluster 模块与 pm2 的多进程模型如何工作?端口共享(SO_REUSEPORT/句柄传递)与优雅重启如何实现?

  • cluster 的主从模型(master 分发、worker 进程)与端口共享机制
  • 句柄传递(handle passing)与 SO_REUSEPORT 的差异
  • pm2 的进程管理(cluster 模式、优雅重启、停机时序)

cluster 模块实现"单端口多进程":master 进程(不处理业务)fork 出多个 worker(子进程,各自独立事件循环与内存),worker 共享服务器端口——机制(经典实现):master 创建 net.Server 并把"句柄(handle)"传递给 worker。句柄传递:master 收到连接后按策略(round-robin,默认;或 SO_REUSEPORT——多个 worker 各自 listen 同一端口,内核分发连接)把底层句柄交给某个 worker,worker 直接处理该连接(无二次转发);round-robin(cluster 的 SCHED_RR)在 master 层轮流分发(负载均衡可控、跨平台一致),SO_REUSEPORT 由内核做分发(性能高但负载不均与平台差异);注意:句柄传递使"多个进程监听同一端口"成为可能,但 worker 间的共享状态(内存不共享)需外部存储(Redis 等)。

pm2 的多进程管理:pm2 start app.js -i max 启动 cluster 模式(PM2 作为 master 派生子进程,内部也走 Node cluster 或 fork 模式);能力:自动重启(崩溃/内存超限(max_memory_restart))、负载均衡(cluster 模式)、优雅重启(--kill-timeout、reload(滚动重启:逐个 worker 重启,先摘流量(listen 停止)等存量完成再启新)——配合应用的 SIGTERM 优雅停机实现"零停机"重启;pm2 还管理日志、环境与启动脚本(开机自启);与 K8s 的关系:容器内仍可用 pm2/cluster 做"进程级多核利用"(Node 单进程用不满多核 CPU),但"多副本"由 K8s Pod 管理(双层扩展:Pod 级 + 进程级);注意:pm2 在容器内的"进程守护"与 K8s 的探针/重启策略职责重叠,需避免双重重启竞争(或选择其一);cluster 的 sticky 会话问题(轮询分发导致 session 丢失——需外部 session 存储或 sticky 配置))。

本题考察 Node 多进程的端口共享与进程管理:cluster 主从分发(句柄传递 vs SO_REUSEPORT)、pm2 的滚动重启。回答应说明机制差异与优雅重启时序、容器内外扩展边界。

#
★★

9. Node.js 内存泄漏的常见来源(闭包缓存/事件监听器/全局变量)与 heapsnapshot 排查方法?

Node.js 内存泄漏的常见来源是什么?heapsnapshot 如何排查?

  • 泄漏来源(闭包缓存、事件监听器、全局变量、定时器、流未关闭)
  • heapsnapshot 的抓取与分析(快照对比、retaining path)
  • 排查流程(内存曲线、heapdump、对比定位)与治理

常见泄漏来源:一是"闭包缓存"——闭包捕获了大对象且闭包被长期持有(缓存 Map/数组只增不删、回调闭包引用上下文);二是"事件监听器"——重复添加监听不移除(EventEmitter 的监听器数组增长,警告 MaxListenersExceededWarning)、第三方库的监听未清理(如全局进程监听);三是"全局变量"——把对象挂全局/模块级缓存(数据积累不清理、LRU 缺失);四是"定时器/句柄"——setInterval/setTimeout 未清除且回调引用大对象、句柄(socket/文件)未关闭;五是"流与缓冲"——流未销毁(背压失效时缓冲堆积)、缓存对象滞留;六是"V8 内部"——闭包引用导致的老生代对象无法回收。

排查方法(heapsnapshot):一是"证据先行"——监控内存曲线(--trace-gc、heap 采样、prometheus 指标)确认"持续增长不回落"(泄漏 vs 波动);二是"抓快照"——heapdump/--heapsnapshot 或在 Node 中触发(v8.writeHeapSnapshot()),多次抓取(启动后、峰值、回落期);三是"对比分析"——Chrome DevTools/Heap Profiler 加载两份快照对比(Allocation 对比、"对象计数差异"),找"持续增长的对象类"(如数组、Buffer、自定义类);四是"retaining path"——对嫌疑对象看"从 GC root 到它的引用链"(Retainers 树),定位"谁持有它"(闭包、监听器、缓存),典型:Closure(context)→ 变量、Listener 数组 → 函数;五是"验证修复"——修复后复测内存曲线(峰值/稳定水平下降);工程实践:设置内存阈值告警(--max-old-space-size + 监控)、泄漏回归测试(压测后检查堆大小)、常见坑(日志对象滞留、异步回调引用 this 链、未清理的 worker)。

本题考察内存泄漏的系统排查:来源分类(闭包/监听器/全局/句柄)、快照对比与 retaining path 定位。回答应说明来源、抓取分析流程与修复验证。

#
★★

10. Node.js 的 Cluster 与 Worker Threads 在 CPU 密集场景的取舍,多进程内存模型?

Node.js 的 Cluster 与 Worker Threads 在 CPU 密集场景如何取舍?多进程内存模型是什么?

  • Cluster(多进程)与 Worker Threads(多线程)的模型差异
  • CPU 密集任务的处理(worker 并行 vs 子进程隔离)
  • 内存模型(进程隔离 vs 线程共享)与通信(IPC vs 消息/SharedArrayBuffer)

Cluster 与 Worker Threads 都是"利用多核"的手段但模型不同:Cluster 是"多进程"——每个 worker 是独立进程(独立 V8 实例、独立内存、崩溃隔离),通信走 IPC(进程间消息,序列化开销),适用"请求级并行"(多进程各服务请求,负载均衡)与"隔离性要求高"(一个 worker 崩溃不影响其他)的场景;Worker Threads 是"多线程"——同一进程内多个线程(各线程有自己的 JS 上下文与事件循环,但共享进程内存空间),通信走消息(结构化克隆)或 SharedArrayBuffer(共享内存、零拷贝),适用"计算任务并行"(把 CPU 密集任务分片到多线程并行执行,主线程不被阻塞)。

CPU 密集场景取舍:Cluster 的 CPU 密集处理——每个 worker 仍是单线程,密集计算仍会阻塞该 worker(其他请求被该 worker 阻塞),适合"并发请求分片"而非"单任务加速";Worker Threads 的 CPU 密集——主线程把任务分发给多个 worker 并行计算(如图像处理、加密、大数据计算、复杂正则/JSON 处理),总吞吐显著提升(利用多核),主线程保持响应;内存模型:进程隔离(Cluster)——各进程独立堆(内存 = 进程数 × 单进程占用,缓存不共享),崩溃与内存泄漏互不影响,但共享数据需外部存储;线程共享(Worker)——同一进程内线程共享堆外(ArrayBuffer 等)与进程资源,消息传递无进程开销(克隆成本仍在),共享状态需谨慎(SharedArrayBuffer + Atomics 同步);选型:CPU 密集计算并行 → worker_threads(或子进程(child_process 的独立进程模型:更强隔离、可运行外部程序,但通信更重));高并发请求服务 → cluster/多实例(进程级扩展);两者可组合(cluster worker 内再开 worker_threads 处理计算);注意 worker 的创建开销(线程池复用、线程数量 = CPU 核数匹配)、错误隔离(线程崩溃会导致进程崩溃——与 cluster 的隔离性差异)。

本题考察多核利用的模型选型:进程 vs 线程的隔离/内存/通信差异。回答应说明模型对比、CPU 密集场景取舍与内存模型、组合用法。

#
★★

11. Node.js 的流(Stream)背压机制与内存安全,大文件处理如何避免 OOM?

Node.js 流的背压机制如何保证内存安全?大文件处理如何避免 OOM?

  • 背压的信号链(write 返回 false、drain、暂停/恢复)与缓冲阈值
  • 流式大文件处理的内存模型(恒定内存)
  • 背压失效的常见坑(忽略返回值、未 pipe 的手工读写、Transform 缓冲)

背压机制的核心是"读写节奏协商":可写流内部有缓冲(highWaterMark,默认 16KB-64KB 级别),write() 返回 false 表示"缓冲区满,请暂停",可读流收到"暂停信号"(pause/pipe 内部处理)停止拉取数据,写端缓冲排空后触发 drain 事件(恢复写入)——数据流始终"生产 ≤ 消费 + 缓冲",内存占用被限制在缓冲级别,不会因"读得快写得慢"而无限堆积;pipe/pipeline 自动接线(可读流的 data 事件流与可写流的 write/drain 循环),开发者手工实现时需遵守同一协议(write 返回 false 就停、drain 后继续)。

大文件避免 OOM:readFile 一次性读入内存(文件多大内存多大,GB 级直接 OOM 或拖垮 GC),流式处理把文件切成 chunk 逐块读写:内存峰值 ≈ 缓冲窗口(highWaterMark × 管道级数),与文件大小无关;场景:文件上传/下载(req.pipe(stream) 边收边写盘)、日志处理(逐行流式解析)、压缩/加密(Transform 链读-变换-写)、数据库大结果集(流式查询);工程要点:一是"优先 pipeline"——stream/promises 的 pipeline 自动处理背压、错误传播与资源清理(比手动 pipe 可靠,pipe 不传播错误且不自动销毁);二是"背压失效坑"——手工循环里忽略 write() 返回值(照写不误导致内存堆积)、未监听 drain、Transform 内部缓存(transform 不返回写信号时背压断链)、http 响应(res.write 的 backpressure 需 await 处理或 pipeline);三是"缓冲调优"——highWaterMark 调大提吞吐但增内存(按实际资源设置)、监控堆与 RSS(内存告警);四是"跨进程/网络"——子进程 stdio 与 socket 的背压同样重要(写端过快会 OOM)。

本题考察背压与内存安全的关联:信号链机制、流式恒定内存、失效坑与 pipeline 推荐。回答应说明机制与工程实践。

#
★★

12. Node.js 的错误处理模式,异步回调错误、Promise 拒绝与全局 uncaughtException/unhandledRejection 的兜底策略,如何避免进程崩溃与静默失败?

Node.js 的错误处理模式是什么?异步回调错误、Promise 拒绝与全局兜底(uncaughtException/unhandledRejection)如何设计?如何避免进程崩溃与静默失败?

  • 错误传播路径(回调 err-first、Promise 链、async/await)与处理原则
  • 全局兜底的定位(记录、上报、重启)与"不恢复不可信状态"
  • 防崩溃与防静默失败(未处理拒绝、错误吞掉)的治理

Node.js 的错误传播三路径:回调风格(err-first:callback(err, data),错误显式传递,遗漏检查则静默);Promise 链(.catch/链式传播);async/await(try/catch 捕获,异常沿栈抛出);处理原则:错误"在最近有上下文处处理或显式上抛"(业务可恢复的局部处理(重试、降级),不可恢复的上抛到边界);异步边界(事件回调、定时器、流事件)的错误无法被调用方捕获,需在回调内自行 try/catch 或监听 error 事件(EventEmitter 的 error 事件未监听会抛异常)。

全局兜底:process.on('uncaughtException')(未捕获同步异常)与 process.on('unhandledRejection')(未处理 Promise 拒绝)——定位是"最后防线":记录完整堆栈、上报监控、执行"受控重启"(K8s/pm2 拉起或进程自行退出重启);重要原则:uncaughtException 后进程状态不可信(可能处于半破坏状态),"继续运行"风险高(官方建议:记录后退出(exit(1) 由管理器重启),或仅在"确信可恢复"(如连接池重建)时继续);unhandledRejection 同理(Node 15+ 默认"未处理拒绝即崩溃"——强制开发者处理,避免静默失败);避免静默失败:禁用"空 catch"(lint 规则 no-empty、no-unused-vars 的 catch 绑定)、异步错误必须有出口(错误中间件/兜底 handler)、监控未处理拒绝计数(崩溃前的预警信号);工程实践:统一错误对象(错误码、上下文、可操作性)、错误日志结构化(请求 ID 关联)、以及"故障演练"(注入错误验证兜底路径真的工作)。

本题考察错误处理的完整链路:传播路径、边界处理、全局兜底定位与防静默。回答应说明三类路径、兜底的"记录+受控重启"原则与治理手段。

#
★★

13. Node.js 内存与性能调优,--max-old-space-size、堆快照分析、GC 停顿与吞吐的权衡,高并发下如何定位瓶颈(CPU/IO/GC)?

Node.js 内存与性能调优怎么做?--max-old-space-size、堆快照、GC 停顿与吞吐如何权衡?高并发下如何定位瓶颈?

  • 堆配置(--max-old-space-size)与 V8 内存分区(新生代/老生代)
  • GC 行为(停顿、频率)与堆大小的权衡
  • 瓶颈定位(CPU/IO/GC 的区分方法与工具)

V8 内存分区:新生代(Scavenger:小对象快速回收)、老生代(Mark-Sweep/Compact:大对象与晋升对象,GC 停顿在此发生);--max-old-space-size 设置老生代上限(默认约 2GB,视版本/平台):堆越大,GC 频率越低(吞吐高)但单次 GC 停顿越长(延迟高)、内存占用高;堆越小,GC 频繁(CPU 开销)但停顿短、内存低——权衡:以"服务延迟分位数与内存预算"为准(如上限设为"峰值使用的 1.5-2 倍 + 余量",避免频繁 GC 与 OOM);监控:heapUsed 曲线、GC 统计(--trace-gc、--trace-gc-verbose)、停顿时间(performance 的 gc 指标、监控 GC 事件)。

瓶颈定位方法论:一是"区分三类瓶颈"——CPU 密集(事件循环延迟高、CPU 使用率高、GC 占比大)、I/O 密集(等待 I/O、CPU 空闲但响应慢(事件循环空转))、GC 问题(GC 时间占比高、停顿长);二是"工具"——CPU:--cpu-prof(火焰图)、clinic、0x;事件循环延迟:monitor(process.hrtime 采样)、事件循环延迟指标(作为健康度);GC:--trace-gc、heapsnapshot;I/O:网络/磁盘监控(连接数、句柄)、perf 事件;三是"高并发定位流程"——先看"响应慢在哪":事件循环延迟(CPU/阻塞)→ CPU profile 找热点函数(正则、JSON、计算)→ 堆快照(内存泄漏/分配热点)→ I/O 层(连接池、慢查询、外呼延迟)→ 逐一排除;四是"优化方向"——CPU:缓存热点结果、worker_threads 并行、算法优化;GC:对象复用(避免大对象频繁分配)、减少闭包滞留、调整堆;I/O:连接池、并发限制、缓存;五是"基准与回归"——压测基线(吞吐、延迟分位)与每次优化的对比,避免"调了没测"。

本题考察 Node 调优的系统方法:堆与 GC 权衡、三类瓶颈的区分与工具链、优化方向。回答应说明机制(堆分区/GC 权衡)与定位流程(延迟→CPU→堆→IO)。

#
★★

14. Worker Threads 与子进程的选型,worker_threads 共享内存 vs child_process 独立进程的差异,CPU 密集与 I/O 密集任务各用哪种?

Worker Threads 与子进程如何选型?worker_threads 共享内存与 child_process 独立进程的差异是什么?CPU 密集与 I/O 密集任务各用哪种?

  • worker_threads(同进程多线程)与 child_process(独立进程)的模型差异
  • 共享内存(SharedArrayBuffer)vs 进程隔离的取舍
  • 按任务类型(CPU 密集/IO 密集)的选型

Worker Threads:同一进程内的线程(worker 有独立 JS 上下文与事件循环,与主线程共享进程资源:堆外内存、文件句柄、进程 ID);通信:postMessage(结构化克隆,跨线程无进程开销)与 SharedArrayBuffer(共享内存,零拷贝,需 Atomics 同步);优势:启动快(无新进程)、内存共享(大数据免复制)、通信轻量;代价:线程崩溃拖垮整个进程(隔离性弱)、共享状态需并发控制、CPU 并行受进程内资源限制。child_process:独立进程(独立 V8、独立内存空间、进程级隔离);通信:IPC(进程间消息,序列化/反序列化开销大)、stdio 管道;优势:强隔离(崩溃/内存泄漏不影响主进程)、可运行外部程序(非 Node)、资源配额独立(独立堆配置);代价:启动慢(进程创建)、通信重(大数据复制)、内存不共享(共享数据需外部存储或序列化传递)。

选型:CPU 密集任务——worker_threads 优先(同进程多线程并行计算,共享结果免复制、启动快;如加密、图像处理、复杂计算分片),数据量大时 SharedArrayBuffer 零拷贝;需要"独立运行外部程序"或"强隔离"(不可信代码、易崩溃的第三方库)用 child_process(exec/spawn/fork);I/O 密集任务——通常不需要多线程/多进程(Node 本身异步 I/O 高效),瓶颈在"单个 I/O 操作"(如某同步库)时用子进程隔离,或"大量并发 I/O + 事件循环被拖累"时用 cluster/多实例(进程级水平扩展)而非 worker;注意:worker 与子进程都"不共享 JS 对象"(消息是拷贝),高频小消息用 worker(开销低)、低频大消息评估序列化成本;实践:任务分发模式(主线程调度、worker/子进程执行、结果回传)、进程/线程池复用(避免反复创建)、worker 数量 = CPU 核数(超线程考量)、错误与退出处理(进程退出码、worker error 事件)。

本题考察并行模型的选型框架:线程(共享、轻、弱隔离)vs 进程(隔离、重、强)。回答应对比模型与通信差异,按 CPU/IO 密集任务给出选型。

#

15. CommonJS 与 ESM 在 Node.js 中的互操作边界(require ESM 的限制与 dual package)?

CommonJS 与 ESM 在 Node.js 中的互操作边界是什么?require ESM 的限制与 dual package 问题如何处理?

  • require(ESM) 的版本与语义限制(同步加载约束、命名导出)
  • dual package 双实例的成因与危害
  • 互操作治理(入口统一、isomorphic 包装、加载器策略)

CJS 与 ESM 互操作边界:一是"require ESM 的限制"——ESM 是异步加载(依赖图可能含顶层 await),require 是同步语义,因此早期 Node 禁止 require(ESM)(ERR_REQUIRE_ESM),Node 22+ 支持 require(ESM)(在"图已完整加载"的前提下同步返回,含顶层 await 的模块仍报错);即使可 require,ESM 的具名导出对 require 不可用(只能拿 default),需 require('pkg').default 或解构;二是"import CJS 的边界"——import CJS 包时 default 恒为 module.exports,命名导出依赖 cjs-module-lexer 的静态检测(复杂 CJS 模式检测不到,命名导入为 undefined);三是"循环依赖"——两种格式循环引用时的绑定/导出可见性规则不同(CJS 运行时部分导出、ESM 静态绑定),互操作图更易出问题。

dual package(双实例)边界:同一包同时提供 CJS 与 ESM 两个入口(exports 的 require/import 条件),被"不同格式的引用方"分别加载时产生两份实例(状态分裂、instanceof 失败、单例失效),且 ESM 包被 require 时"ESM 入口走 CJS 兼容路径"(Node 的 require(ESM) 或打包器转换)可能再生成一份——多格式混用的双实例风险;治理:一是"单入口策略"——只发布一种格式(新包只发 ESM,或 CJS 包用 ESM 包装 re-export);二是"isomorphic 共享核心"——双入口共享同一核心实现(ESM 入口 re-export CJS 实现),实例状态尽量下沉到"无状态核心"或外部存储;三是"加载策略统一"——应用侧统一模块格式(全 ESM 或全 CJS),避免同一依赖被两种格式加载;四是"验证"——发布前用"双格式加载测试"(分别 require 与 import 验证单实例、导出形状一致);五是"工具链"——打包器/转译器的 interop 语义与 Node 原生不同,按"Node 原生行为"为基准验证。

本题考察互操作边界的细节:require(ESM) 的同步限制、命名导出可见性、dual package 双实例。回答应说明限制与治理策略。

#

16. Node 22+ 的类型剥离(type stripping)与单可执行文件对服务端开发的影响?

Node 22+ 的类型剥离(type stripping)与单可执行文件(SEA)对服务端开发有什么影响?

  • 类型剥离(--experimental-strip-types/默认支持)的机制与限制
  • 单可执行文件(SEA)的打包与分发场景
  • 对开发流程(免构建运行 TS、部署分发)的影响

Node 22+ 的类型剥离(type stripping):Node 原生支持直接运行 TypeScript 文件(node app.ts),运行时剥离类型注解(不进行类型检查),其演进(22 的实验到后续默认):支持的 TS 特性受限——"仅可剥离的语法"(类型注解、接口、type 声明等纯类型语法可剥离),而"需要转换的特性"(枚举(enum)、命名空间(namespace)、参数属性(constructor 参数属性)、experimental 装饰器等)在剥离模式下报错或需 --experimental-transform-types 转换;且不做类型检查(类型正确性仍需 tsc 检查),适合"开发/脚本快速运行"(免 ts-node/tsx、免构建),生产仍需类型检查与打包治理(或直接用 tsc 产物)。

单可执行文件(SEA,Single Executable Applications):把 Node 应用(JS + 资源 + 模块)与 Node 运行时打包为单一可执行文件(node --experimental-sea-config 配置 + postject 注入),分发无需安装 Node(用户直接运行二进制),场景:CLI 工具分发(免 Node 环境)、离线部署(边缘/受限环境)、桌面化工具;注意:SEA 的模块支持限制(早期仅单文件、无 require 外部模块,后支持嵌入资源与模块(需要打包器把依赖 bundle 进单文件))、体积(含完整运行时)与平台(每平台单独构建);对服务端开发的影响:一是"免构建开发"——类型剥离让"源码直接跑"成为默认(dev 提速、调试栈与源码对应),但"类型检查与生产构建"职责仍在(剥离 ≠ 类型安全);二是"部署简化"——SEA 简化"依赖 Node 版本"的部署约束(二进制自带运行时),但服务器场景通常已有 Node 环境(收益主要在分发/隔离);三是"工具链协作"——与打包器(bundle 依赖进单文件)、CI(各平台产物矩阵)配合;四是"边界认知"——剥离模式是"执行"而非"编译",需配合 tsc --noEmit 做类型门禁。

本题考察 Node 新能力的工程影响:类型剥离(免构建运行 + 特性限制)与 SEA(单文件分发)。回答应说明机制、限制与对开发/部署流程的影响。

#

17. Node.js 的错误处理,async/await 的异常传播?

Node.js 中 async/await 的异常如何传播?处理模式与陷阱是什么?

  • async 函数的异常传播(reject → throw、await 的捕获语义)
  • try/catch 的边界(异步边界、并行任务、事件回调)
  • 陷阱(忘记 await、未捕获拒绝、错误吞掉)与治理

async/await 的异常传播模型:async 函数内 throw 等价于返回 rejected Promise;await 一个 rejected Promise 会"抛异常"(被外层 try/catch 捕获);异常沿"调用栈中的 await 链"向上传播——被最近的 try/catch 接住,或最终成为"未处理的 rejected Promise"(async 函数未捕获的拒绝传播到调用方,直到 unhandledRejection);因此 async/await 让"同步式错误处理"成为可能(try/catch 覆盖异步代码),是 Node 现代错误处理的主流。

关键陷阱与治理:一是"忘记 await"——不 await 的异步调用错误成为未处理拒绝(静默或崩溃):lint 规则(no-floating-promises、require-await 相关)拦截;二是"并行任务"——Promise.all 任一 reject 整体 reject(需处理其余任务的错误/结果丢失),Promise.allSettled 收集全部结果(部分失败场景)、Promise.race 的"谁先谁抛"语义;三是"异步边界"——事件回调(EventEmitter、setTimeout)内的 async 错误不会传播到外部 try/catch(async 函数"吞掉"边界:回调内的异常需在回调内 try/catch 或显式处理);四是"错误吞掉"——空 catch、catch 后不处理(记录或重新抛出);五是"边界错误出口"——路由/中间件层统一 catch(express 的 async 错误需包装器(express 5 原生支持 async 错误)或手动 next(err))、顶层兜底(unhandledRejection 记录 + 受控处理);实践:统一错误类型(错误码、上下文)、错误日志结构化、测试覆盖错误路径(断言 reject 而非只看成功)。

本题考察 async/await 错误语义的精确掌握:传播模型、捕获边界与常见陷阱。回答应说明传播机制与"忘记 await、异步边界、吞错"的治理。

#

18. Node.js 的流与背压,可读/可写流的应用?

Node.js 的可读/可写流有哪些应用?背压在实际场景中如何处理?

  • 可读流(Readable)与可写流(Writable)的典型应用
  • 实际场景的背压处理(文件、网络、转换链)
  • 事件/API 的正确用法(data/end/error、write/drain、pipeline)

可读流应用:文件读取(fs.createReadStream 分块读大文件)、网络接收(http 请求体 req、响应 res 的流语义)、子进程输出(stdout/stderr)、数据库结果集流式消费(大查询不整体驻留内存)、日志逐行处理(readline 流式);可写流应用:文件写入(createWriteStream 追加/写入大文件)、网络发送(res 流式响应、上传流式转发)、日志输出(stdout/stderr 的写入)、压缩/加密输出(Transform 链的写端);核心价值都是"数据以块流动,内存与数据量无关"。

实际背压处理:一是"pipe/pipeline"——readable.pipe(writable)(自动背压:write 满则暂停读、drain 恢复)与 pipeline(推荐:自动处理错误与清理,含 Transform 链(read → transform → write)与跨流错误传播);二是"手工模式"——用 data 事件消费时需自己管理(readable.on('data') 后,write() 返回 false 时 pause(),drain 时 resume()——否则内存堆积);三是"写端背压"——大响应(res.write 返回 false 时等待 drain 再继续写)、上传转发(req.pipe(dest) 直接管道,避免缓冲整个请求体);四是"Transform 的背压"——自定义 Transform 的 _transform 必须调用 callback 或 push 控制(不回调则链阻塞),内部缓冲(可写侧 highWaterMark 与可读侧队列)需理解;五是"错误与关闭"——流错误(读取失败、写入失败)通过 error 事件/ pipeline 传播,需监听处理(未处理 error 会抛异常);六是"性能与调优"——highWaterMark 调整(大块处理提吞吐)、对象模式(objectMode 传对象)、背压监测(write 返回 false 的频率反映消费瓶颈)。

本题考察流的应用面与背压实操:两类流的场景清单、pipe/pipeline 与手工模式的背压处理、Transform 与错误治理。回答应说明应用场景与正确用法。

#

19. node:test 内置测试框架(node --test、mock 与断言)在 Node 服务端单元测试的工程价值,与 Jest/Vitest 的取舍?

node:test 内置测试框架(node --test、mock 与断言)在 Node 服务端单元测试中有什么工程价值?与 Jest/Vitest 如何取舍?

  • node:test 的能力(test runner、断言、mock、覆盖)与零依赖特性
  • 服务端测试场景(免构建、原生 TS 支持、进程内运行)
  • 与 Jest/Vitest 的取舍(生态、快照、模拟、watch)

node:test 是 Node 内置测试框架(node --test 运行):提供 test/it 声明、describe 分组、断言(node:assert 与 assert/strict)、mock(node:test 的 mock 模块:mock.fn/mock.method、定时器 mock)、覆盖统计(--experimental-test-coverage → 稳定后 --test-coverage)、测试过滤(--test-name-pattern)、并发(--test-concurrency)与分片(--test-shard);工程价值:一是"零依赖"——Node 自带(版本 18+ 逐步完善),服务端项目无需引入测试框架依赖(体积与供应链更小)、无需配置(约定目录/命名即可跑);二是"免构建"——配合类型剥离直接测 TS 源码(Node 22+)、进程内运行(快)、与 Node 运行时行为一致(测真实环境);三是"CI 集成"——node --test 进脚本即跑(无框架版本漂移)、node:test 与 Node 版本同步演进。

与 Jest/Vitest 的取舍:Jest——生态最全(快照、自动 mock 系统(jest.mock 自动替换模块)、testEnvironment(jsdom)、watch/交互、大社区),但配置复杂、转换层重(babel/jest 转换)、类型与 ESM 支持曾有痛点;Vitest——Vite 生态(配置共享、HMR、快照、mock 丰富、TS 原生)、速度快、开发者体验好,适合"前端/同构代码"(组件、hooks、Vite 项目)与服务端均可;node:test——零依赖最简、无转换、贴近运行时,但生态(快照、自动 mock 模块系统、watch、报告插件)相对基础(mock 能力简单、无模块级自动 mock、jsdom 需自行组合);取舍:服务端纯逻辑/API 测试、追求"零依赖 + 原生"选 node:test;需要复杂 mock(模块替换)、快照、前端代码与丰富断言选 Vitest/Jest;也可组合(核心服务逻辑 node:test、前端组件 Vitest);注意 node:test 的能力边界(自动模块 mock 缺失可用手动依赖注入、覆盖率细粒度)需在选型时确认。

本题考察测试框架的现代选型:node:test 的零依赖原生价值与边界。回答应说明能力、工程价值、与 Jest/Vitest 的对比及按场景选型。

#

20. Node.js 服务端安全基础,路径遍历、原型污染、命令注入与 SSRF 在 Node 应用中的典型入口与防护?

Node.js 服务端的路径遍历、原型污染、命令注入与 SSRF 的典型入口与防护是什么?

  • 四类漏洞的机制与 Node 典型入口
  • 防护手段(输入校验、安全 API、最小权限)
  • 安全治理(依赖安全、默认安全、测试与审计)

四类漏洞的 Node 典型入口与防护:一是"路径遍历"——入口:文件接口用用户输入拼路径(fs.readFile(userPath)、静态文件服务的 path.join 未规范化、上传解压路径),攻击者用 ../ 逃逸目录读任意文件;防护:白名单/规范化后校验(path.resolve 后检查是否在允许根内、使用安全的路径 API)、禁止用户控制路径(用 ID 映射)、文件服务用专门中间件(限制根目录);二是"原型污染"——入口:把不可信对象 merge 进对象(深合并(Object.assign/自定义 merge 递归赋值 proto/constructor.prototype)、JSON.parse 后未校验 key、查询参数解析),污染 Object.prototype 后影响所有对象(绕过校验、RCE(配合属性链));防护:不合并不可信数据(用 Object.create(null) 或结构化克隆)、合并前过滤危险键(proto、constructor、prototype)、使用安全的 schema 校验(拒绝未知键);三是"命令注入"——入口:child_process.exec/spawn 用用户输入拼 shell 命令(exec(ls ${userInput})、shell: true 场景)、不安全的模板拼接;防护:优先 spawn 数组参数(不经 shell)、避免 exec/shell:true、输入严格白名单校验、最小权限运行(非 root);四是"SSRF"——入口:服务端发起请求的 URL 由用户控制(图片代理、webhook、URL 抓取功能 fetch(userUrl)、DNS 重绑定),攻击者让服务器访问内网/云元数据(169.254.169.254);防护:URL 白名单(协议/域名校验)、解析后校验最终 IP(防 DNS 重绑定)、禁止内网/保留地址段、代理出口限制、对响应做类型与体积限制。

治理框架:一是"默认安全"——输入校验(schema 校验库:zod/joi)、安全 API 优先(spawn 数组、path 规范化)、最小权限(进程用户、文件权限、网络出网限制);二是"依赖与配置"——依赖安全(audit/OSV)、配置安全(secrets 管理、环境变量不进代码);三是"测试与审计"——安全测试(针对四类漏洞的用例)、SAST(eslint-plugin-security、Semgrep)、渗透/审计周期;四是"纵深防御"——CSP、鉴权授权、速率限制、日志与告警(异常访问模式)。

本题考察服务端安全的基础面:四类漏洞的入口-防护对应关系与治理框架。回答应逐类说明机制、Node 入口与防护手段,再给安全治理闭环。