nodeJs入门的第一节课
- nodejs是什么?
- nodejs的架构模式以及优缺点
- nodejs异步IO
- nodejs事件驱动
- nodejs单线程
- nodejs应用场景
一、nodejs是什么?
1.1nodejs是一个开源的、跨平台的JavaScript的运行环境。就像java的运行环境JRE一样,比如JRE自带的java基础类库,在nodejs中也提供了一系列的JavaScript基础模块。
就nodejs应用阶段的学习我们可以通过学习这些基础模块的API应用,理解nodejs的架构模式以及这个架构模式的优缺点,通过对基础模块的应用深入理解nodejs的特性:异步IO、事件驱动、单线程,然后弄清楚在什么场景下可以使用nodejs。
然后就是nodejs与浏览器中的JavaScript的区别:
-- 在浏览器中JS大部分时间都是与DOM或Web平台的其他API(如H5的一些API)进行交互,在node中就不存在document、window和web平台的其他API。
-- 在浏览器中没有nodejs提供的模块机制友好的API,比如文件系统访问功能。
-- 最大的不同是使用nodejs你可以控制运行环境,而浏览器环境你是没办法选择的,所以一般情况下你并不需要做类型浏览器那样的兼容操作。除非你的项目是开源的要求其可以部署在任何nodejs环境中,比如像NPM、CLI这类工具。
-- 在模块系统上nodejs使用了CommonJS模块系统,而浏览器正在实施ES Modules标准。
1.2nodejs的架构模式:
-- Natives modules层:翻译为本机模块层,从直译角度不是很好理解,简单的理解就是它是nodejs这个JS运行环境的JS本身语言层,也就是说这一层的内容由JS实现。提供JS程序可以直接调用的库,例如fs、path、http等模块。
-- Bindings层:翻译为绑定层,这个从直译角度就非常好理解了,它就是绑定Natives modules的JS语言层和nodejs底层调用硬件的内核层的中间胶水层。因为JS无法直接调用底层硬件资源,如果JS需要与底层硬件设施进行通信,这需要一个桥梁,这个桥梁就是Bindings层。通过这个桥梁就可以让nodejs的核心模块获取到具体的服务支持,从而实现底层操作,比如:文件的读写行为。这一层模块基本上都是使用C/C++实现的,但它们并不提供具体的功能,更像是功能调用的对照表。比如Natives modules成的js代码需要调用一个A功能,而这个A功能的最终实现是基于C/C++实现的C功能,且C功能被放在另外一个地方,这时候A功能就需要去找到这个C功能,Bindings就是去帮助A找到C的这样一个对照表。
-- 底层模块:v8、libuv、c-ares(DNS)、http parser、zlib compressior等
v8:(JS内核)负责执行JS代码,提供桥梁接口。比如在JS调用了某个函数,而这个函数的具体功能最终是由C/C++实现的,这中间的具体调用和实现就需要v8来转换。v8为nodejs提供基础的初识化操作,创建执行上下文环境和作用域这些功能,nodejs有了v8就实现了JS的转换和调用底层的能力。
libuv库:nodejs中的事件循环、事件队列、异步IO底层都是基于这个库实现的。
-- 第三方功能库:c-ares(DNS)、http、zlib等都是具体的第三方功能。
-- 硬件资源和操作系统层:CPU/RAM/DISK OS
1.3nodejs中JavaScript组成:
在nodejs中的javascript是由ECMAScript和nodejs扩展的模块共同组成,当然扩展的包括nodejs內置的模块也包含我们编写的JS代码、以及引入的第三方JS依赖模块组成。
这里需要注意的nodejs中的是基于ES5的规范,如果需要使用ES6的规范需要使用babel或其他转换工具实现。
1.3nodejs的优缺点:
在讨论具体的优缺点之前,我们先来基于B/S架构模型来讨论同步、异步、单线程、多线程对架构的性能的影响:
假设现在有5个客户端请求同时发起请求,如果服务采用单线程处理这些请求,那么总时长就是(业务逻辑处理+IO处理)*5的时间之和。在现代服务器上这种单线程处理已经几乎不存在,从多线程的角度来说,假设现在服务器同步支持5个线程同时处理IO,那么总时长最糟糕的可能时长是(业务逻辑*5)+(处理最长时间的IO*5),并且如过处理最长时间的IO是第一个IO,这时候其他4个请求都需要等待第一个IO处理完毕,这种情况就会导致大量的请求被拥塞,导致客户端出现大量的客户端请求超时。
假设现在服务异步支持5个线程同时处理IO,总时长就是(业务逻辑*5)+处理最长时间的IO,并且可以使用已经处理完毕的请求继续处理后面的新请求,解决了请求被拥塞的问题。
所以从上面对B/S架构的分析来看,服务采用异步/IO多线程的策略就是最优的方案。这种模式主要就是解决了服务浪费时间等待IO处理的问题,这样的模式避免服务进行线程切换上下文的时候需要做的状态保存、时间消耗(IO时间)、状态锁等操作,这个模式也通常被叫做应答者模式 —— Reactor模式。
而nodejs基于v8内核本身就支持异步IO/多线程的Reactor模式,虽然这是nodejs被大量应用的原因,但并不代表nodejs就没有缺点,那么接下来就简单的基于nodejs的特性分析nodejs的优缺点。
nodejs区别传统服务的特点:异步事件驱动、非堵塞IO、单线程。通过下一节的内容来逐一分析这些特性,深入的了解nodejs的优缺点。
二、nodejs的特点及适用的应用场景
2.1异步IO
IO操作分为两类:阻塞IO、非阻塞IO,这种区分实际上并不在IO操作的本身,而是在调用IO操作的主进程上才能体现出来。因为多核CPU就支持多进程的IO操作,这种操作进程在相互之间并不会产生冲突,而是服务上调用系统IO操作的主线程是如何处理不同进程的IO操作。我们都知道每个IO操作进程会因为当前操作的资源大小需要不同的操作时间,这时候服务上的主线程是按照调用IO操作的现后顺序在原地逐个等待IO操作的返回结果;还是将IO操作启动后丢给一个管理机制,由这个管理机制去监听IO操作的返回结果,然后当这一机制监听到了IO操作的返回结果再通知主线程来处理。
显然,在nodejs中选择了后者,管理IO操作的机制被称为事件轮询,在后面的事件驱动架构中会就这个机制做具体分析。这里只需要大概的了解nodejs中的是基于主线程的事件轮询机制,实现的异步非阻塞IO,大概的实现架构逻辑参考下图:
异步IO逻辑流程图解析:
-- node代码:调用IO异步事件任务API;
-- event loop(事件轮询):基于事件路由分解器和事件队列,循环监听事件(IO请求/响应);当事件多路分解器中有空闲的事件处理资源,并且事件队列中有未处理的对应事件请求,event loop将对应的事件请求分配给事件多路分解器处理;当事件队列中有已经处理的事件将其处理结果交给主线程处理。并且当事件队列为空时会停止轮询。
-- event demultiplexer(事件多路分解器):负责监听系统上提供的硬件资源,并调用这些资源的系统API处理当前事件,然后将处理结果添加到事件队列中。
-- Event Queue(事件队列):负责缓存当前正在处理的事件对象,是事件轮询的资源。
-- 操作系统层IO接口:网络资源接口、文件资源接口等。
常见的轮询技术:read、select、poll、kqueue、event ports。
在nodejs的架构模式中底层模块有一个libuv这个组件库,也是事件多路分解器的底层,当在nodejs中发起一个事件通过事件轮询将IO任务传递给事件多路分解器,多路分解器会根据事件类型匹配相对应的libuv组件去处理IO任务,通过下面的libuv组件库示图了解都有哪些组件:(关于这个组件库更多详细内容可以参考官网:http://libuv.org/)
最后总结一下nodejs异步IO的优缺点:在高并发程序中异步IO肯定是nodejs的优势,反之不能说是缺点而是同步异步都无所谓,根据项目的实际应用场景而定,如果在非必要的情况下使用nodejs的异步IO导致项目架构变得更复杂那肯定是劣势。
2.2事件驱动
首先了解一下事件驱动的概念:所谓事件驱动是指在持续事件任务管理过程中,进行任务处理决策的一种策略,即跟随当前程序上出现的事件,调用可用资源处理相关任务,使得随着程序的运行持续出现的任务得以处理,防止任务堆积。
nodejs中的事件驱动机制是基于JS内核v8的异步编程作为基础实现,所以他与浏览器的事件机制除了在API上有一些差异,内部机制完全一致。其优点就是可以充分利用系统资源,实现执行过程中无需阻塞等待某个事件完成再往下执行。同样其优点也是其缺点,异步编程的风格会导致某些功能的代码过于分散、回调地狱等问题导致代码编写和阅读的体验不是很好;其二,既然事件采用异步模式就必然需要消耗资源去做监听和响应,但这相对于同步阻塞来说或许并不是缺点。
这里先简单的介绍一下nodejs的事件驱动机制的概念和优缺点,后面再在具体的nodejs模块分析中对事件API做具体的应用分析。
2.3单线程
nodejs的单线程机制还是基于JS内核v8的单线程机制,nodejs单线机制不是说nodejs就是单线程,而是说nodejs的主线程是采用单线程机制,然后配合异步事件多线程协作处理各种任务。
nodejs单线程的优点:
-- 不用像多线程编程那样,处处注意状态的同步问题,不会出现死锁的情况,也没有线程上下文交换所带来的性能上的开销。
nodejs单线程的缺点:
-- 无法利用多核CPU。
-- 错误会导致整个程序退出,应用的健壮性值得考验。
-- 如果有CPU密集型任务会导致异步I/O任务无法继续调用。
当下的nodejs基于的v8单线程机制是在最开始的时候Google公司为了解决JavaScript与UI共用一个线程出现长时间执行会导致UI渲染和响应会被中断,长时间的CUP占用也会导致异步I/O发不出调用,以完成的异步I/O回调函数也会得不到及时的处理。然后Google公司开发了Gears启用一个完全独立的进程,将计算量通过事件机制分发到其他进程,以此来降低运算造成的阻塞机率。后来HTML5制定了Web Workers的标准,Google放弃了Gears,全力支持Web Workers。Web Workers能够创建线程来进行计算,以此来解决JavaScript大计算阻塞的问题,工作线程不阻塞主线程,通过消息传递方式来传递运行结果,这也使得工作线程不能访问主线程中的UI。
nodejs中同样采用Web Workers创建线程的方式来解决单线程大量计算的问题:Child_process,子线程的出现既提高了单线程的健壮性,又解决了单程无法使用多核CPU的问题。关于如何通过子进程充分利用硬件资源和提升应用的健壮性,后面会有单独博客进行分析介绍。
2.4nodejs的适用应用场景
-- 非阻塞I/O特性使得nodejs适用I/O密集型高并发请求场景。基于这样的应用场景,很多高并发项目就基于nodejs在客户端和后端之间搭建了一个BFF层。BFF层主要用于处理网络请求、格式化数据、渲染HTML界面、合并接口、解决跨域需求、数据缓存。
-- 如果将nodejs看作是后端语言,项目不存在大量的业务逻辑的情况下,同样可以使用nodejs操作数据库,搭建一个轻量的高效的API服务,比如时实的聊天程序。
-- 前端工程化:nodejs本身基于JavaScript的平台,对于前端工程师来说是非常熟悉语言搭建的平台,并且还是基于浏览器的v8内核,应用nodejs在前端工程化中构建各种工具提高开发效率和协同工作具有非常大的价值。比如模块化构建的webpack、基于工作流的自动化构建工具glup、npm包管理器、还有各种各样的手脚架工具等。
2.5在特殊的应用场景下nodejs的适应性和问题如何解决
-- nodejs真的不擅长CPU密集型业务吗?
这个问题在前面的单线程特性里说过,因为密集型任务会导致异步I/O任务无法继续调用,这是一个nodejs的缺点,但是在一些项目需求中个方面都与nodejs的优点相契合,但在一些特殊的业务上会出现CUP密集型业务,这时候我们会非常纠结到底要不要使用nodejs,针对这一问题我们先来看看基于斐波那契数列算法实现n=40的计算各种语言的性能指数:
通过上面的测试指数可以显然的看到nodejs在性能上拥有不俗的表现,那么现在问题了,如我们一开始说的某些特殊业务可能出现CPU密集型业务,比如:长时运行的计算导致CPU事件片段不能释放,使得后续I/O无法发起。这种情况可以考虑将这个长时运算的大型运算任务分解成多个小任务,使得运算能适时的释放,不阻塞I/O的调用发起,这样就可以同时享受到并行异步I/O的好处,又能充分的利用CPU。
关于长时运算的分解方案有两种:
nodejs可以通过编写C/C++扩展的方式高效的利用CPU,将V8不能做到的性能极致通过C/C++来实现,在相关资料中这样的测试结论“通过C/C++扩展方式实现的斐波那契数列计算比Java还要快”。
如果上面的方案还不能达到要求,还可以通过子进程的方式,将一部分nodejs进程当作常驻服务进程用于计算,然后通过进程间的消息来传递结果,将计算与I/O分离,这样还可以充分的利用多核CPU。
-- nodejs如何与遗留系统和平共处?
过去大多都是同步的方式编写程序,这种串行调用下层应用数据的过程中充斥着串行的等待事件,如果采用多线程来解决这种串行等待有会显得小题大作。在这种情况或许会选择对系统进行彻底的重构,但这显然是一个非常大的工程。如果此时系统依然能稳定的提供数据输出,持续为网站服务,同时还能为移动端项目提供数据源。这时候或许可以考虑使用nodejs将该数据源当作数据接口,发挥nodejs的异步并行优势,而不用关心背后是用什么语言实现。
还有就是在一些JAVA项目中,为了避免java烦锁的表达,可以考虑使用nodejs替代java/jsp来完成web端的开发,使得前端工程师在HTTP协议的两端高效灵活的开发。这既利用了java作为后端和中间件的稳定性,又提高了开发中的协同合作效率。
-- nodejs的分布式应用:
分布式应用意味者可伸缩性的要求非常高,数据平台通常在一个数据集群中去寻找需要的数据。这时候可以考虑基于nodejs的中间层应用,这样可以利用nodejs实现高效的并行I/O,也同时实现了高效使用数据库。对于nodejs而言,这个只是一个普通的I/O,但对于数据库而言,却是一次复杂的计算,所以也是充分压榨硬件资源的过程,实际的应用案例有NodeFox、ITier。