一般来说,Node.js如何处理10,000个并发请求?

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

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

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

201928 次浏览

您可能认为大部分处理都是在节点事件循环中处理的。节点实际上将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.JSevent loop正在处理thread pool,其中每个线程处理non-blocking request,事件循环在将工作委派给thread pool的一个线程后继续侦听更多的请求。当其中一个线程完成工作时,它发送一个信号给event loop,它已经完成了,也就是callbackEvent loop然后处理这个回调并发送回响应。

作为NodeJS的新手,请阅读更多关于nextTick的内容,以了解事件循环内部是如何工作的。 阅读http://javascriptissexy.com上的博客,当我开始使用JavaScript/NodeJS时,它们对我真的很有帮助

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

  • 客户端向Web服务器发送请求。

  • Node JS Web Server内部维护一个有限线程池

  • .为客户端请求提供服务
  • Node JS Web Server接收这些请求并将它们放入一个 队列中。它被称为“事件队列”。

  • Node JS Web Server内部有一个组件,称为“Event Loop”。 为什么取这个名字是因为它使用不定循环接收

    .

    .
  • 事件循环只用单线程。它是Node JS的核心 平台处理模型。

  • 事件循环检查置于事件队列中的任何客户端请求。如果

  • .
  • 如果是,则从事件队列中提取一个客户端请求

    1. 启动客户端请求的处理
    2. 如果客户端请求不需要任何阻塞IO 操作,然后处理一切,准备响应并发送
    3. .返回客户端
    4. 如果客户端请求需要一些阻塞IO操作 与数据库,文件系统,外部服务进行交互 将遵循不同的方法
    5. 李< / ol > < / >
    6. 检查内部线程池中的线程可用性
    7. 选择一个线程并将此客户端请求分配给该线程。
    8. 该线程负责接收请求,处理它, 执行阻塞IO操作,准备响应并发回 到事件循环

      @Rambabu Posa很好地解释了更多的解释,去扔这个链接

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

nodeJs的内部线程池默认只有4个线程。它不像整个请求被附加到线程池中的一个新线程,整个请求的执行就像任何正常的请求一样(没有任何阻塞任务),只是当一个请求有任何长时间运行或繁重的操作,如db调用,文件操作或HTTP请求时,任务被排队到libuv提供的内部线程池中。由于nodeJs默认在内部线程池中提供了4个线程,每5个或下一个并发请求等待一次,直到一个线程空闲,一旦这些操作结束,回调就会被推到回调队列中。并被事件循环拾取并发送回响应。

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

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

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

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

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

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

下面是来自中文章的一个很好的解释:

给定一个NodeJS应用程序,因为Node是单线程的,假设处理涉及Promise。所有这些都需要8秒,这是否意味着在这个请求之后的客户端请求将需要等待8秒? 不。NodeJS事件循环是单线程的。NodeJS的整个服务器架构不是单线程的

在进入Node服务器架构之前,看看典型的多线程请求响应模型,web服务器将有多个线程,当并发请求到达web服务器时,web服务器从threadPool中选择threadOne, threadOne处理requestOne并响应clientOne,当第二个请求进入时,web服务器从threadPool中选择第二个线程,并选择requestTwo并处理它并响应clientTwo。threadOne负责requestOne要求的所有类型的操作,包括执行任何阻塞IO操作。

线程需要等待阻塞IO操作的事实使其效率低下。使用这种模型,web服务器只能处理与线程池中线程数量相同的请求。

NodeJS Web Server维护一个有限的线程池,为客户端请求提供服务。多个客户端向NodeJS服务器发出多个请求。NodeJS接收这些请求并将它们放入EventQueue中。 NodeJS服务器有一个内部组件,称为EventLoop,它是一个无限循环,用于接收和处理请求。这个EventLoop是单线程的。换句话说,EventLoop是EventQueue的监听器。 我们有一个放置请求的事件队列,我们有一个事件循环在事件队列中监听这些请求。接下来会发生什么? 监听器(事件循环)处理请求,如果它能够在不需要任何阻塞IO操作的情况下处理请求,那么事件循环将自己处理请求并将响应发送回客户端。 如果当前请求使用阻塞IO操作,事件循环查看线程池中是否有可用线程,从线程池中选择一个线程,并将特定请求分配给所选的线程。该线程执行阻塞IO操作,并将响应发送回事件循环,一旦响应到达事件循环,事件循环将响应发送回客户端 NodeJS如何比传统的多线程请求响应模型更好? 在传统的多线程请求/响应模型中,每个客户端都得到一个不同的线程,而在NodeJS中,更简单的请求都直接由EventLoop处理。这是线程池资源的优化,并且没有为每个客户端请求创建线程的开销

多线程阻塞系统的阻止一部分使其效率降低。被阻塞的线程在等待响应期间不能用于其他任何事情。

而非阻塞单线程系统则充分利用了它的单线程系统。

见下图: enter image description here 在这里,在厨房门口等待或在顾客挑选食物时等待,是“阻塞”。服务员的满员量。在计算系统的意义上,它可以等待IO,或DB响应或任何阻塞整个线程的东西,即使线程在等待时能够进行其他工作

让我们看看非阻塞是如何工作的:

enter image description here 在非阻塞系统中,服务员只接单和上菜,不在任何地方等待。他分享了他的手机号码,当他们完成订单时,他会给出一个回电话。类似地,他将自己的电话号码分享给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文件,有这样的图像:

enter image description here

从文档阶段概述

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开始之间,我们损失了一些时间。大量的线程会导致重载系统在线程调度上花费宝贵的周期 以及上下文切换,这会增加延迟,并对可伸缩性和吞吐量施加限制