Node特性

传统的Web服务器端是多线程的,而Node是单线程的。
多线程就像去银行办理业务有多个窗口,但每个窗口一次只能接待一位客户,当窗口为客户办理业务时其他客户就只能等待。
Node的单线程就像去咖啡馆。一般只有一个收银台,顾客排队下单,店员拿到订单做好饮品后,会通知对应的客户。

  1. 单线程:

Node的运行时环境是基于V8引擎的。V8最大的特点是单线程,因此Node也是单线程的。
Node的单线程指的是主线程是“单线程”。所以不能有耗时很长的同步处理程序阻塞后面的执行,对于耗时任务,应该采用异步执行。
优点:

  • 减少了创建、销毁和切换线程的开销,执行速度相对更快;
  • 内存占用小;
  • 更加安全。多线程容易出现死锁的问题。

缺点:

  • 稳定性不足。线程出错会导致整个进程退出
  • CPU利用率。单线程只能利用CPU的一个核,对于多核CPU的利用不足。
    1. 异步I/O

对于阻塞I/O,当需要执行I/O操作读取硬盘和网络等数据时,线程会被阻塞,直到拿到数据。
对于非阻塞I/O,在发起I/O处理后,不用等请求完成,可以继续做其他事情。
非阻塞I/O在准备好数据以后还是要阻塞进程去内核读取数据,因此不算异步I/O;
而异步I/O是不会造成任何阻塞

  1. 事件驱动

事件驱动就是通过监听事件的状态变化来做出相应的操作。如读取文件。
如果某个事件的回调函数是计算密集型(CPU被占用)函数,那么这个回调函数将会阻塞所有回调函数的执行。

Node优缺点

  1. 优点:
    1. 强大的并发能力。可以瞬间处理大量的客户请求
    2. 丰富的模块。
    3. 成熟的Web框架。
  2. 缺点:

    1. CUP阻塞。不适合做CPU密集型的操作
    2. 稳定性低。因为是单线程,可以通过PM2来解决
    3. 弱类型。可以使用TS来解决

      Node内存限制

      默认情况下V8引擎限制了内存的使用(在64位系统下不超过1.4GB,32位系统下不超过0.7GB)。
      process.memoryUsage()来检查。
      使用 node --max-old-space-size=200或者node --max-old-space-size=200(单位为MB),来指定堆内存中老生代和新生代的最大值,是静态方法。运行时不能更改。

      node内存泄漏:

    4. 缓存:如使用Lodash库的memorize来缓存函数参数和结果

    5. 重复的事件监听;

      node内存分析工具:heapdump

      Node事件循环

      image.png
  • 定时器(timer)阶段:执行setTimeout和setInterval调度的回调任务。
  • 等待回调(pending callback)阶段:用于执行前一轮事件循环中被延迟到这一轮的I/O回调函数(处理网络、IO 等异常时的回调,有的 *niux 系统会等待发生错误的上报,所以得处理下。)
  • 闲置、准备(idle,prepare)阶段:只能在Node事件内部使用。
  • 轮询(poll)阶段:最重要的阶段,执行I/O事件回调,在适当的条件下Node会阻塞在这个阶段。
  • 检查(check)阶段:执行setImmediate的回调任务。
  • 关闭回调(close callback)阶段:执行close事件的回调任务,如套接字(socket)或句柄(handle)突然关闭

Node.js 对宏任务做了优先级划分,从高到低分别是 Timers、Pending、Poll、Check、Close 这 5 种,也对微任务做了划分,也就是 nextTick 的微任务和其他微任务。执行流程是先执行完当前优先级的一定数量的宏任务(剩下的留到下次循环),然后执行 process.nextTick 的微任务,再执行普通微任务,之后再执行下个优先级的一定数量的宏任务。。这样不断循环。其中还有一个 Idle/Prepare 阶段是给 Node.js 内部逻辑用的,不需要关心。

Node的多进程

创建多进程需要考虑的问题:

  • 进程间的通信:进程之间如何发送消息、传递数据。
  • 多进程管理:如何控制各个进程的启停,获取进程的运行状态

    1. PM2

    2. child_process

    3. cluster

    由于cluster模块是基于child_process封装的,所以在进程管理上两者存在一些相似的API和事件。

    4. worker_threads

    这个功能是专门用来处理CPU密集型任务的,在处理I/O任务时并不建议使用,因为可能会降低执行效率。
    相比前面几个都优势:

  • 操作系统切换线程比切换进程更快,开销更小。

  • 线程之间可以共享内存。在处理大型数据的时候就变得非常有优势,比如父进程中有1GB的数据需要发送给两个子进程进行处理,那么由于内存不共享,每个子进程也需要使用1GB内存空间来存放数据,这样父子进程总共要占用3GB内存,而使用线程不需要额外创建。
  • 线程共用父进程 ID,方便管理。比如遇到异常情况要批量停止这些线程时,只需要kill 掉父进程,这些子线程就会随之停止。