我知道Node.js使用单线程和事件循环来处理请求,每次只处理一个(这是非阻塞的)。但是,它是如何工作的,假设有10,000个并发请求。事件循环将处理所有的请求?那样不会太花时间吗?

我不明白(还)它怎么能比多线程web服务器快。我知道多线程web服务器在资源(内存,CPU)上更昂贵,但它不是更快吗?我可能错了;请解释这个单线程是如何在大量请求中更快的,以及它在处理大量请求(如10,000)时通常会做什么(在高层次上)。

还有,单线程能适应这么大的数量吗?请记住,我刚刚开始学习Node.js。


当前回答

您可能认为大部分处理都是在节点事件循环中处理的。节点实际上将I/O工作分配给线程。I/O操作通常比CPU操作要长几个数量级,那么为什么CPU要等待呢?此外,操作系统已经可以很好地处理I/O任务。事实上,由于Node不等待,它实现了更高的CPU利用率。

通过类比的方式,可以将NodeJS想象成一个服务员,在I/O厨师在厨房里准备订单的同时接受客户的订单。其他系统有多名厨师,他们为顾客点单、准备饭菜、清理桌子,然后才为下一位顾客服务。

其他回答

您可能认为大部分处理都是在节点事件循环中处理的。节点实际上将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中的cluster模块正是这样做的。

实际上,这两种方法在技术上是彼此相同的镜像。

添加slebetman的答案,以便更清楚地了解在执行代码时发生了什么。

The internal thread pool in nodeJs just has 4 threads by default. and its not like the whole request is attached to a new thread from the thread pool the whole execution of request happens just like any normal request (without any blocking task) , just that whenever a request has any long running or a heavy operation like db call ,a file operation or a http request the task is queued to the internal thread pool which is provided by libuv. And as nodeJs provides 4 threads in internal thread pool by default every 5th or next concurrent request waits until a thread is free and once these operations are over the callback is pushed to the callback queue. and is picked up by event loop and sends back the response.

现在这里有另一个信息,它不是单一的回调队列,有很多队列。

NextTick队列 微任务队列 定时器队列 IO回调队列(请求,文件操作,数据库操作) IO轮询队列 检查Phase queue或setimmediation 关闭处理程序队列

每当有请求时,代码就按照这个回调队列的顺序执行。

它不像当有一个阻塞请求时,它会附加到一个新线程。默认情况下只有4个线程。这是另一个排队过程。

在代码中,无论何时发生像文件读取这样的阻塞进程,然后调用一个从线程池中利用线程的函数,一旦操作完成,回调被传递到各自的队列,然后按顺序执行。

所有内容都基于回调的类型进行排队,并按照上面提到的顺序进行处理。

我知道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约束的工作负载上就不那么好了。

单线程事件循环模型处理步骤:

Clients Send request to Web Server. Node JS Web Server internally maintains a Limited Thread pool to provide services to the Client Requests. Node JS Web Server receives those requests and places them into a Queue. It is known as “Event Queue”. Node JS Web Server internally has a Component, known as “Event Loop”. Why it got this name is that it uses indefinite loop to receive requests and process them. Event Loop uses Single Thread only. It is main heart of Node JS Platform Processing Model. Event Loop checks any Client Request is placed in Event Queue. If not then wait for incoming requests for indefinitely. If yes, then pick up one Client Request from Event Queue Starts process that Client Request If that Client Request Does Not requires any Blocking IO Operations, then process everything, prepare response and send it back to client. If that Client Request requires some Blocking IO Operations like interacting with Database, File System, External Services then it will follow different approach Checks Threads availability from Internal Thread Pool Picks up one Thread and assign this Client Request to that thread. That Thread is responsible for taking that request, process it, perform Blocking IO operations, prepare response and send it back to the Event Loop very nicely explained by @Rambabu Posa for more explanation go throw this Link