NodeJS是基于chrome浏览器的V8
引擎构建的,也就说明它的模型与浏览器是相似的。咱们的javascript会运行在单个进程的单个线程上。这样有一个好处:javascript
状态单一java
没有锁算法
不须要线程间同步chrome
减小系统上下文的切换数据库
有效提升单核CPU的使用率浏览器
可是V8引擎的单进程单线程并非完美的结构,现现在CPU基本上都是多核的。真正的服务器每每有好几个CPU(像咱们的线上物理机有12个核),因此,这就将抛出NodeJS实际应用中的第一个问题:“如何充分利用多核CPU服务器?”服务器
另外,因为Node执行在单线程上,一旦单线程出现未捕获的异常,就会形成这个进程crash。因此就遇到了第二个问题:“如何保证进程的健壮性和稳定性?”网络
从严格意义上来说,Node其实并非真正的单线程架构,由于Node自身还有I/O
线程存在(网络I/O、磁盘I/O),这些I/O线程是由更底层的libuv
处理,这部分线程对于JavaScript开发者来讲是透明的。JavaScript代码永远运行在V8上,是单线程的。因此表面上来看NodeJS是单线程的。多线程
这类服务器是最先出现的,其执行模型是同步的(基于read或select I/O模型
),它的服务模式是一次只能处理一个请求,其余的请求都须要按照顺序依次等待接受处理。这就意味着除了当前的请求被处理以外,剩下的请求都是处于阻塞等待的状态。
因此,它的处理能力特别的低下。
假如服务器每次响应请求处理的时间为N秒,那么这类服务器的QPS为1/N
。架构
为了解决上面的同步单进程服务器没法处理的并发问题,这类服务器经过进程的复制
同时服务更多的请求和用户。一个请求须要一个进程来服务,也就是100个请求就须要100个进程来进行服务,这须要很大的代价。由于在进程的复制总会复制进程内部的状态,对于每一个链接都进行这样的复制的话,相同的状态会在内存中存在不少份,形成浪费。同时这个过程会由于复制不少个进程影响进行的启动时间。并且服务器的进程数量也是有上限的。
因此,这个模型并无实质上解决并发问题。
假如这类服务器的进程数上限为M,每一个请求处理的时间为N秒,那么这类服务器的QPS为M*1/N
。
为了解决进程复制中的资源浪费问题,多线程被引入了服务模型
,从一个进程处理一个请求改成一个线程处理一个请求。线程相对于进程的开销要小许多,并且线程之间能够共享数据。此外能够利用线程池来减小建立和销毁线程的开销。可是多线程所面临的并发问题只能说比多进程好点而已,由于每一个线程须要必定内存来存放本身的堆栈。另一个CPU核心只能处理一件事,系统是经过将CPU切分为时间片的方法来让线程能够均匀地使用CPU资源,在系统切换线程的过程当中也会进行线程的上下文切换(切换为当前线程的堆栈),当线程数量过多时进行上下文切换会很是耗费时间。
因此在大的并发量下,多线程结构仍是没法作到强大的伸缩性。大名鼎鼎的Apache
服务器就是采用了这样的架构,因此出现了著名的C10K
问题。
咱们忽略系统进行线程的上下文切换的开销,假如这类服务器能够建立M个进程,一个进程可使用L个线程,每一个请求处理的时间为N秒,那么它的QPS为M*L/N
。
为了解决C10K以及解决更高并发的问题,基于epoll(效率最高的I/O事件通知机制)
的事件驱动模型出现了。采用单线程避免了没必要要的内存开销和上下文切换开销。
不过这种基于事件的服务器模型存在的文章刚开始提出两个问题:“CPU的利用率和健壮性”。
另外,全部的请求处理都在单线程上进行,影响事件驱动服务模型性能的只有CPU的计算能力,它的上限决定了这类服务器的性能上限,但它不受多进程多线程模式中资源上限的影响,可伸缩性比前二者都高。若是能够解决多核CPU的利用问题,那么带来的性能提高是很是高的。
面对单进程单线程对多核使用率不高的问题,按照以前的经验,每一个进程各使用一个CPU便可,以此实现多核CPU的利用。Node提供了child_process
模块,而且也提供了fork()
方法来实现进程的复制(只要是进程复制,都须要必定的资源和时间。Node复制进程须要不小于10M的内存和不小于30ms的时间
)。
这样的解决方案就是*nix
系统上最经典的Master-Worker模式
,又称为主从模式
。这种典型并行处理业务模式的分布式架构具有较好的可伸缩性(可伸缩性其实是和并行算法以及并行计算机体系结构放在一块儿讨论的。某个算法在某个机器上的可扩放性反映该算法是否能有效利用不断增长的CPU。
)和稳定性。主进程不负责具体的业务处理,而是负责调度和管理工做进程,工做进程负责具体的业务处理,因此,工做进程的稳定性是开发人员须要关注的。
经过fork()复制的进程都是一个独立
的进程,这个进程中有着独立而全新的V8实例。虽然Node提供了fork()
用来复制进程使每一个CPU内核都使用上,可是依然要记住fork()
进程代价是很大的。好在Node经过事件驱动在单个线程上能够处理大并发的请求。
注意:这里启动多个进程只是为了充分将CPU资源利用起来,而不是为了解决并发问题。
建立一个子进程来执行命令
建立一个子进程来执行命令,和spawn()不一样的是方法参数不一样,它能够传入回调函数来获取子进程的状态
启动一个子进程来执行指定文件。注意,该文件的顶部必须声明SHEBANG符号(#!)用来指定进程类型。
和spawn()相似,不一样点在于它建立Node的子进程只须要执定要执行的JavaScript文件模块便可。
注意:后面的3种方法都是spawn()的延伸应用。
在Master-Worker
模式中,要实现主进程管理和调度工做进程的功能,须要主进程和工做进程之间的通讯。它们经过消息来传递内容,而不是共享文件或直接操做相关资源,这是比较轻量和无依赖的作法。
经过fork()或者其余的API建立子进程以后,为了实现父子进程之间的通讯,父进程与子进程之间将会建立IPC通道。经过IPC通道,父子进程之间才能够传递消息。
IPC全称 Inter-Process Communication
,也就是进程间通讯。进程间通讯的目的是为了让不一样的进程可以互相访问资源并进行协调工做。Node中的IPC建立和实现过程以下:
父进程在实际建立子进程以前,会先建立IPC通道并监听它,而后才真正建立出子进程。子进程在启动的过程当中会去连接这个已存在的IPC通道,从而完成了父子进程之间的链接。
建立好进程之间的IPC以后,若是仅仅只用来发送一些简单数据,显然不够咱们的实际使用。
理想状况下,无论服务启动了多少个进程都应该通过同一个Master进程来进行控制和调度。因此全部请求都应该先通过同一个端口,而后经过Master进程交由具体的Worker进程处理。
让每一个进程监听不一样的端口,其中主进程监听主端口(80端口),主进程对外接受全部的网络请求,再将这些请求代理到不一样的端口进程上。
经过代理,能够避免端口不能重复监听的问题,也能够在代理进程作适当的负载均衡,这样每一个子进程均可以均衡的处理服务。
因为进程每接受到一个链接,将会用到一个文件描述符,所以代理模式链接工做进程的过程须要用到两个文件描述符。操做系统的文件描述符是有限的。因此这种方案影响了系统的扩展能力
Nodejs提供了进程间发送句柄的功能。有了这个功能咱们能够不使用代理模式
方案,使主进程接受到socket请求以后,将这个socket对象直接转发给工做进程,而不是从新遇工做进程之间建立新的socket链接来转发数据。这样的话,文件描述符浪费的问题能够轻轻松解决。
在程序设计中,句柄(handle)是一种特殊的智能指针。当一个应用程序要引用其余系统(如数据库、操做系统)所管理的内存块或对象时,就要使用句柄。
这样全部的请求都是由子进程处理了。整个过程当中,服务的过程发生了一次改变以下图:
主进程发送完句柄并关闭监听以后,就成了下图的机构。
多个应用监听相同的端口时,文件描述符同一时间只能被某个进程所用,也就是网络请求发送的服务器端时,只有一个幸运的进程可以抢到链接,只有它能为这个请求进行服务。因此这些进程服务是抢占式
的。
至此,以此介绍了建立子进程、进程间通讯的IPC通道实现、句柄在进程间的发送和使用原理、端口共用等细节。经过这些基础技术,在多核的CPU服务器上,让Node进程可以充分利用资源不是难题。
搭建好了集群,充分利用了多核CPU的资源,可是在迎接大量的客户端请求以前,还有不少稳定性的问题亟待解决。
工做进程存活状态管理
工做进程平滑重启
工做进程限量重启
工做进程性能问题
工做进程负载均衡
工做进程状态共享
这些问题下次再写。。。