Skip to main content

NodeJS初探

Nodejs官网的定义:Node.js是一个基于Chrome V8引擎的javascript运行环境。Node.js使用了一个事件驱动、非阻塞式I/O的模型,使其轻量又高效
简单来说:Node.js不是一门语言也不是框架,它只是基于Google V8引擎的JavaScript运行时环境,结合了Libuv扩展了JavaScript功能,使其能够支持IO、fs等特性,使得JavaScript能够同时具有操作DOM和IO的能力,是目前最简单的全栈式语言。
Node.js可用于多个领域的开发例如:

  • 客户端应用程序(nw.js/electorn)
  • 后端(webapi、koa)
  • 工具(glup,webpack等构建工具)
  • 物联网、硬件(ruff)

Node.js基础

Node.js是基于Chrome V8引擎构建的,由事件循环(Event Loop)分发I/O任务,最终工作线程(Work Thread)将任务丢到线程池(Thread Pool)里面去执行,而事件循环只要等待执行结果就可以了,如下是Node.js早起的架构图。
Locale Dropdown

  • Node.js Bindings 层将Chrome V8等暴露的C/C++接口转成JavaScript Api,并且结合这些Api编写了Node.js标准库,所有这些API统称为Node.js SDK。
  • V8层是Google发布的开源JavaScript引擎,主要是将JS代码编译成原生机器码。
  • Thread Pool层:专门用来执行任务,执行完成后,将结果返回给EventLoop层。
  • EventLoop层:事件循环层,在代码执行时,首先由Event Loop来接受处理,而真正执行操作的是具体的线程池(Thread Pool)里的I/O任务,之所以说Node.js是单线程,就是因为在接受任务的时候是单线程的,它无需进程/线程切换上下文的成本,非常高效,但它的执行具体任务的时候是多线程的。
提示

Event Loop事件循环,Thread Pool线程池都是由Libuv提供,Libuv是整个Node.js运行的核心。

异步编程

异步编程是Node.js的一大特色,掌握好Nodejs的异步编程是每个Node.js开发者必备的技能。

异步I/O的好处

  • 前端通过异步IO可以消除阻塞
  • 请求耗时少
  • IO是昂贵的额
  • Node.js更适合IO密集型,而不适用于CPU密集型
  • 并不是所有场景都用异步任务好,遵循一个公式: s= (Ws+Wp)/(Ws+Wp/p) Ws 表示同步任务,Wp 表示异步任务,p 表示处理器的数量。

Node.js对异步I/O的实现

如下是Node.js异步I/O实现图:
Locale Dropdown

  • 应用程序现将JS代码经V8转换为机器码
  • 通过Node.js Bindings层,向操作系统Libuv的事件队列中添加一个任务
  • Libuv将事件推送到线程池中执行
  • Libuv将返回结果通过Node.js Bindings返回给V8
  • V8再将结果返回给应用程序

Libuv实现了Node.js中的Event Loop,主要有以下几个阶段:

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

上图中每一个阶段都有一个先进先出的回调队列,只有当队列内的事件执行完成之后,才会进入下一个阶段。

  • timers:执行setTimeoutsetInterval中到期的callback
  • pending callbacks:上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段执行。
    • 执行一些系统操作的回调,例如tac连接发生错误
  • idle、prepare:仅内部使用
  • poll:为最重要的阶段,执行I/O callback(node 异步api的回调,事件订阅回调等),在适当的条件下会阻塞在这个阶段。
    • 如果poll队列不为空,直接执行队列内的事件,直到队列清空。
    • 如果poll队列为空
      • 如果有设置setImmediate,则直接进入到check阶段
      • 如果没有设置setImmediate,则会检查是否有timers事件到期。
        • 如果有timers事件到期,则执行timers阶段
        • 如果没有timers事件到期,则会阻塞在当前阶段,等待事件加入
  • check:执行setImmediate的callback
  • close callbacks:执行close事件的callback,例如socket.on("close", func)

除此之外,Node.js提供了process.nextTick(微任务,promise也一样)方法,在以上的任意阶段开始执行的时候都会触发

小知识
  • Event Loop是一种很重要的概念,指的是计算机系统的一种运行机制
  • Libuv在Linux下基于Custom Threadpool实现
  • Libuv在Windows下基于IOCP实现

常用的异步I/O使用方式

  • 使用step,q,async等异步控制库。
  • 使用Promise处理异常
  • 使用EventEmitter,实现“发布/订阅”模式处理异步
  • Node.js暂不支持协程,可使用Generator代替
  • 终极解决方案:async、await

Node.js内存管理

Node.js是单线程的,所以必须保证这个线程持续稳定,最容易导致Node.js应用程序挂掉的因素是内存泄露。常见的内存泄露:

  • 无限增长的数组。
  • 无限制设置对象的属性和值。
  • 任何模块的私有变量都是永驻的。
  • 大循环,无 GC 机会。
  • 队列消费不及时。
  • 全局变量太多

Node.js 采用 V8 的 分代式垃圾回收策略,将内存分为新生代内存老生代内存

  • 新生代内存通过Scavenge算法,将内存分位Foem空间和To空间,初始时Form空间存放所有对象,To空间空闲。在一次垃圾回收时,清除Form空间中没有使用的对象,将To空间和Form空间交换
  • 老生代内存通过Mark-Sweep和Mark-compact,标记清除和移动清除。标记没有使用的内存空间,标记完毕后进行统一清除,清楚后为了避免内存空间不连续,会将已使用的内存连在一起,放在队列的一端,然后清除另一端的所有内存空间。

Node.js调试与部署

由于Node.js单线程的原因,所以Node.js的调试和部署特别重要,因为一旦出错,整个应用程序就挂了。

Node.js调试

  • node --inspect app.js
    • 打开浏览器进行调试:chrome://inspect/#devices
  • 使用vscode自带的调试

Node.js部署

Node.js端一般不会直接当成项目后端来使用,而是当成BFF层来使用,如下是Node.js项目架构图:
Locale Dropdown

  1. 用户请求Node.js服务器
  2. 经过LVS传输层负载均衡
  3. 经过Nginx服务器,反向代理,负载均衡到多个PM2运行的机器上。
    • PM2守护进程,保证Node.js进程永远活着,Node.js挂掉后自动0秒重载。
  4. 配置Varnish、Squid,实现服务器HTTP缓存
  5. Node.js作为Web服务层,会将请求转发微Java服务器。
    • 配置心跳检测Heartbeats,检测Java端是否挂掉。
  6. Java服务器根据请求,做相应的业务处理,可能会对数据库进行读写操作。
  7. 服务或写入Database
    • 只读数据库
    • 只写数据库
  8. Java服务器将数据库访问结果返回给Node.js层。
  9. Node.js层将结果返回给用户。

Node.js web端应用程序部署流程

  • 单元测试
  • 压力测试、性能分析工具
    • 提前发现内存泄露问题
    • 根据压力测试,准确计算QPS,推算出服务器性能。
  • 静态资源上传到CND
  • 配置Nginx实现负载均衡和反向代理
  • 开启PM2守护进程,小流量灰度上限。

剩下的工作交给运维和后端去完成,例如:配置Web服务器缓存Varnish、心跳检测等。