node深入浅出01

176 阅读14分钟

一. NodeJs 简介

1. Node命名与起源
为什么是js?
 高性能(chrome的V8引擎的高性能)
 符合事件驱动(JavaScript在浏览器中有广泛的事件驱动方面的应用)
 没有历史包袱(为其导入非阻塞的I/O库没有而外阻力)

为什么是Node?
 Node是构建诸如服务器、客户端、命令行工具等
 Node是不共享任何资源的单线程、单进程系统,其目标是成为一个构建快速、可伸缩的网络应用平台
 它通过通信协议来组织很多Node,非常容易通过扩展来构建大型网络应用
 每一个Node进程都构成这个网络应用中的一个节点
2.Node给JS带来的意义
Chrome浏览器: html+js+webkit+V8,  Node:js+V8
Node结构与Chrome十分相似,基于事件驱动的异步架构
NodeJS可以访问本地文件,搭建服务器,连接数据库
Node打破了过去JS只能在浏览器中运行的局面,前后端统一
3.Node特点
1)异步I/O
异步I/O的API,从文件读取到网络请求,均是

2)事件与回调函数
事件编程方式:轻量级,松耦合,只会关注事务点

const http = require('http'); 
const querystring = require('querystring'); 
// 侦听服务器的request事件
// 请求对象的data事件和end事件
http.createServer(function (req, res) { 
 var postData = ''; 
 req.setEncoding('utf8'); 

 req.on('data', function (trunk) { 
  postData += trunk; 
 }); 
 req.on('end', function () { 
  res.end(postData); 
 }); 
}).listen(8080); 
console.log('服务器启动ྜ成');

3)单线程
无法利用多核CPU
错误会引起整个应用退出,应用的健壮性
大量计算占用CPU导致无法继续调用异步I/O

解决方案:
child_progress:解决单线程中大量算量的问题
Master-Worker:管理各个工作(子)进程

4)跨平台:兼容Windows和*nix平台
4.Node的应用场景
1)I/O密集型
  利用事件循环的处理机制,资源占用极少。
2)不是很擅长CPU密集型业务,但是可以合理调度
3)与遗留系统问题和平共处
4)分布式应用
5.Node的使用者
1) 前后端编程语言环境统一
2) Node带来的高性能的I/O用于实时应用:Voxer的实时语音和腾讯的实时通知(socket.io)
3) 并行I/O使得使用者可以更高效地利用分布式环境:阿里巴巴和eBay
4) 并行I/O,有效利用稳定接口提升web渲染能力
5) 云计算平台提供Node支持
6) 游戏开发领域:网易的pomelo实时框架
7) 工具类应用

二. 模块机制

CommonJS规范给JS的愿景—— 希望js能够在任何地方运行

image

1.CommonJS规范
js本身: 没有模块系统 / 标准库较少 / 没有标准接口 / 缺乏包管理系统
js期望: 开发富客户端应用 / 服务端js应用 / 桌面图形界面应用 / 混合应用
js结果:
    Node借鉴CommonJS的Modules规范实现了一套非常易用的模块系统
    NPM对Packages规范的完好支持使得Node在应用开发过程中更加规范
模块的意义:
将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出的功能顺畅的连接上下游依赖

const math = require('math')
exports.increment = function (val) { 
 return math.add(val, 1)
 ...
}
2.Node模块的实现
模块的分类:
核心模块:Node提供的模块
文件模块:用户编写的模块(动态加载的)
模块的加载过程:
1)路径分析
2)文件定位
3)编译执行
8

待完善...

3.核心模块

待完善...

4.包与NPM
包结构:
package.json:包描述文件
bin:用于存放可执行二进制文件的目录
lib:用于存放JavaScript代码的目录
doc:用于存放文档的目录
test:用于存放单元测试用例的代码
包描述文件:
必需字段:name,description,version,keywords,maintainers
npm必需字段:contributios,bugs,licences,repositories,dependencies(依赖包列表)
NPM常用功能:
安装依赖包
最常见:     npm install express
全局安装:  npm install express -g
从本地安装:npm install <file>
从非官方源安装:npm install underscore --registry=http://registry.url
NPM潜在问题:
NPM平台上面包质量良莠不齐
Node代码可以运行在服务端,需要考虑安全问题
5.前后端共用模块
1)模块的侧重点(前端的瓶颈于带宽,后者的瓶颈在于cpu和内存)
  CommonJS适合后端js规范,前端适用AMD规范
2)AMD规范:是CommonJS模块规范的一个延伸
3)CMD规范:区别定义模块和依赖引入
4)兼容多种模块规范

三. 异步I/O

事件循环是异步实现的核心,与浏览器中的执行模型基本保持一致。

1.为什么要异步I/O
用户体验,消耗时间为max(M,N)
资源分配,让单线程远离阻塞,更好利用CPU

Node选择: 
利用单线程,远离多线程死锁,状态同步等问题,
利用异步I/O,让单线程远离阻塞,更好的使用CPU。
2.异步I/O实现现状
操作系统内核对于I/O只有两种方式:阻塞与非阻塞

阻塞I/O: 完成整个获取数据的过程
非阻塞I/O: 不带数据直接返回,要获取数据,还需要通过文件描述符再次读取(轮询)

阻塞I/O,造成CPU等待浪费
非阻塞I/O,轮询,会让cpu处理状态判断,对cpu资源的浪费
轮询技术的演进,以减小I/O状态判断的CPU损耗:
read -- 重复调用来检查I/O的状态来完成完整数据的读取(性能最低)
select -- 文件描述符上的事件状态来进行判断(最多1024个文件描述符)
poll -- 链表的方式避免数组长度的限制,能避免不需要的检查(性能低下)
epoll -- I/O事件通知机制,进行休眠(不会浪费CPU,执行效率较高)
kqueue -- 和epoll类似,存在FreeBSD系统下
理想的非阻塞异步I/O:

应用程序发起非阻塞调用,可以直接处理下一个任务(无需通过遍历或者事件唤醒等方式轮询)
在I/O完成后通过信号或回调将数据传递给应用程序
3.Node的异步I/O
step1.事件循环
 每执行一次循环体的过程, 称之为Tick,
 如果有,就取出事件及其相关的回调函数,执行它们,进入下个循环
 如果无,就退出进程

step2.观察者
 事件循环是一个典型的生产者/消费者模型
 异步I/O,网络请求等则是事件的生产者  -> 传递到观察者 -> 事件循环取出事件并处理
 事件对应的观察者有文件I/O观察者,网络I/O观察者
 
step3.请求对象
 从Js发起调用到内核执行完I/O操作的过度过程中,存在一种中间产物
 所有的状态都保存在这对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理

step4.执行回调
 回调通知,完成完整异步I/O的第二部分
 线程池中的I/O操作调用完毕后,会将结果存储在req -> reslut 属性上。然后调用

image

4.非I/O的异步API
定时器: setTimeout(),setInterval()
调用setTimeout()创建的定时器会被插入到定时器观察者内部的一个红黑树中,超过定时时间,
就形成一个事件,它的回调函数将立即执行

process.nextTick():操作比较轻量,高效
会将回调函数放入队列中,在下一轮Tick时取出执行,每轮循环中会将数组中的回调函数全部执行完

setImmediate()
优先级低于process.nextTick()
5.事件驱动和高性能服务器
事件驱动的本质:通过主循环加事件触发的方式来运行程序。
Node构建web服务器:

同步式:  一次只能处理一个请求,并且其余请求都处于等待状态

每进程/每请求:
    每个请求启动一个进程,这样可以处理多个请求。(不具备扩展性)
    
每线程/每请求: (Apache)
    为每个请求启动一个线程来处理。
    优点:线程比进程要轻量
    缺点:每个线程都占用一定内存,当大并发请求到来时,内存会很快耗光,导致服务器缓慢。

四. 异步编程

1.函数式编程
高阶函数:把函数作为参数,或者将函数作为返回值的函数

偏函数用法:创建一个调用另外一部分——参数或变量已经预置的函数——的函数的用法。
通过指定部分参数来创建一个新的函数的形式

如一些数组方法,都是高阶函数
forEach()、map()、reduce()、reduceRight()、filter()、every()、some()

function(x){
    return function(){...}
}
2.异步编程的优势与难点
优势:基于事件驱动的非阻塞I/O模型(灵魂所在)
效果:非阻塞I/O可以是CPU与I/O并不相互依赖等待,让资源得到更好的利用

对于网络应用而言,并行带来的想象空间更大,延展而来的是分布式和云
难点:
1)异常处理
    异步I/O的实现主要包含两个阶段:提交请求和处理结果。
    这两个阶段中间有事件循环的调度,两者彼此不关联。
    异步方法则通常在第一阶段提交请求后立即返回,因为异常并不发生在这个阶段。
2)函数嵌套过深
3)阻塞代码
4)多线程编程
5)异步转同步
3.异步编程解决方案

待完善...

事件发布/订阅模式
事件发布/订阅模式自身并无同步和异步调用的问题。
但在Node中,emit()调用多半是伴随着时间循环而异步触发

Promise/Deferred模式

流程控制库

4.异步并发控制

待完善...

问题:并发量过大,下层服务器将会吃不消。
      如果对文件系统进行大量并发调用,操作系统的文件描述符数量将会被瞬间用光
bagpipe的解决方案
async的解决方案

五. 内存控制

1.V8的垃圾回收机制与内存限制

内存控制正是在海量请求和长时间运行前提下,需考虑的

V8的内存限制:
Node中通过Js使用内存时只能使用部分内存(64位系统1.4GB,32位系统0.7GB)

V8的对象分配:
    在V8中,所有js对象都是通过堆来分配的
    限制堆大小原因:
    V8最初为浏览器而设计,不太可能使用到大量内存的场景
    V8垃圾回收机制的限制(一次增量式垃圾回收要1秒以上,会引起JS线程暂停执行的时间,响应能力会直线下降)

V8的垃圾回收机制:
实际应用中,对象的生存周期长短不一,不同的算法只能针对特定情况具有最好的效果
1) V8的内存分代
    将内存分为新生代和老生代
    新生代,对象为存活时间较短对象;老生代,对象为存活较长或常驻内存的对象
2) Scavenge算法
    复制的方式实现的垃圾回收算法,时间效率高, 适合于新生代对象中
3) Mark-Sweep & Mark-Compact
    Mark-Sweep标记清除:标记和清除,两个阶段
    Mark-Compact 标记整理, 内存碎片的问题
4)Incremental Marking (增量标记)
    全停顿:将应用逻辑展亭下来,待执行完来回收后再回复执行应用逻辑。
    垃圾回收与应用逻辑交替执行直到标记阶段完成。除了增量标记,V8还引入延迟清理,增量式清理。


想要高性能的执行效率,需要让垃圾回收尽量少的进行,尤其是全堆垃圾回收
查看垃圾回收日志:
node --tarce_gc -e
// 垃圾回收日志信息

node --prof test.js
// 垃圾回收时所占用的时间

通过分析垃圾回收日志,可以了解垃圾回收的运行状况,找出垃圾回收的哪些阶段比较耗时,触发的原因是什么
2.高效使用内存
作用域:
    形成作用域的有, 函数调用、width以及全局作用域
    标识符查找 --> 作用域链  --> 变量的主动释放
    
    
闭包:
    实现外部作用域访问内部作用域的中的变量的方法(得益于高阶函数的特性)
    一旦有变量应用这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,作用域中产生的内存占用也不会得到释放

3.内存指标
查看进程的内存占用:process.memoryUsage()
查看系统的内存占用:
    os.totalmem() //系统的总内存
    os.freemen()  //系统的闲置内存
    
    
堆外内存:
    Node中的内存使用并非都是通过V8进行分配的,这些不通过V8分配的内存,称为堆外内存
    如:buffer对象不经过v8内存分配,因此,也不会有堆内存的大小限制
    
汇总:Node的内存主要由通过V8进行分配的部分和Node自行分配的部分,受V8的垃圾回收限制的主要是V8的堆内存
4.内存泄漏
内存泄漏的实质是应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象
造成内存泄漏原因:缓存 / 队列消费不及时 / 作用域未释放
1) 缓存
    慎将内存当做缓存
    v8内存是通过垃圾回收进行处理的,没有过期策略,而真正的缓存是存在过期策略的
    缓存限制策略: 将结果记录在数组中,一旦超过数量,就以先进先出的方式进行淘汰。如果需要更高效的缓存,可以参与LRU算法,地址为
    
缓存的解决方案:
    最好是使用外部缓存(如redis), 将缓存转移到进程的外部,减少常驻内存的对象数量,让垃圾回收更有效率
    同时,进程间可以共享缓存,节约宝贵的资源
    
2) 队列消费不及时
    监控队列的长度,一旦产生堆积,应当通过监控系统报警
    设置合理的超时机制,一旦超时,通过回调函数传递超时异常
5.内存泄漏排查

定位Node应用的内存泄漏常用工具如下

工具说明
v8-profiler可以对v8堆内存抓取快照,并对cpu进行分析
node-heapdump可以对v8堆内存抓取快照,用于事后分析
node-mtrace使用gcc的mtrace工具来分析堆的使用
dtrace在smartos上使用的内存分析工具
node-memwatch采用wtfpl许可发布的内存分析工具
6.大内存应用

操作大内存,使用流的方式, stream模块

因为流使用了buffer作为读写的编码方式,因此,不受v8内存的限制。但是,物理内存依然有限制

var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.on('data', function (chunk) {
    writer.write(chunk);
});
reader.on('end', function () {
    writer.end();
});

//或者

var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.pipe(writer);

六. Buffer

在node中需要处理网络协议、操作数据库、处理图片、接受上传文件等,在网络流和文件操作中,
还要处理大量二进制数据,js自有的字符串远远不能满足这些需求,于是Buffer对象应运而生。
1.Buffer结构
真正的buffer内存是在node的c++层面提供的,js层面只是使用它
let buf = new Buffer(100)
2.Buffer的转换
Buffer对象可以和字符串进行相互转换,支持的编码类型有:ASCII、UTF-8、UTF-16LE/UCS-2、Base64、Binary、Hex

1)字符串转Buffer
new Buffer(str,'utf-8')

2)Buffer转字符串
buf.toString('utf-8', [start], [end])
3.Buffer的拼接
var chunks = [];
var size = 0;
res.on('data', function (chunk) {
    chunks.push(chunk);
    size += chunk.length;
});
res.on('end', function () {
    var buf = Buffer.concat(chunks, size);
    var str = iconv.decode(buf, 'utf8');
    console.log(str);
});

1) 用一个数组来存储接收到的buffer片段,然后调用buffer.concat()合成一个buffer对象
2) 解决上文乱码问题,设置一些编解码格式:setEncoding()和string_decoder(),传递的不再是buffer对象,而是编码后字符串
4.Buffer与性能
1)静态内容可以先转换为buffer对象,提升传输性能
2)文件读取时,有一个highWaterMark设置对性能影响至关重要
3)highWaterMark大小会触发系统调用和data事件的次数
4)highWaterMark越大读取速度越快