当前位置: 首页 > 后端技术 > Node.js

NodeJS充分利用了多核CPU及其稳定性

时间:2023-04-03 19:45:08 Node.js

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