我知道Node.js使用单线程和事件循环来处理请求,每次只处理一个(这是非阻塞的)。但是,它是如何工作的,假设有10,000个并发请求。事件循环将处理所有的请求?那样不会太花时间吗?
我不明白(还)它怎么能比多线程web服务器快。我知道多线程web服务器在资源(内存,CPU)上更昂贵,但它不是更快吗?我可能错了;请解释这个单线程是如何在大量请求中更快的,以及它在处理大量请求(如10,000)时通常会做什么(在高层次上)。
还有,单线程能适应这么大的数量吗?请记住,我刚刚开始学习Node.js。
您可能认为大部分处理都是在节点事件循环中处理的。节点实际上将I/O工作分配给线程。I/O操作通常比CPU操作要长几个数量级,那么为什么CPU要等待呢?此外,操作系统已经可以很好地处理I/O任务。事实上,由于Node不等待,它实现了更高的CPU利用率。
通过类比的方式,可以将NodeJS想象成一个服务员,在I/O厨师在厨房里准备订单的同时接受客户的订单。其他系统有多名厨师,他们为顾客点单、准备饭菜、清理桌子,然后才为下一位顾客服务。
如果你不得不问这个问题,那么你可能不熟悉大多数web应用程序/服务的功能。你可能认为所有的软件都是这样做的:
user do an action │ v application start processing action └──> loop ... └──> busy processing end loop └──> send result to user
然而,这不是web应用程序的工作方式,也不是任何以数据库作为后端的应用程序的工作方式。Web应用会这样做:
user do an action │ v application start processing action └──> make database request └──> do nothing until request completes request complete └──> send result to user
在这种情况下,软件的大部分运行时间使用0%的CPU时间来等待数据库返回。
多线程网络应用程序像这样处理上述工作负载:
request ──> spawn thread └──> wait for database request └──> answer request request ──> spawn thread └──> wait for database request └──> answer request request ──> spawn thread └──> wait for database request └──> answer request
因此,线程大部分时间都在使用0%的CPU等待数据库返回数据。在这样做的时候,他们不得不为每个线程分配所需的内存,其中包括为每个线程分配完全独立的程序堆栈等。此外,他们将不得不启动一个线程,虽然不像启动一个完整的进程那么昂贵,但仍然不便宜。
既然我们大部分时间都在使用0%的CPU,为什么不在不使用CPU的时候运行一些代码呢?这样,每个请求将获得与多线程应用程序相同的CPU时间,但我们不需要启动线程。所以我们这样做:
request ──> make database request request ──> make database request request ──> make database request database request complete ──> send response database request complete ──> send response database request complete ──> send response
实际上,这两种方法返回的数据延迟大致相同,因为数据库响应时间主导着处理过程。
这里的主要优点是我们不需要生成一个新线程,所以我们不需要做很多很多的malloc,这会减慢我们的速度。
看似神秘的事情是,上述两种方法是如何“并行”地运行工作负载的?答案是数据库是线程化的。所以我们的单线程应用实际上是利用了另一个进程的多线程行为:数据库。
如果在返回数据之前需要进行大量的CPU计算,那么单线程应用程序就会失败。现在,我指的不是处理数据库结果的for循环。基本上还是O(n)我的意思是做傅里叶变换(例如mp3编码),光线追踪(3D渲染)等。
单线程应用程序的另一个缺陷是它只使用单个CPU核心。因此,如果你有一个四核服务器(现在并不少见),你就不会使用其他3核。
如果你需要为每个线程分配大量的RAM,那么多线程应用程序就会失败。首先,RAM使用本身意味着你不能像单线程应用程序那样处理那么多请求。更糟糕的是,malloc很慢。分配大量的对象(这在现代web框架中很常见)意味着我们最终可能会比单线程应用程序慢。这是node.js通常获胜的地方。
当您需要在线程中运行另一种脚本语言时,这种用例最终会使多线程变得更糟。首先,通常需要malloc该语言的整个运行时,然后需要malloc脚本使用的变量。
所以如果你用C或go或java编写网络应用程序,那么线程的开销通常不会太糟糕。如果你正在编写一个C web服务器来提供PHP或Ruby,那么用javascript或Ruby或Python编写一个更快的服务器是非常容易的。
一些web服务器使用混合方法。例如,Nginx和Apache2将网络处理代码实现为事件循环的线程池。每个线程运行一个事件循环,同时处理单线程请求,但请求在多个线程之间是负载平衡的。
一些单线程架构也使用混合方法。而不是从一个进程启动多个线程,你可以启动多个应用程序-例如,在四核机器上的4个node.js服务器。然后使用负载均衡器将工作负载分散到各个进程中。node.js中的集群模块正是这样做的。
实际上,这两种方法在技术上是彼此相同的镜像。
我知道Node.js使用一个单线程和一个事件循环来 .处理请求,一次只处理一个(非阻塞)
我可能误解了您在这里所说的内容,但是“一次一个”听起来您可能没有完全理解基于事件的架构。
在“传统的”(非事件驱动的)应用程序体系结构中,流程花费大量时间等待某些事情发生。在基于事件的架构(如Node.js)中,进程不只是等待,它可以继续其他工作。
例如:你从客户端获得一个连接,你接受它,你读取请求头(在http的情况下),然后你开始对请求进行操作。您可能会读取请求体,通常会将一些数据发送回客户端(这是有意简化的过程,只是为了说明这一点)。
在每一个阶段,大部分时间都花在了等待数据从另一端到达——在JS主线程中处理的实际时间通常是相当少的。
当一个I/O对象(例如网络连接)的状态发生变化,需要处理(例如在套接字上接收数据,套接字变得可写等)时,主Node.js JS线程会被唤醒,并显示需要处理的项列表。
它找到相关的数据结构,并在该结构上发出一些事件,导致回调运行,处理传入数据,或将更多数据写入套接字,等等。一旦所有需要处理的I/O对象都被处理完,Node.js主线程将再次等待,直到它被告知有更多数据可用(或者其他一些操作已经完成或超时)。
下一次它被唤醒时,很可能是由于需要处理不同的I/O对象——例如,不同的网络连接。每次都运行相关的回调,然后它回到睡眠状态,等待其他事情发生。
重要的一点是不同请求的处理是交错的,它不会从头到尾处理一个请求,然后再转移到下一个请求。
在我看来,这样做的主要好处是,一个慢速的请求(例如,你试图通过2G数据连接向移动电话设备发送1MB的响应数据,或者你正在进行一个非常慢的数据库查询)不会阻止更快的请求。
在传统的多线程web服务器中,您通常会为每个正在处理的请求设置一个线程,并且它只处理该请求,直到完成。如果有很多慢速请求会发生什么?最终会有大量线程处理这些请求,而其他请求(可能是可以很快处理的非常简单的请求)则排在它们后面。
除了Node.js之外,还有很多其他基于事件的系统,与传统模型相比,它们往往有相似的优点和缺点。
我不会说基于事件的系统在每种情况下或每种工作负载下都更快——它们在I/ o约束的工作负载上工作得很好,但在cpu约束的工作负载上就不那么好了。
添加到slebetman回答: 当你说Node.JS可以处理10,000个并发请求时,它们本质上是非阻塞请求,即这些请求主要与数据库查询有关
Node.JS
在内部,Node.JS的event loop正在处理thread pool,其中每个线程处理non-blocking request,事件循环在将工作委派给thread pool的一个线程后继续侦听更多的请求。当其中一个线程完成工作时,它发送一个信号给event loop,它已经完成了,也就是callback。Event loop然后处理这个回调并发送回响应。
event loop
thread pool
non-blocking request
callback
Event loop
nextTick
单线程事件循环模型处理步骤:
客户端向Web服务器发送请求。
Node JS Web Server内部维护一个有限线程池
Node JS Web Server接收这些请求并将它们放入一个 队列中。它被称为“事件队列”。
事件循环只用单线程。它是Node JS的核心 平台处理模型。
事件循环检查置于事件队列中的任何客户端请求。如果
如果是,则从事件队列中提取一个客户端请求
该线程负责接收请求,处理它, 执行阻塞IO操作,准备响应并发回 到事件循环
@Rambabu Posa很好地解释了更多的解释,去扔这个链接
添加到slebetman的答案,以便更清楚地了解在执行代码时发生了什么。
nodeJs的内部线程池默认只有4个线程。它不像整个请求被附加到线程池中的一个新线程,整个请求的执行就像任何正常的请求一样(没有任何阻塞任务),只是当一个请求有任何长时间运行或繁重的操作,如db调用,文件操作或HTTP请求时,任务被排队到libuv提供的内部线程池中。由于nodeJs默认在内部线程池中提供了4个线程,每5个或下一个并发请求等待一次,直到一个线程空闲,一旦这些操作结束,回调就会被推到回调队列中。并被事件循环拾取并发送回响应。
现在这里有另一个信息,它不是单一的回调队列,有很多队列。
每当有请求时,代码就按照这个回调队列的顺序执行。
它不像当有一个阻塞请求时,它会附加到一个新线程。默认情况下只有4个线程。这是另一个排队过程。
在代码中,无论何时发生像文件读取这样的阻塞进程,然后调用一个从线程池中利用线程的函数,一旦操作完成,回调被传递到各自的队列,然后按顺序执行。
所有内容都基于回调的类型进行排队,并按照上面提到的顺序进行处理。
下面是来自中文章的一个很好的解释:
在进入Node服务器架构之前,看看典型的多线程请求响应模型,web服务器将有多个线程,当并发请求到达web服务器时,web服务器从threadPool中选择threadOne, threadOne处理requestOne并响应clientOne,当第二个请求进入时,web服务器从threadPool中选择第二个线程,并选择requestTwo并处理它并响应clientTwo。threadOne负责requestOne要求的所有类型的操作,包括执行任何阻塞IO操作。
线程需要等待阻塞IO操作的事实使其效率低下。使用这种模型,web服务器只能处理与线程池中线程数量相同的请求。
多线程阻塞系统的阻止一部分使其效率降低。被阻塞的线程在等待响应期间不能用于其他任何事情。
而非阻塞单线程系统则充分利用了它的单线程系统。
见下图: 在这里,在厨房门口等待或在顾客挑选食物时等待,是“阻塞”。服务员的满员量。在计算系统的意义上,它可以等待IO,或DB响应或任何阻塞整个线程的东西,即使线程在等待时能够进行其他工作
让我们看看非阻塞是如何工作的:
在非阻塞系统中,服务员只接单和上菜,不在任何地方等待。他分享了他的手机号码,当他们完成订单时,他会给出一个回电话。类似地,他将自己的电话号码分享给Kitchen,以便在订单准备就绪时回调
这就是Event循环在NodeJS中的工作方式,并且比阻塞多线程系统执行得更好。
在node.js中,请求应该是IO绑定,而不是CPU绑定。这意味着每个请求不应该强迫node.js做大量的计算。如果在解决请求时涉及大量计算,那么node.js不是一个好的选择。IO界需要很少的计算量。请求的大部分时间都花在对DB或服务的调用上。
Node.js有单线程事件循环,但它只是一个厨师。在后台,大部分工作是由操作系统完成的,Libuv确保了与操作系统的通信。Libuv的文档如下:
在事件驱动编程中,应用程序表示感兴趣的 当某些事件发生时,对它们做出反应。的责任 从操作系统收集事件或监视其他 事件源由libuv处理,用户可以注册 .事件发生时调用的回调
传入的请求由操作系统处理。这对于基于请求-响应模型的几乎所有服务器来说都是正确的。入网呼叫在OS非阻塞IO队列中排队。'Event Loop不断轮询OS IO队列,这是它了解传入客户端请求的方式。“Polling"指定期检查某个资源的状态。如果有任何传入请求,evnet循环将接受该请求,它将执行synchronously。在执行时,如果有任何异步调用(即setTimeout),它将被放入回调队列。在事件循环完成执行同步调用后,它可以轮询回调,如果它发现需要执行的回调,它将执行该回调。然后它将轮询任何传入的请求。如果你检查node . js文件,有这样的图像:
'Event Loop
synchronously
从文档阶段概述
poll:检索新的I/O事件;执行I/O相关的回调(几乎 除了关闭回调,由 定时器,和setimmediation ());
事件循环不断地从不同队列轮询。如果一个请求需要外部调用或磁盘访问,这将被传递给操作系统,操作系统也有2个不同的队列。一旦event loop检测到某些事情必须异步完成,它就会将它们放入队列中。一旦它被放入队列中,事件循环将处理到下一个任务。
这里要提到的一件事是,事件循环持续运行。只有Cpu可以将这个线程移出Cpu,事件循环本身不会这样做。
从文档中可以看出:
Node.js可伸缩性的秘密是它使用了一个小的 处理多个客户端的线程数。如果Node.js可以凑合 线程越少,它就会占用更多的系统时间和内存 为客户服务,而不是为客户支付空间和时间的管理费用 线程(内存、上下文切换)。但是因为node。js只有一个 线程很少,你必须构造你的应用程序来明智地使用它们 这里有一个很好的经验法则来保持你的Node.js服务器速度: 当工作与任何给定的每个客户端相关联时,Node.js是快速的 .
注意,小任务意味着IO绑定任务而不是CPU绑定任务。只有当每个请求的工作主要是IO工作时,单个event loop才会处理客户端负载。
Context switch基本上意味着CPU资源耗尽,所以它需要停止一个进程的执行,以允许另一个进程执行。OS首先必须驱逐process1,这样它就会从CPU中取出这个进程,并将这个进程保存在主存中。接下来,OS将通过从内存中加载进程控制块来恢复process2,并将其放在CPU上执行。然后process2将开始执行。在进程1结束和进程2开始之间,我们损失了一些时间。大量的线程会导致重载系统在线程调度上花费宝贵的周期 以及上下文切换,这会增加延迟,并对可伸缩性和吞吐量施加限制
Context switch