概述nodejs核心机制

1,517 阅读4分钟

nodejs

  • Non-Blocking I/O Model
  • Event Loop
  • Event-Driven
    • 基本架构
    • 何为阻塞
    • 代码执行时
    • 阻止事件循环的几个维度
    • Worker Pool
    • npm模块的风险

Non-Blocking I/O Model

non-blocking是指node.js进程中不同步等待执行非javascript操作(例如I/O)完成而继续执行下一块代码的特性。

注:CPU密集型属于javascript操作。

I/O通常指与磁盘网络的交互

非阻塞I/O模型使得nodejs支持高并发且非常适合于I/O密集型应用

Nodejs Event Loop and Worker Pool

共6个阶段

  • timers setTimeout与setInterval回调函数队列
  • pending callbacks 会在下一次loop中执行的系统级回调队列。如TCP ECONNREFUSED -idle,prepare 内部使用
  • poll 接收新的I/O事件。执行I/O相关回调。在这个阶段node进程可能会阻塞
  • check setImmediate回调会在这个阶段执行
  • close 一些关闭的回调。比如connect.on('close', () => {....})

注:process.nextTick不属于任何一个阶段,它是介于任意两个阶段之间,并且在阶段切换时执行nextTick回调

Event-Driven

基本架构

  • nodejs事件驱动架构中有两种线程:事件循环线程(Event Loop)以及工作线程池(worker pool)
  • Event Loop负责编排客户端请求而后调度Worker Pool处理CPU密集型任务

注:因此nodejs并不是纯粹的单线程语言!

何为阻塞

  • 如果Event Loop执行回调或worker执行任务需要很长时间,即为阻塞。当发生阻塞时,主要会有两点需要考虑:
    • 性能:如果某worker线程定期执行heavyweight任务,会影响服务吞吐量(请求/秒)
    • 安全性:假设某些输入会引起程序阻塞,则存在被恶意客户端利用并攻击的风险。即拒绝服务攻击。

代码执行时

  • 在Event Loop中同步执行常规的变量、方法的定义与调用,javascript所有回调以及非阻塞异步I/O如网络I/O
  • Worker Pool是libuv(线程池工作调度的c++库)在Worker Pool中异步执行“昂贵”繁重的任务。node提供非阻塞I/O(操作系统不提供)API,以及CPU密集的I/O API
    • I/O密集型API:
      • DNS: dns.lookup()
      • fs: fs.readFile(),除了那些显示说明同步的方法
    • CPU密集型API:
      • crypto: crypto.pbkdf2()
      • zlib: 除了那些显示说明同步的方法

Event Loop实质

抽象来说,Event Loop维护挂起事件的队列,Worker Pool维护挂起任务的队列。

实际上,Event Loop并不是维护一个队列。而是一个文件描述符的集合,这些文件描述符从系统级事件通知机制获取比如epoll(linux),kqueue(OSX),IOCP(Windows)。这些文件描述符对应于某些网络套接字以及node正在监视的文件等等。当某个描述符准备好时,Event Loop会将其转换为合适的事件并执行对应的回调。

另外,Worker Pool维护的是一个真正的队列。Worker会pop出队列的task并执行,完成后会触发Event Loop“至少一个事件已完成”的事件。

阻止事件循环的几个维度

  1. 数据处理流程中是否包含计算复杂度高的任务,比如使用CPU密集型Node API比如crypto,fs,zlib,child-process(分区处理与offload to Worker Pool)
  2. ReDoS攻击,检查是否存在易受攻击的正则表达式(使用安全正则表达式库做安全校验
  3. 是否在主线程中使用JSON.parse以及JSON.stringify(潜在风险,因此也建议offload给Worker Pool)

Worker Pool

nodejs默认的Worker Pool专门用于处理I/O任务,维护自己的线程池可以使用cluster模块以及child_process模块做自定义线程池。

Node服务器的吞吐量取决于WorkerPool的吞吐量。有效降低逐个任务时间开销以及稳定任务时间开销的变化将最大程度提升服务器的吞吐量。最常见的方法就是复杂重复型任务(比如数组迭代)做分区处理。

注:由于调度Worker Pool会增加额外的通信开销,因为Worker Pool无法获取主线程的命名空间从而无法直接读取Javascript对象,所以需要序列化/反序列化导致增加通信成本。

npm模块的风险

npm生态系统中存在数十万个模块为开发者提供了极大的便利,然而社区中npm包良莠不齐,因为无法较为准确的估计其使用Event Loop或者Worker Pool的成本而导致一些程序隐患。