一. 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十分相似,基于事件驱动的异步架构
Node中JS可以访问本地文件,搭建服务器,连接数据库
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能够在任何地方运行
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 属性上。然后调用
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越大读取速度越快