NodeJS充分利用多核CPU以及它的稳定性

NodeJS是基于chrome浏览器的V8引擎构建的,也就说明它的模型与浏览器是类似的。我们的javascript会运行在单
个进程的单个线程上。这样有一个好处:

  • 状态单一
  • 没有锁
  • 不需要线程间同步
  • 减少系统上下文的切换
  • 有效提高单核CPU的使用率

服务器进程模型的进化

1、同步单进程服务器

执行模型是同步的(基于read或select I/O模型),服务模式是一次只能处理一个请求,其他的请求(处于阻塞等待的状态)都需要按照顺序依次等待接受处理
处理能力低下。
假如服务器每次响应请求处理的时间为N秒,那么这类服务器的QPS为1/N

2、同步多进程服务器

特点

解决同步单进程服务器无法处理的并发问题,
这类服务器通过进程的复制同时服务更多的请求和用户。

缺点

因为在进程的复制总会复制进程内部的状态,对于每个连接都进行这样的复制的话,相同的状态会在内存中存在很多份,造成浪费。同时这个过程会因为复制很多个进程影响进行的启动时间。而且服务器的进程数量也是有上限的。所以,这个模型并没有实质上解决并发问题。
假如这类服务器的进程数上限为M,每个请求处理的时间为N秒,那么这类服务器的QPS为M*1/N

3、同步多进程多线程服务器

特点

为了解决进程复制中的资源浪费问题,多线程被引入了服务模型
从一个进程处理一个请求改为一个线程处理一个请求。线程相对于进程的开销要小许多而且线程之间可以共享数据。此外可以利用线程池来减少创建和销毁线程的开销。

缺点

因为每个线程需要一定内存来存放自己的堆栈。当线程数量过多时进行上下文切换会非常耗费时间。所以在大的并发量下,多线程结构还是无法做到强大的伸缩性。
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模式,又称为主从模式。这种典型并行处理业务模式的分布式架构具备较好的可伸缩性(可伸缩性实际上是和并行算法以及并行计算机体系结构放在一起讨论的。某个算法在某个机器上的可扩放性反映该算法是否能有效利用不断增加的CPU。)和稳定性。主进程不负责具体的业务处理,而是负责调度和管理工作进程,工作进程负责具体的业务处理,所以,工作进程的稳定性是开发人员需要关注的。
进程,线程 - 图1
通过fork()复制的进程都是一个独立的进程,这个进程中有着独立而全新的V8实例。虽然Node提供了fork()用来复制进程使每个CPU内核都使用上,但是依然要记住fork()进程代价是很大的。好在Node通过事件驱动在单个线程上可以处理大并发的请求。

注意:这里启动多个进程只是为了充分将CPU资源利用起来,而不是为了解决并发问题。 **

Node单线程模型


Node.js采用 事件驱动 和 异步I/O 的方式,实现了一个单线程、高并发的运行时环境,而单线程就意味着同一时间只能做一件事。


1、事件循环
Node.js 在主线程中维护了一个事件队列,当接收到请求后,就将请求作为一个事件放入该队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非I/O任务,就亲自处理,并通过回调函数返回到上层调用;如果是I/O任务,就从线程池中拿出一个线程来执行这个事件,并指定回调函数,然后继续循环队列中的其他事件。当线程中的I/O任务完成后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫事件循环(Event Loop),如下图所示:
201707100934136.png
这个图是整个Node.js的运行原理,从左到右,从上到下,Node.js被分成了四层,分别是应用层、V8引擎层、Node API层 和 LIBUV层,
应用层: 即Javascript交互层,常见的就是Node.js的模块,比如 http,fs
V8引擎层: 即利用V8引擎来解析Javascript语法,进而和下层API交互
NodeAPI层: 为上层模块提供系统调用,一般是由C语言来实现,和操作系统进行交互
LIBUV层: 即Event Loop,是Node.js实现异步的核心,由LIBUV库来实现,而LIBUV中的线程池是由操作系统内核接受管理的。
从上述理解来看,Node.js的单线程仅仅是指Javascript运行在单线程中,而并非Node.js是单线程,在Node中,无论是Linux平台还是Windows平台,内部都是通过线程池来完成IO操作,而LIBUV就是针对不同平台的差异性实现了统一调用。
2、事件驱动
总结上面的过程可以发现,Node.js的核心是使用事件驱动模式实现了异步I/O,为了更具体、更清晰的理解和接受这个事实,我们用代码来描述Node.js的事件驱动模型:
3.1、事件队列
首先,我们需要定义一个事件队列,既然是队列,那就是一个先进先出(FIFO)的数据结构,我们用JS的数组来描述,如下:
?/**

  1. 定义事件队列
  2. 入队:unshfit()
  3. 出队:pop()
  4. 空队列:length == 0
  5. */

eventQueue:[],为了方便理解,我们规定:数组的第一个元素是队列的尾部,数组的最后一个元素是队列的头部, unshfit 就是在尾部插入一个元素,pop就是从头部弹出一个元素,这样就实现了一个简单的队列。

3.2、事件循环
当主线程处于空闲时就开始循环事件队列,所以,我们再定义一个事件循环的函数:

  1. /**
  2. * 事件循环主体,主线程择机执行
  3. * 循环遍历事件队列
  4. * 处理事件
  5. * 执行回调,返回给上层
  6. */
  7. eventLoop:function(){
  8. //如果队列不为空,就继续循环
  9. while(this.eventQueue.length > 0){
  10. //从队列的头部拿出一个事件
  11. var event = this.eventQueue.pop();
  12. //如果是IO任务
  13. if(isIOTask(event)){
  14. //从线程池里拿出一个线程
  15. var thread = getThreadFromThreadPool();
  16. //交给线程处理
  17. thread.handleIOTask(event)
  18. }else {
  19. //非IO任务处理后,直接返回结果
  20. var result = handleEvent(event);
  21. //最终通过回调函数返回给V8,再由V8返回给应用程序
  22. event.callback.call(null,result);
  23. }
  24. }
  25. },

主线程不停的检测事件队列,对于IO任务就交给线程池来处理,非IO任务就自己处理并返回。

进程与线程的区别


进程,线程 - 图3
1、首先是定义
进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。
2、一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。
3、线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小。
4、线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权,线程文本包含在他的进程 的文本片段中,进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段, 寄存器的内容,栈段又叫运行时段,用来存放所有局部变量和临时变量。
5、父和子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信。
6、进程内的任何线程都被看做是同位体,且处于相同的级别。不管是哪个线程创建了哪一个线程,进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制,进程中任何线程都可以通过销毁主线程来销毁进程,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程。
7、子进程不对任何其他子进程施加控制,进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制,进程中所有线程都可以对主线程施加控制。
相同点:
进程和线程都有ID/寄存器组、状态和优先权、信息块,创建后都可更改自己的属性,都可与父进程共享资源、都不鞥直接访问其他无关进程或线程的资源。

Process进程Api

  • process.arch() 返回当前的系统类型,一个操作系统CPU架构的字符串(系统的处理器),x64代表64位,x84代表32位
  • process.platform 返回当前系统的平台 分window和苹果
  • process.events 事件对象
  • process.argv() 返回一个数组,获取的命令行的参数
  • process.argv0 获取nodejs启动时传入的argv[0]的原始值只读副本
  • process.config 返回一个js对象,此对象描述了用于编译当前nodejs执行程序时涉及的配置项信息
  • process.cpuUsage 获取到当前进程用的CPU时间和系统CPU时间的对象
  • process..memoryUsage() 返回内存使用情况
  • process.cwd() 返回nodejs进程的当前工作目录//路径
  • process.version 返回node的版本
  • process.pid() 当前的进程号
  • process.ppid() 当前的进程号
  • process.versions 返回nodejs及其依赖项的版本,是一个对象
  • process.title 返回命令行参数
  • process.env 返回系统的环境变量,指向当前shell的环境变量
  • process.execPath 返回运行当前进程的可执行文件的绝对路径
  • process.stderr 输出错误
  • process.stdout 命令行的输出
  • process.stdin 命令行的输入
  • process.nexrTick 讲一个回调函数放在下次事件循环的顶部 效果更好,描述更准确
  • process.exit() 退出当前进程
  • process.abort() 触发abort事件,会让nodejs进程立即结束,并生成一个core文件
  • process.getgid() 获取进程的GID,GID为Gropld即组ID,用来标识用户组的唯一标识符
  • process.setgid() 设置进程的GID
  • process.getuid() 获取用户的ID
  • process.setuid(id) 设置用户的ID
  • process.initgroups(user,extra_group) 初始化group分组访问列表
  • process.kill 向指定进程pid发送一个信息singnal
  • process.hrtime 返回当前的好精度事件
  • process.uptime 返回node程序已经运行的描述
  • process.umask 设置或者读取进程文件的权限掩码
  • process.chdir 改变工作目录,process.cwd()查看目录切换是否成功,process.chdir(‘/home/bbb’)
  • process.uncaughtException 当前进程抛出一个没有被捕捉的意外时,会触发uncaughtException事件。
  • process.on(‘end’,(code)=>{//code:程序退出码}
  • process.mainModule 指向启动脚本的模块