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

说说Nodejs的高并发原理

时间:2023-04-03 18:43:24 Node.js

写在前面。我们先来看一些常见的说法。Nodejs是单线程+非阻塞I/O模型。Nodejs适合高并发。Nodejs适用于I/O密集型应用程序,而不是CPU密集型应用程序。在具体分析这些说法是否正确以及为什么正确之前,我们先做一些准备工作,开始说说一个普通的Web应用程序会做什么操作(执行业务逻辑、数学运算、函数调用等,主要工作在CPU中执行)I/O(如读写文件、读写数据库、读写网络请求等。主要工作在各种I/O设备上,如磁盘、网卡等)一个典型的传统Web应用实现了多种processes,andarequestforkA(child)process+blockingI/O(blockingI/OorBIO)multithreading,arequestcreateathread+blockingI/Omulti-processwebapplicationexample伪代码listenFd=newSocket();//创建监听器socketBind(listenFd,80);//绑定端口Listen(listenFd);//开始监听(;;){//接收客户端请求,通过新套接字建立连接connFd=Accept(listenFd);//fork子进程if((pid=Fork())===0){//在子进程中//BIO读取网络请求数据,阻塞,发生进程调度request=connFd.read();//BIO读取本地文件,阻塞,进程调度发生content=ReadFile('test.txt');//将文件内容写入响应Response.write(content);}}多线程应用其实和多进程类似,只是请求Allocatingaprocess被请求allocateathread代替了。线程比进程轻量级,占用系统资源少,有上下文切换(ps:所谓上下文切换,稍微解释一下:在单核CPU的情况下,一个线程中只能执行一个进程或任务同时,对于宏并行,需要根据时间片在多个进程或线程之间来回切换,保证每个进程和线程都有机会被执行),开销也更小;同时,线程之间更容易共享内存,方便上面提到的开发。Web应用有两个核心点,一个是线程模型,一个是I/O模型。那么阻塞I/O到底是什么?还有哪些其他I/O模型?别着急,先来看看什么是阻塞,什么是阻塞?什么是阻塞I/O?简而言之,阻塞就是在函数调用返回之前,当前进度(线程)线程会被挂起,进入等待状态。在这种状态下,当前的进度(线程)线程会被挂起,导致CPU进度(线程)线程进行调度。只有在所有内部工作执行完毕后,函数才会返回给调用者。所以阻塞I/O是指应用程序通过API调用I/O操作后,当前(线程)线程会进入等待状态,代码无法继续执行,此时CPU可以进行线程调度,即切换到其他可执行线程(thread)继续执行,当前线程(thread)会在底层I/O请求处理完后返回并继续执行multi-in(thread)线程+阻塞I/有什么问题模特儿?了解了什么是阻塞和阻塞I/O之后,我们来分析一下传统Web应用多线程(thread)+阻塞I/O模型的弊端。因为一个请求需要分配一个线程(thread),这样的系统在并发量大的时候需要维护大量的线程(thread),需要大量的上下文切换,需要大量的CPU,内存等系统资源来支撑,因此,当高并发请求进来时,CPU和内存开销会急剧上升,可能会迅速拖累整个系统,导致服务不可用。Nodejs应用实现接下来我们看看nodejs应用是如何实现的。事件驱动,单线程(主线程)非阻塞I/O在官网可以看到,nodejs的两大特点,一个是单线程事件驱动,一个是“非阻塞”"输入输出模型。单线程+事件驱动更容易理解。前端同学应该对js的单线程和事件循环机制很熟悉了,那我们主要研究一下这个“非阻塞I/O”到底是个什么东西。先来看一段nodejs服务端应用的常用代码,constnet=require('net');constserver=net.createServer();constfs=require('fs');服务器.listen(80);//监听端口//监听建立连接的事件server.on('connection',(socket)=>{//监听读取请求数据的事件socket.on('data',(data)=>{//异步读取本地文件fs.readFile('test.txt',(err,data)=>{//将读取的内容写入响应socket.write(data);socket.end();})});});可以看出,在nodejs中,我们可以通过异步的方式进行I/O操作。通过API调用I/O操作后,会立即返回,然后继续执行其他代码逻辑,那么为什么nodejs中的I/O是“非阻塞”的呢?在回答这个问题之前,我们先做一些准备工作。参考nodejs进阶视频讲解:进入学习读操作的基本步骤。首先看下一次读操作需要经过哪些步骤。用户程序调用I/O操作API,内部发出系统调用。用户态转入内核态系统发送I/O请求,等待数据就绪(如网络I/O,等待数据从网络到达socket;等待系统从磁盘读取数据等)数据准备好后,复制到内核缓冲区,从内核空间复制到用户空间,用户程序拿到数据。接下来,让我们看看操作系统中的I/O模型。多种I/O模型。BlockingI/O,non-blockingI/O,I/Omulti-channelMultiplexing(进程可以同时监听多个I/O设备就绪)signal-drivenI/OasynchronousI/O那么使用哪种I/O模型在节点?是上图中的“非阻塞I/O”吗?别着急,我们先往下看,了解下nodejs架构nodejs架构、线程、I/O模型分析最上层是我们编写nodejs应用代码时可以使用的API库,下层是一个中间层,用来连接nodejs和它所依赖的底层库,比如让js代码调用底层c代码库。来到底层,可以看到前端同学熟悉的V8,还有一些其他的底层依赖。请注意,有一个名为libuv的库,它是做什么的?从图中也可以看出,libuv帮助nodejs实现了底层线程池、异步I/O等功能。libuv其实是一个跨平台的C语言库,在windows、linux等不同平台下会调用不同的实现。这里我主要分析一下linux下libuv的实现,因为我们大部分的应用还是运行在linux环境下,平台的差异不会影响我们对nodejs原理的分析和理解。那么对于linux下的nodejs的I/O模型,libuv在不同的场景下其实提供了两种不同的实现,其中网络I/O的处理主要是通过epoll函数来实现的(其实就是I/O多路复用,在上图,select函数用于实现I/O多路复用,epoll可以理解为select函数的升级版,暂不详细分析),同时处理文件I/O通过多线程(线程池)+BlockingI/O来模拟异步I/O的实现下面是我写的一段nodejs底层实现的伪代码,帮助大家理解listenFd=newSocket();//创建监听socketBind(listenFd,80);//绑定端口Listen(listenFd);//开始监听(;;){//阻塞在epoll函数上,等待网络数据准备好//epoll可以在多个客户端连接上同时监听listenFd和数据是否准备好//clients代表当前所有的客户端连接,curFd表示epoll函数curFd=Epoll(listenFd,clients)最终得到的一个就绪连接;if(curFd===listenFd){//监听套接字接收到新的客户端连接,创建套接字intconnFd=Accept(listenFd);//将新创建的连接添加到epoll监听列表中clients.push(connFd);}else{//客户端连接数据准备好,读取请求数据request=curFd.read();//这里获取到请求数据后,可以发送数据事件进入nodejs事件循环...}}//libuv读取本地文件时,使用多线程(线程池)+BIO模拟异步I/OThreadPool.run((callback)=>{//在线程中使用BIO读取文件Stringcontent=Read('text.txt');//发送事件调用nodejs提供的回调});通过I/O复用+异步I/O进行多线程模拟配合事件循环机制,nodejs实现了单线程处理并发请求,不阻塞。那么回到前面提到的“非阻塞I/O”模型,其实nodejs并没有直接使用通常定义的非阻塞I/O模型,而是I/O多路复用模型+多线程BIO。我觉得“非阻塞I/O”其实更多的是对nodejs程序员的描述。从编码方式和代码执行顺序来看,nodejs的I/O调用确实是“非阻塞”的。应该明白,nodejs的I/O模型其实主要由多线程下的I/O多路复用和阻塞I/O组成,而处理高并发请求的主要作用就是I/O多路复用。好了,最后总结一下nodejs的线程模型和I/O模型相对于传统的web应用多线程(thread)+阻塞I/O模型的优缺点。Nodejs采用单线程模型,省去了系统的维护和切换。传入(thread)线程开销,多路复用的I/O模型可以防止nodejs的单线程阻塞在某个连接上。在高并发场景下,nodejs应用只需要创建和管理多个client连接对应的socket描述符,无需创建相应的进程或线程,大大降低了系统开销,因此可以同时处理更多的client连接到nodejs提高底层真实I/O操作的效率。如果底层I/O成为系统的性能瓶颈,nodejs仍然无法解决,即nodejs可以接收高并发请求,但如果需要处理大量慢速I/O操作(如读和读)写磁盘),它仍然可能导致系统资源过载。所以高并发不能简单的通过单线程+非阻塞I/O模型来解决CPU密集型应用,这可能会使nodejs的单线程模型成为写内存的性能瓶颈)