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

g_b*_*g_b 345 node.js

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

我无法理解它是如何比多线程Web服务器更快的.据我所知,多线程Web服务器的资源(内存,CPU)会更昂贵,但它不会更快吗?我可能错了; 请解释这个单线程如何在大量请求中更快,以及在处理大量请求(例如10,000)时它通常会做什么(在高级别).

而且,那个单线程是否能够很好地扩展?请记住,我刚刚开始学习Node.js.

sle*_*man 681

如果您不得不提出这个问题,那么您可能不熟悉大多数Web应用程序/服务的功能.您可能认为所有软件都这样做:

user do an action
       ?
       v
 application start processing action
   ???> loop ...
          ???> busy processing
 end loop
   ???> send result to user
Run Code Online (Sandbox Code Playgroud)

但是,这不是Web应用程序,或者实际上任何具有数据库作为后端的应用程序的工作原理.网络应用执行此操作:

user do an action
       ?
       v
 application start processing action
   ???> make database request
          ???> do nothing until request completes
 request complete
   ???> send result to user
Run Code Online (Sandbox Code Playgroud)

在这种情况下,软件花费大部分运行时间使用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
Run Code Online (Sandbox Code Playgroud)

因此,线程花费大部分时间使用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
Run Code Online (Sandbox Code Playgroud)

实际上,这两种方法都返回大致相同延迟的数据,因为它是主导处理的数据库响应时间.

这里的主要优点是我们不需要生成一个新线程,所以我们不需要做很多很多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服务器),而不是从单个进程启动多个线程.然后使用负载均衡器在进程之间分配工作负载.

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

  • 到目前为止,这是我读过的节点的最佳解释."单线程应用程序实际上正在利用另一个进程的多线程行为:数据库."完成了工作 (95认同)
  • @GaneshKarewad算法使用CPU,服务(数据库,REST API等)使用I/O. 如果AI是用js编写的算法,那么你应该在另一个线程或进程中运行它.如果AI是在另一台计算机上运行的服务(如Amazon或Google或IBM AI服务),则使用单线程架构. (5认同)
  • @CaspainCaldion这取决于您所指的是非常快的客户群。照原样,node.js每秒可以处理多达1000个请求,并且速度仅限于网卡的速度。请注意,不是客户端同时连接,而是每秒1000个请求。它可以处理10000个并发客户端,而不会出现问题。真正的瓶颈是网卡。 (3认同)
  • @slebetman,有史以来最好的解释。不过有一件事,如果我有一个机器学习算法来处理一些信息并相应地提供结果,我应该使用多线程方法还是单线程方法 (2认同)
  • @Roshan Node(或者实际上是 Google Chrome 或 Safari 或 Edge)不需要按照收到的顺序完成请求。请记住,异步函数会立即返回而不处理任何内容,因此节点可以处理其他请求。外部服务的响应也是如此。如果 rq3 在 rq2 之前返回值,则节点将简单地调用 rq3 异步函数的回调,然后该函数可以立即将响应发送回客户端。请注意,您不使用“return”关键字将数据返回给客户端。相反,您在回调中调用发送响应函数 (2认同)
  • @VikasDubey 请注意,视频流根本不由节点服务器处理。99% 的处理发生在浏览器/媒体播放器中。在这种情况下,node.js 只不过是一个文件服务器,这意味着它利用操作系统的并行磁盘/网络 I/O 功能。对于网络 I/O,大多数操作系统都具有同样的能力,但对于磁盘 I/O,如果与 Btrfs 或 ext4 等快速文件系统一起使用,Linux 往往会胜过其他操作系统(当然,RAID 使几乎所有东西都变得更快) (2认同)

chr*_*lly 37

您似乎在想的是,大多数处理都是在节点事件循环中处理的.节点实际上将I/O工作转移到线程上.I/O操作通常比CPU操作长几个数量级,那么为什么CPU会等待呢?此外,操作系统已经可以很好地处理I/O任务.实际上,因为Node不会等待它,所以实现了更高的CPU利用率.

通过类比,将NodeJS视为服务员接受客户订单,而I/O厨师则在厨房准备它们.其他系统有多位厨师,他们接受客户订单,准备餐点,清理餐桌,然后只接待下一位顾客.

  • 感谢餐厅的比喻!我发现类比和现实世界的例子更容易学习. (5认同)
  • @AliSherafat 有点晚了,但如果你问服务员是否可以过度劳累,答案是肯定的。一般来说,I/O 操作比内存 CPU 管理的操作慢数百倍。即真正的线程(厨师)正在为服务员接受的每个请求做更多的工作。当然,在现实生活中,10000 个客户对于服务员来说已经太多了,但管理转发和返回 10000 个请求可能还不够。这就是节点与事件循环一起工作的现成方式,但有一些方法可以使用更多等待线程(例如工作线程)。 (2认同)

sud*_*nna 30

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

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

  • Node JS Web Server 内部维护了一个有限线程池来为客户端请求提供服务。

  • Node JS Web Server 接收这些请求并将它们放入队列中。它被称为“事件队列”。

  • Node JS Web Server 内部有一个组件,称为“事件循环”。之所以有这个名字,是因为它使用无限循环来接收请求并处理它们。

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

  • 事件循环检查任何放置在事件队列中的客户端请求。如果没有,则无限期地等待传入请求。

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

    1. 启动客户端请求的进程
    2. 如果该客户端请求不需要任何阻塞 IO 操作,则处理所有内容,准备响应并将其发送回客户端。
    3. 如果该客户端请求需要一些阻塞 IO 操作,例如与数据库、文件系统、外部服务交互,那么它将遵循不同的方法
  • 从内部线程池检查线程可用性
  • 选取一个线程并将此客户端请求分配给该线程。
  • 该线程负责接收该请求、处理它、执行阻塞 IO 操作、准备响应并将其发送回事件循环

    @Rambabu Posa 很好地解释了更多解释,请抛出此链接

  • AFAIK,节点中没有阻塞 I/O(除非您使用同步 API),线程池仅暂时用于处理 I/O 响应并将其传递到主线程。但是,在等待 I/O 请求时[没有线程](https://blog.stephencleary.com/2013/11/there-is-no-thread.html),否则线程池很快就会堵塞。 (3认同)
  • 该博客文章中给出的图表似乎是错误的,他们在那篇文章中提到的内容并不完全正确。 (2认同)

Anu*_*hra 18

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

而非阻塞单线程系统则充分利用其单线程系统。

见下图: 在此输入图像描述 在厨房门口等待或在顾客选择食物时等待,会“阻塞”服务员的全部能力。从计算系统的角度来看,它可能正在等待 IO、数据库响应或任何阻塞整个线程的东西,即使该线程在等待时能够执行其他工作。

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

在此输入图像描述 在非阻塞系统中,服务员只接受订单并上菜,不会在任何地方等待。他分享了自己的手机号码,以便在订单完成后给他们回电。同样,他与厨房分享了他的电话号码,以便在订单准备好时回电。

这就是事件循环在 NodeJS 中的工作原理,并且比阻塞多线程系统执行得更好。


she*_*ond 13

我知道Node.js使用单线程和事件循环来处理请求,一次只处理一个(非阻塞).

我可能会误解你在这里所说的内容,但"一次一个"听起来好像你可能无法完全理解基于事件的架构.

在"传统的"(非事件驱动的)应用程序架构中,该过程花费大量时间等待某些事情发生.在基于事件的体系结构(如Node.js)中,进程不仅仅等待,还可以继续进行其他工作.

例如:您从客户端获得连接,您接受它,您读取请求标头(在http的情况下),然后您开始对请求进行操作.您可能会阅读请求体,通常最终会将一些数据发送回客户端(这是故意简化程序,只是为了证明这一点).

在每个阶段,大部分时间都花在等待一些数据从另一端到达 - 在主JS线程中处理的实际时间通常相当少.

当I/O对象(例如网络连接)的状态发生变化以使其需要处理(例如,在套接字上接收数据,套接字变为可写等)时,主Node.js JS线程被唤醒并带有列表需要处理的项目.

它找到相关的数据结构,并在该结构上发出一些事件,导致回调被运行,处理传入的数据,或者将更多的数据写入套接字等.一旦需要处理的所有I/O对象都被执行了处理后,主Node.js JS线程将再次等待,直到它被告知有更多数据可用(或其他一些操作已完成或超时).

下一次被唤醒时,很可能是由于需要处理不同的I/O对象 - 例如不同的网络连接.每次都会运行相关的回调,然后它会回到睡眠状态,等待其他事情发生.

重要的是,不同请求的处理是交错的,它不会从头到尾处理一个请求,然后移动到下一个请求.

在我看来,这样做的主要优点是缓慢的请求(例如,你试图通过2G数据连接向移动电话设备发送1MB的响应数据,或者你正在进行非常慢的数据库查询)阻止更快的.

在传统的多线程Web服务器中,您通常会为每个正在处理的请求创建一个线程,它将仅处理该请求,直到完成为止.如果您有很多缓慢的请求会发生什么?最终你的很多线程都在处理这些请求,而其他请求(可能非常简单的请求可以很快处理)会排在他们后面.

除了Node.js之外,还有很多其他基于事件的系统,与传统模型相比,它们往往具有相似的优点和缺点.

我不认为基于事件的系统在任何情况下或每个工作负载都更快 - 它们往往适用于I/O绑定的工作负载,对于受CPU限制的工作负载则不太好.


rra*_*anj 10

添加到slebetman的答案以更清楚地了解执行代码时发生的情况。

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

现在又来了一个信息,它不是一个单一的回调队列,有很多队列。

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

每当请求到来时,代码就会按照排队的回调顺序执行。

它不像当有一个阻塞请求时它附加到一个新线程。默认只有 4 个线程。所以那里发生了另一个排队。

每当在代码中发生文件读取等阻塞过程时,就会调用一个函数,该函数利用线程池中的线程,然后一旦操作完成,回调将传递到相应的队列,然后按顺序执行。

一切都根据回调的类型排队,并按上述顺序处理。


Ama*_*pta 9

添加到slebetman答案:当你说Node.JS可以处理10,000个并发请求时,它们本质上是非阻塞请求,即这些请求主要与数据库查询有关.

在内部,event loopNode.JS受理的thread pool,其中每个线程处理一个non-blocking request和事件循环继续委托工作的线程之一后,听取更多的要求thread pool.当其中一个线程完成工作时,它会向event loop已完成的信号发送信号callback.Event loop然后处理此回调并发回响应.

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


asi*_*haj 8

这是这篇媒体文章中的一个很好的解释:

给定一个 NodeJS 应用程序,由于 Node 是单线程的,假设处理涉及一个需要 8 秒的 Promise.all,这是否意味着该请求之后的客户端请求需要等待 8 秒?不会。NodeJS 事件循环是单线程的。NodeJS 的整个服务器架构不是单线程的。

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

线程需要等待阻塞 IO 操作这一事实导致其效率低下。使用这种模型,网络服务器只能处理与线程池中的线程一样多的请求。

NodeJS Web Server 维护一个有限的线程池来为客户端请求提供服务。多个客户端向 NodeJS 服务器发出多个请求。NodeJS 接收这些请求并将它们放入 EventQueue 中。NodeJS 服务器有一个称为 EventLoop 的内部组件,它是一个无限循环,用于接收请求并处理它们。这个EventLoop是单线程的。换句话说,Eve​​ntLoop是EventQueue的监听者。因此,我们有一个事件队列,其中放置了请求,并且有一个事件循环侦听事件队列中的这些请求。接下来发生什么?监听器(事件循环)处理请求,如果它能够处理请求而不需要任何阻塞 IO 操作,那么事件循环将自行处理请求并将响应发送回客户端。如果当前请求使用阻塞 IO 操作,事件循环会查看线程池中是否有可用线程,从线程池中选取一个线程,并将特定请求分配给选取的线程。该线程执行阻塞 IO 操作并将响应发送回事件循环,一旦响应到达事件循环,事件循环就会将响应发送回客户端。

NodeJS 与传统的多线程请求响应模型相比有何优势?在传统的多线程请求/响应模型中,每个客户端都会获得一个不同的线程,而与 NodeJS 一样,更简单的请求都直接由 EventLoop 处理。这是线程池资源的优化,并且没有为每个客户端请求创建线程的开销。