深入浅出Node.js

Posted ZyBlog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出Node.js相关的知识,希望对你有一定的参考价值。

一、Node简介

二、模块机制

A.CommonJS规范

1.模块引用:通过require()方法来引入外部模块

2.模块定义:提供exports对象用于导出当前模块的方法或者变量,并且是唯一导出的出口

3.模块标识:就是传递给require()方法的参数,必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径

B.Node的模块实现

1.不论是核心模块还是文件模块,require()方法对相同模块的干净加载都一律采用缓存优先的方式,这是第一优先级的

2.核心模块》路径形式的文件模块》自定义模块(自定义模块的生成方式与JS原型链或作用域链的查找方式十分类似)

3.Node会按.js、.json、.node次序补足扩展名,在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在,这里会是一个引起性能问题的地方,如果是.node和.json文件,在传递给require()时带上扩展名

4.js模块的编译:包装成(function(exports, require,module,__filename,__dirname)){….})的方式

C.核心模块

1.JS核心模块

  • Node采用了V8附带的js2c.py工具,将所有内置的JS代码转换成C++里的数组,生成node_natives.h头文件

  • 与文件模块的区别在于:获取源代码的方式(核心模块是从内存中加载的)以及缓存执行结果的位置

2.C/C++核心模块

  • C++主内完成核心,JS主外实现封装的模式,Node的buffer、crypto、evals、fs、os等模块都是部分通过C/C++编写的

D.C/C++扩展模块

1.JS的一个典型弱点是位运算,效率不高

E.模块调用栈

1.C/C++内建模块属于最底层模块,如果不是非常了解要调用的C/C++内建模块,尽量避免使用process.binding()方法直接调用

2.JS核心模块的职责:作为C/C++内建模块的封装层和桥接层;纯粹的功能模块;

3.文件模块通常由第三方编写,包括普通JS模块和C/C++扩展模块

F.包与NPM

1.包描述文件:package.json,可以帮助Node解决依赖包安装的问题

G.前后端共用模块

1.AMD、CMD规范

三、异步I/O

A.为什么要异步I/O

1.用户体验

2.资源分配

  • 单线程同步编程模型会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程模型也因为编程中的死锁、状态同步等问题让开发人员头疼

  • Node在两者之间给出了它的方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好地使用CPU

B.异步I/O实现现状

1.阻塞/非阻塞:操作系统内核对于I/O只有两种方式,阻塞与非阻塞

  • 在调用阻塞I/O时,应用程序需要等待I/O完成才返回结果

  • 阻塞I/O的一个特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束

  • 非阻塞I/O的差别是调用之后立即返回,返回的并 不是业务层期望的数据,而仅仅是当前调用的状态。为了获取完整的数据,需要重复调用I/O操作来确认是否完成

  • 这种重复调用判断操作是否完成的技术叫做轮询:read(原始、性能最低)、select(改进read,只能同时检查1024个文件描述符)、poll(采用链表方式,但文件描述符多的情况下性能还是十分低下)、epoll(目前Linux下效率最高的I/O事件通知机制,真实利用了事件通知、执行回调的方式,而不是遍历查询)、kqueue(仅在FreeBSD系统下存在)

2.理想的非阻塞异步I/O:AIO(仅支持Linux,仅支持内核I/O中的0_DIRECT方式读取,无法利用系统缓存)

3.现实的异步I/O:模拟线程池、glibc的AIO、libeio、windows下的IOCP

C.Node的异步I/O

1.事件循环:Node自身的执行模型,在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们称为Tick,每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们

2.观察者:每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件,浏览器采用了类似的机制,Node中有文件I/O观察者、网络I/O观察者等

3.事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,事件被传递到观察者那里,事件循环则从观察都那里取出事件并处理

4.请求对象:从JS发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,叫做请求对象

5.事件循环、观察者、请求对象、I/O线程池这四者共同构成了Node异步I/O模型的基本要素

D.非I/O的异步API

1.定时器

  • setTimeout()和setInterval()与浏览器中的API是一致的,他们的实现原理与异步I/O类似,只是不需要I/O线程池的参与

  • 利用定时器观察者内部的一个红黑树,定时器并不精确

2.process.nextTick()

  • 相对轻量,每次调用时,只会将回调函数放入队列中,在下一轮Tick时取出执行

  • 定时器时间复杂度为O(lg(n)),nextTick()时间复杂度为O(1)

3.setImmediate()

  • 与nextTick()类似,优先级比nextTick()低,原因在于事件循环对观察者的检查是有午后顺序的,nextTick()属于idle观察者,setImmediate()属于check观察者

  • idle观察者->I/O观察者->check观察者

E.事件驱动与高性能服务器

1.Node通过事件驱动 的方式处理请求,无须为每一个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销,同时操作系统在调度任务时因为线程少,上下文切换的代价很低

2.nginx同样采用事件驱动的方式

四、异步编程

A.函数式编程

1.高阶函数:可以将函数作为参数或是返回值,并形成了一种后续传递风格,将函数的业务重点从返回值转移到了回调函数中

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

B.异步编程的优势与难点

1.优势

  • Node带来的最大特性莫过于事件驱动的非阻塞I/O模型,这是它的灵魂所在

  • Node是为了解决编程模型中阻塞I/O的性能问题的,采用了单线程模型,这导致Node更像一个处理I/O密集问题的能手

  • 呆计算不影响异步I/O的调度,那就不构成问题,建议对CPU的耗用不要超过10ms,或者将大量的计算分解为诸多的小量计算,通过setImmediate()进行调度

2.难点

  • 异步处理:Node在处理异常上形成了一种约定,将异步作为回调函数的第一个参数传回,不要对用户传递的回调函数进行异常捕获

  • 函数嵌套过程:对于Node而言,事务中多个异步调用的场景比比皆是,这并没有利用好异步I/O带来的并行优势

  • 阻塞代码:没有sleep()这样的线程沉睡功能

  • 多线程编程:由于前端浏览器存在对标准的滞后性,Web Workers没有流行下来,Node借鉴了这个模式,child_process是其基础API,cluster模块是更深层次的应用

  • 异步转同步:偶尔出现的同步需求将会因为没有同步API让开发者突然无所适从

C.异步解决方案

1.事件发布/订阅模式

  • 事件监听器模式是一种广泛用于异步编程的模式,是回调函数的事件化,又称发布/订阅模式

  • Node自身提供的events模块是发布/订阅模式的一个简单实现,Node中部分模块都继承自它

  • 事件发布/订阅模式自身并无同步和异步调用的问题,但在Node中,emit()调用多半是伴随事件循环而异步触发的,所以广泛应用于异步编程

  • 常常用来解耦业务逻辑,也是一种钩子机制,利用钩子导出内部数据或状态给外部的调用者

  • 如果对一个事件添加了超过10个侦听器,会得到警告;为了处理异常,EventEmitter对象对error事件进行了特殊对待

  • 利用once解决缓存雪崩问题

2.Promise/Deferred模式

  • Promises/A:只要具备then()有一份发即可

  • Promise通过封装异步调用,实现了正向用例和反向用例的分享以及逻辑处理延迟

  • Promise模式比原始的事件侦听和触发略为优美,它的缺陷则是需要为不同的场景封装不同的API,没有直接的原生事件那么灵活

  • Promise和秘决其实在于对队列的操作

3.流程控制库

  • 尾触发与Next:除了事件和Promise外,还有一类方法是需要手工调用才能持续执行后续调用的,我就将此类方法叫做尾触发,常见的关键词是next,应用最多的是Connect的中间件

  • 中间件机制使得在处理网络请求时,可以像面向切面 编程一样进行过滤、验证、日志等功能,而不与具体业务逻辑产生关联,以致产生耦合

  • 中间件并不要求每个中间方法都是异步的,但是如果每个步骤都采用异步来完成,实际上只是串行化的处理,没办法通过并行的异步调用来提升业务的处理效率

  • async方法:series()实现一组任务的串行执行;parallel()实现并行异步操作;waterfall()实现前一个结果是后一个的输入;auto实现自动依赖处理

  • Step库:默认实现串行方式,this中包含parallel()方法实现并行,group()实现分组

  • Wind库

D.异步并发控制

1.异步I/O与同步I/O的显著差距:同步I/O因为每个I/O都是彼此阻塞的,在循环体中,总是一个接一个调用,不会出现耗用文件描述符太多的情况,同时性能也是低下的;对于异步I/O,虽然并发容易实现,但是由于太容易实现,依然需要控制。尽管是要压榨底层系统的恒通,但还是需要给予一定的过载保护,以防止过犹不及

2.bagpipe的解决方案

  • 通过一个队列来控制并发量

  • 如果当前活跃(指调用发起但未执行回调)的异步调用量小于限定值,从队列中取出执行

  • 如果活跃调用达到限定值,调用暂存放在队列中

  • 每个异步调用结束时,从队列中取出新的异步调用执行

3.async的解决方案:parallelLimit()方法

五、内存控制

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

1.V8的内存限制:64位系统下约为1.4GB,32位系统下约为0.7GB

2.V8中,所有的JS对象都是通过堆来进行分配的,使用process.memoryUsage()来查看,heapTotal和heapUsed表示已申请到的内存和当前使用的量,rss是resident set size的缩写,即进程的常驻内存部分

3.在V8中,主要将内存分为新生代和老生代,新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象

4.在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要采用了Cheney算法;在老生代中主要采用了Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收

5.为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为这么多小“步进”,每做完一“步进”就让JS应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成

B.高效使用内存

1.作用域:如果变量是全局变量(不通过var或定义在global变量上),由于全局作用域需要直到进程退出才能释放,此时将导致引用 的对象常驻内存(常驻在老生代中),如果需要释放常驻内存的对象,可以通过delete操作来删除引用关系,在V8中通过delete删除对象的属性有可能干扰V8的优化,所以通过赋值方式解除引用更好

2.闭包:一旦有变量引用中间函数,这个中间函数将不会释放,同时也支使原始的作用域不会得到释放,作用域中产生的内存占用也不会得到释放。除非不同有引用,都会逐步释放

C.内存指标

1.查看内存使用情况

2.os模块中的totalmem()和freemen()这两个方法用于查看操作系统的内存使用情况,分别返回系统的总内存和闲置内存

不是通过V8分配的内存称为堆外内存,利用堆外内存可以突破内存限制 的问题

3.Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存

D.内存泄漏

1.在Node中,缓存并非物美价廉,一旦一个对象被当做缓存来使用,那就意味着它将会常驻在老生代中。缓存中的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功

2.尽量使用外部缓存,如Redis和Memcached

3.队列问题,如数据库写入操作的堆积:

  • 表层解决方案是换用消费速度更高的技术

  • 深层的解决方案应该是监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员

E.内存泄漏排查

1.node-heapdump、node-memwatch等工具

F.大内存应用

1.Node提供了stream处理大文件,如果不需要进行字符串层面的操作,则不需要V8来处理,可以尝试进行纯粹的Buffer操作,这不会受到V8内存堆的限制

六、理解Buffer

A.Buffer结构

1.Buffer是一个典型的JS与C++结合的模块,它将性能相关部分用C++实现,将非性能相关的部分用JS实现

2.Buffer受Array类型的影响很大,可以访问length属性得到长度,也可以通过下标访问元素;给元素的赋值如果小于0,就将该值逐次加到256,直到得到一个0到255之间的整数。如果得到的数值大于255,就逐次减256,如果是小数,舍弃小数部分

3.Node在内存的使用上应用的是在C++层面申请内存、在JS中分配内存的策略。Node采用了slab分配机制

B.Buffer的转换

1.字符串:

  • new Buffer(str,[encoding]);

  • buf.toString([encoding],[start],[end]);

  • Buffer.isEncoding(encoding),判断编码是否支持转换,可以使用iconv和iconv-lite库

C.Buffer的拼接

1.Buffer不等于字符串,只是会隐式转换!!需要注意编码问题

2.setEncoding()只能处理utf8、Base64和UCS-2/UTF-16LE这3种编码

3.用一个数组来存储接收到的所有Buffer片段并记录下所有片段的总长度,然后调用Buffer.concat()方法生成一个合并的Buffer对象。Buffer.concat()方法封装了从小Buffer对象向大Buffer对象的复制过程。

D.Buffer与性能

1.通过预告转换静态内容为Buffer对象,可以有效地减少CPU的重复使用,节省服务器资源。在Node构建的Web应用中,可以选择将页面中的动态内容和静态内容分离,静态内容部分可以通过预先转换为Buffer的方式,使性能得到提升。由于文件自身是二进制数据,所以在不需要改变内容的场景下,尽量只读取Buffer,然后直接输出,不做额外的转换,避免损耗

2.highWaterMark的大小对性能的影响

  • highWaterMark设置对Buffer内存的分配和使用有一定影响

  • highWaterMark设置过滤,可能导致系统调用次数过多

3.如果文件较小(小于8kb),有可能造成slab未能完全使用;对于大文件而言,highWaterMark的大小决定会触发系统调用和data事件的次数;读取一个相同的大文件时,highWaterMark值的大小与速度的关系:该值越大,读取速度越快

七、网络编程

A.构建TCP服务

1.服务器事件(net.createServer()):listening、connection、close、error

2.连接事件(net.connecct()):data、end、connect、drain、error、close、timeout

3.在Node中,由于TCP默认启用了Nagle算法

B.构建UDP服务

1.UDP事件:message、listening、close、error

C.构建HTTP服务

1.http服务端事件:connection、request、close、checkContinue、connect、upgrade、clientError

2.http客户端事件:response、socket、connect、upgrade、continue

D.构建WebSocket服务

E.网络服务与安全

1.Node在网络安全上提供了3个模块,分别为crypto、tls和https

八、构建Web应用

1.Cookie优化:减小Cookie的大小;为静态组件使用不同的域名;减少DNS的查询;

2.缓存规则:添加Expires或Cache-Control到报文头中;配置ETags;让Ajax可缓存; 

3.清除缓存:url请求后带版本号,如http://xxx.com/?v=1.0.0

4.Content-Disposition,inline表示内容只需即时查看,attachment表示数据可以存为附件

九、玩转进程

1.php的健壮性是由它给每个请求都建立独立的上下文来实现的

2.Master-Worker模式,又称主从模式。主进程不负责具体的业务处理,而是负责调度或管理工作进程,它是趋向于稳定的。工作进程负责具体的业务处理。

3.child_process模块:

  • spawn():启动一个子进程来执行命令

  • exec():与spawn()不同的是有一个回调函数获子进程的状况,可以指定timeout属性设置超时时间,适合执行现有命令

  • execFile():启动一个子进程来执行可执行文件,适合执行文件

  • Fork():创建Node的子进程只需要指定要执行的JS文件模块即可

4.WebWorker允许创建工作线程并在后台运行,使得一些阻塞较为严重的计算不影响主线程上的UI渲染

5.IPC(Inter-Process Communication,进程间通信),是为了让不同的进程能够互相访问资源并进行协调工作,Node中使用的是管道(pipe)技术

6.句柄是一种可以用来标识资源的引用,它的内部包含了指向对象的文件描述符

7.Cluster事件:fork、online、listening、disconnect、exit、setup

十、测试

A.单元测试

1.编写可测试代码的原则:单一职责、接口抽象、层次分离

2.单元测试主要包含断言、测试框架、测试用例、测试覆盖率、mock、持续集成等,Node还会加入异步代码测试和私有方法测试

3.断言:是一种放在程序中的一阶逻辑(如一个结果为真或是假的逻辑判断式),目的是为了标示程序开发者预期的结果——当程序运行到断言的位置时,对应的断言应该为真。若断言不为真,程序会中止运行,并出现错误信息

4.Node中的assert模块包含:ok()、equal()、notEqual()、deepEqual()、notDeepEqual()、strictEqual()、notStrictEqual()、throws()、doesNotThrow()、ifError()

5.单元测试测试风格:

  • TDD(测试驱动开发):关注所有功能是否被正确实现,表述方式偏向于功能说明书的风格

  • BDD(行为驱动开发):关注整体行为是否符合预期,表述方式更接近于自然语言的习惯

6.相关工具:mocha、blanket、jscover、muk、Makefile、travis-ci

B.性能测试

1.基准测试:benchmark

2.压力测试:ab、siege、http_load

十一、产品化

A.项目工程化

1.目录结构 :只要遵循单一原则即可

2.构建工具:Makefile、Grunt

3.编码规范:JSLint、JSHint

4.代码审查

B.部署流程

1.在实际的项目需求中,有两点需要验证:一是功能的正确性,一是与数据相关的检查

C.性能

1.拆分原则:做专一的事、让擅长的工具做擅长的事情、将模型简化、将风险分离

2.动静分离、启用缓存、多进程架构、读写分离

D.日志

1.访问日志、异常日志、数据库记录、分割日志

E.监控报警

1.监控:日志监控、响应时间、进程监控、磁盘监控、内存监控、CPU占用监控、CPU load监控、I/O负载、网络监控、应用状态监控、DNS监控

F.稳定性

1.多机器:需要考虑负载均衡、状态共享、数据一致性、反向代理

2.多机房

3.容灾备份

G.异构共存

1.通过协议与已有的系统进行异构共存

附录B.调试Node

1.Debugger

通过debugger;设置断点

使用node debug xxxx.js

步进指令:cont或c、next或n、step或s、out或o、pause

2.Node Inspector


https://github.com/zhangyue0503/html5js/tree/master/shenruqianchunodejs


以上是关于深入浅出Node.js的主要内容,如果未能解决你的问题,请参考以下文章

《深入浅出node.js(朴灵)》PDF下载

阿里大佬力荐,深入浅出node.js,匠心之作!

深入浅出Node.js - 模块机制

深入浅出Node.js 模块机制

深入浅出Node.js - 理解Buffer

深入浅出Node.js - 玩转进程