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早起的架构图。
- 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实现图:
- 应用程序现将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:执行
setTimeout和setInterval中到期的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项目架构图:
- 用户请求Node.js服务器
- 经过LVS传输层负载均衡
- 经过Nginx服务器,反向代理,负载均衡到多个PM2运行的机器上。
- PM2守护进程,保证Node.js进程永远活着,Node.js挂掉后自动0秒重载。
- 配置Varnish、Squid,实现服务器HTTP缓存
- Node.js作为Web服务层,会将请求转发微Java服务器。
- 配置心跳检测Heartbeats,检测Java端是否挂掉。
- Java服务器根据请求,做相应的业务处理,可能会对数据库进行读写操作。
- 服务或写入Database
- 只读数据库
- 只写数据库
- Java服务器将数据库访问结果返回给Node.js层。
- Node.js层将结果返回给用户。
Node.js web端应用程序部署流程
- 单元测试
- 压力测试、性能分析工具
- 提前发现内存泄露问题
- 根据压力测试,准确计算QPS,推算出服务器性能。
- 静态资源上传到CND
- 配置Nginx实现负载均衡和反向代理
- 开启PM2守护进程,小流量灰度上限。
剩下的工作交给运维和后端去完成,例如:配置Web服务器缓存Varnish、心跳检测等。