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

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

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


当前回答

如果你不得不问这个问题,那么你可能不熟悉大多数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模块正是这样做的。

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

其他回答

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

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

添加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中,请求应该是IO绑定,而不是CPU绑定。这意味着每个请求不应该强迫node.js做大量的计算。如果在解决请求时涉及大量计算,那么node.js不是一个好的选择。IO界需要很少的计算量。请求的大部分时间都花在对DB或服务的调用上。

Node.js有单线程事件循环,但它只是一个厨师。在后台,大部分工作是由操作系统完成的,Libuv确保了与操作系统的通信。Libuv的文档如下:

在事件驱动编程中,应用程序表示感兴趣的 当某些事件发生时,对它们做出反应。的责任 从操作系统收集事件或监视其他 事件源由libuv处理,用户可以注册 事件发生时调用的回调。

The incoming requests are handled by the Operating system. This is pretty much correct for almost all servers based on request-response model. Incoming network calls are queued in OS Non-blocking IO queue.'Event Loop constantly polls OS IO queue that is how it gets to know about the incoming client request. "Polling" means checking the status of some resource at a regular interval. If there are any incoming requests, evnet loop will take that request, it will execute that synchronously. while executing if there is any async call (i.e setTimeout), it will be put into the callback queue. After the event loop finishes executing sync calls, it can poll the callbacks, if it finds a callback that needs to be executed, it will execute that callback. then it will poll for any incoming request. If you check the node.js docs there is this image:

从文档阶段概述

poll:检索新的I/O事件;执行I/O相关的回调(几乎 除了关闭回调,由 定时器,和setimmediation ());节点将在适当的时候阻塞在这里。

事件循环不断地从不同队列轮询。如果一个请求需要外部调用或磁盘访问,这将被传递给操作系统,操作系统也有2个不同的队列。一旦事件循环检测到某些事情必须异步完成,它就会将它们放入队列中。一旦它被放入队列中,事件循环将处理到下一个任务。

这里要提到的一件事是,事件循环持续运行。只有Cpu可以将这个线程移出Cpu,事件循环本身不会这样做。

从文档中可以看出:

The secret to the scalability of Node.js is that it uses a small number of threads to handle many clients. If Node.js can make do with fewer threads, then it can spend more of your system's time and memory working on clients rather than on paying space and time overheads for threads (memory, context-switching). But because Node.js has only a few threads, you must structure your application to use them wisely. Here's a good rule of thumb for keeping your Node.js server speedy: Node.js is fast when the work associated with each client at any given time is "small".

注意,小任务意味着IO绑定任务而不是CPU绑定任务。只有当每个请求的工作主要是IO工作时,单个事件循环才会处理客户机负载。

Context switch basically means CPU is out of resources so It needs to stop the execution of one process to allow another process to execute. OS first has to evict process1 so it will take this process from CPU and it will save this process in the main memory. Next, OS will restore process2 by loading process control block from memory and it will put it on the CPU for execution. Then process2 will start its execution. Between process1 ended and the process2 started, we have lost some time. Large number of threads can cause a heavily loaded system to spend precious cycles on thread scheduling and context switching, which adds latency and imposes limits on scalability and throughput.

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

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

如果你不得不问这个问题,那么你可能不熟悉大多数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模块正是这样做的。

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