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

Posted

技术标签:

【中文标题】一般来说,Node.js 如何处理 10,000 个并发请求?【英文标题】:How, in general, does Node.js handle 10,000 concurrent requests? 【发布时间】:2016-04-23 15:51:43 【问题描述】:

我了解 Node.js 使用单线程和事件循环来处理一次只处理一个请求(这是非阻塞的)。但是,它是如何工作的,比如说 10,000 个并发请求。事件循环会处理所有请求吗?会不会太费时间?

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

此外,单线程能否在如此大的数量下很好地扩展?请记住,我刚刚开始学习 Node.js。

【问题讨论】:

因为大部分工作(移动数据)不涉及 CPU。 还要注意,仅仅因为只有一个线程在执行 javascript,并不意味着没有很多其他线程在工作。 这个问题要么太宽泛,要么与其他各种问题重复。 ***.com/questions/3629784/… 除了单线程之外,Node.js 还做了一些称为“非阻塞 I/O”的事情。所有的魔法都在这里完成 【参考方案1】:

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

虽然非阻塞单线程系统充分利用其单线程系统。

见下图:

【讨论】:

【参考方案2】:

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

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

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

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

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

当有阻塞请求时,它不会被附加到一个新线程。默认情况下只有 4 个线程。所以那里又发生了一次排队。

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

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

【讨论】:

nodeJs中的内部线程池默认只有4个线程……能详细点吗? node 是如何使用 4 个线程的? NodeJs 内部使用几个线程(默认为 4 个)用于连接数据库、从磁盘读取文件、与操作系统交互等。您可以通过使用环境变量来增加此线程数 ' UV_THREADPOOL_SIZE' 通过在运行你的 nodejs 应用程序时将其设置为不同的数字。【参考方案3】:

这是medium article的一个很好的解释:

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

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

【讨论】:

【参考方案4】:

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

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

Node JS Web 服务器内部维护了一个 Limited Thread 池以 为客户请求提供服务。

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

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

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

事件循环检查任何客户端请求是否放置在事件队列中。如果 然后不要无限期地等待传入的请求。

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

    启动客户端请求进程 如果该客户端请求不需要任何阻塞 IO 操作,然后处理所有内容,准备响应并发送 返回给客户。 如果该客户端请求需要一些阻塞 IO 操作,例如 与数据库、文件系统、外部服务交互然后它 将采用不同的方法
检查内部线程池中的线程可用性 选择一个线程并将此客户端请求分配给该线程。

那个线程负责接受那个请求,处理它, 执行阻塞 IO 操作,准备响应并发送回 到事件循环

@Rambabu Posa 很好地解释了更多解释去扔这个Link

【讨论】:

那篇博文中给出的图表似乎是错误的,他们在那篇文章中提到的并不完全正确。 AFAIK,节点中没有阻塞 I/O(除非您使用同步 API),线程池仅用于暂时处理 I/O 响应并将它们传递到主线程。但是,在等待 I/O 请求时[没有线程](blog.stephencleary.com/2013/11/there-is-no-thread.html),否则线程池会很快被堵塞。 我消除了困惑***.com/a/70161215/4034825【参考方案5】:

如果您必须问这个问题,那么您可能不熟悉大多数 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 编写一个更快的服务器非常容易。

混合方法

一些网络服务器使用混合方法。例如,nginx 和 Apache2 将其网络处理代码实现为事件循环的线程池。每个线程同时运行一个事件循环,单线程处理请求,但请求在多个线程之间进行负载平衡。

一些单线程架构也使用混合方法。您可以启动多个应用程序,而不是从单个进程启动多个线程 - 例如,四核机器上的 4 个 node.js 服务器。然后,您使用负载平衡器在进程之间分配工作负载。

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

【讨论】:

这是迄今为止我读过的对节点最好的解释。那个“单线程应用程序实际上是在利用另一个进程的多线程行为:数据库。”完成了这项工作 @CaspainCaldion 这取决于您所说的非常快和很多客户的意思。照原样,node.js 每秒可以处理超过 1000 个请求,并且速度仅限于网卡的速度。请注意,每秒 1000 个请求不是同时连接的客户端。它可以毫无问题地同时处理 10000 个客户端。真正的瓶颈是网卡。 @slebetman ,最好的解释。不过有一件事,如果我有一个机器学习算法可以处理一些信息并相应地提供结果,我应该使用多线程方法还是单线程 @GaneshKarewad 算法使用 CPU,服务(数据库、REST API 等)使用 I/O。如果 AI 是用 js 编写的算法,那么您应该在另一个线程或进程中运行它。如果 AI 是在另一台计算机上运行的服务(如 Amazon、Google 或 IBM AI 服务),则使用单线程架构。 @VikasDubey 请注意,节点服务器根本不处理视频流。 99% 的处理发生在浏览器/媒体播放器中。在这种情况下,node.js 只不过是一个文件服务器,这意味着它利用了操作系统的并行磁盘/网络 I/O 功能。对于网络 I/O,大多数操作系统都具有同样的能力,但对于磁盘 I/O,如果与 Btrfs 或 ext4 等快速文件系统一起使用,Linux 往往胜过其他所有操作系统(当然,RAID 使几乎一切都变得更快)【参考方案6】:

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

打个比方,把 NodeJS 想象成一个服务员,当 I/O 厨师在厨房里准备顾客的订单时,他们会接受客户的订单。其他系统有多个厨师,他们接受顾客的订单、准备餐点、收拾桌子,然后才为下一位顾客服务。

【讨论】:

感谢餐厅的比喻!我发现类比和现实世界的例子更容易学习。 非常清晰。好比喻! 节点的js部分是单线程的,而底层的c++部分使用的是线程池。 ***.com/a/70161215/4034825 在餐厅示例中:如果我们有 10000 名顾客和一名服务员怎么办?一个服务员(一个线程)处理这些客户是不是很慢?【参考方案7】:

添加到 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 时,它们对我很有帮助。

【讨论】:

【参考方案8】:

我了解 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 密集型工作负载。

【讨论】:

很好的解释,用于理解事件循环同时适用于多个请求。

以上是关于一般来说,Node.js 如何处理 10,000 个并发请求?的主要内容,如果未能解决你的问题,请参考以下文章

运行 Mongoose 时执行 ajax 调用时如何处理 Node.js 发现

Node.JS、Express 和 Heroku - 如何处理 HTTP 和 HTTPS?

Node.js如何处理健壮性

如何处理 json 并写入 node.js 中的文件

Node.js:如何处理在字典中查找同名的不同城市?

异步文件 IO 方法如何处理 Node.js 中的本地文件?