How, in general, does Node.js handle 10,000 concurrent requests?
我知道Node.js使用单线程和事件循环来处理请求,一次只处理一个(非阻塞)。 但是,如何工作,让我们说10,000个并发请求。 事件循环将处理所有请求? 这不会花太长时间吗?
我无法理解它是如何比多线程Web服务器更快的。 我知道多线程Web服务器的资源(内存,CPU)会更加昂贵,但它不会更快吗? 我可能错了; 请解释这个单线程如何在大量请求中更快,以及在处理大量请求(例如10,000)时它通常会做什么(在高级别)。
而且,那个单线程是否能够很好地扩展? 请记住,我刚开始学习Node.js.
如果您不得不提出这个问题,那么您可能不熟悉大多数Web应用程序/服务的功能。您可能认为所有软件都这样做:
1 2 3 4 5 6 7 8 | user do an action │ v application start processing action └──> loop ... └──> busy processing end loop └──> send result to user |
但是,这不是Web应用程序,或者实际上任何具有数据库作为后端的应用程序的工作原理。网络应用执行此操作:
1 2 3 4 5 6 7 8 | 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时间等待数据库返回。
多线程网络应用:
多线程网络应用程序处理上述工作负载,如下所示:
1 2 3 4 5 6 7 8 9 | 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时间,但我们不需要启动线程。所以我们这样做:
1 2 3 4 5 6 | 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服务器),而不是从单个进程启动多个线程。然后使用负载均衡器在进程之间分配工作负载。
实际上,这两种方法在技术上是彼此相同的镜像。
您似乎在想的是,大多数处理都是在节点事件循环中处理的。 节点实际上将I / O工作转移到线程上。 I / O操作通常比CPU操作长几个数量级,那么为什么CPU会等待呢? 此外,操作系统已经可以很好地处理I / O任务。 实际上,因为Node不会等待它,所以实现了更高的CPU利用率。
通过类比,将NodeJS视为服务员接受客户订单,而I / O厨师则在厨房准备它们。 其他系统有多位厨师,他们接受客户订单,准备餐点,清理餐桌,然后只接待下一位顾客。
I understand that Node.js uses a single-thread and an event loop to
process requests only processing one at a time (which is non-blocking).
我可能会误解你在这里所说的话,但"一次一个"听起来好像你可能无法完全理解基于事件的架构。
在"传统的"(非事件驱动的)应用程序架构中,该过程花费大量时间等待某些事情发生。在基于事件的体系结构(如Node.js)中,进程不仅仅等待,它还可以继续进行其他工作。
例如:您从客户端获得连接,您接受它,您读取请求标头(在http的情况下),然后您开始对请求进行操作。您可能会阅读请求体,通常最终会将一些数据发送回客户端(这是故意简化程序,只是为了证明这一点)。
在每个阶段,大部分时间都花在等待一些数据从另一端到达 - 在主JS线程中处理的实际时间通常相当少。
当I / O对象(例如网络连接)的状态发生变化以使其需要处理(例如,在套接字上接收数据,套接字变为可写等)时,主Node.js JS线程被唤醒并带有列表需要处理的项目。
它找到相关的数据结构,并在该结构上发出一些事件,导致回调被运行,处理传入的数据,或者将更多的数据写入套接字等。一旦需要处理的所有I / O对象都被执行了处理后,主Node.js JS线程将再次等待,直到它被告知有更多数据可用(或其他一些操作已完成或超时)。
下一次被唤醒时,很可能是由于需要处理不同的I / O对象 - 例如不同的网络连接。每次都会运行相关的回调,然后它会回到睡眠状态,等待其他事情发生。
重要的是,不同请求的处理是交错的,它不会从头到尾处理一个请求,然后移动到下一个请求。
在我看来,这样做的主要优点是缓慢的请求(例如,你试图通过2G数据连接向移动电话设备发送1MB的响应数据,或者你正在进行非常慢的数据库查询)阻止更快的。
在传统的多线程Web服务器中,您通常会为每个正在处理的请求都有一个线程,它只会处理该请求,直到完成为止。如果您有很多缓慢的请求会发生什么?最终你的很多线程都在处理这些请求,而其他请求(可能非常简单的请求可以很快处理)会排在他们后面。
除了Node.js之外,还有很多其他基于事件的系统,与传统模型相比,它们往往具有相似的优点和缺点。
我不认为基于事件的系统在任何情况下或每个工作负载都更快 - 它们往往适用于I / O绑定的工作负载,对于受CPU限制的工作负载则不太好。
加入slebetman答案:
当你说
在内部,
由于您不熟悉NodeJS,请阅读有关
阅读http://javascriptissexy.com上的博客,当我开始使用JavaScript / NodeJS时,它们对我很有帮助。
单线程事件循环模型处理步骤:
-
客户端向Web服务器发送请求。
-
Node JS Web Server在内部维护一个有限的线程池
为客户请求提供服务。 -
节点JS Web服务器接收这些请求并将它们放入
队列。它被称为"事件队列"。 -
Node JS Web Server内部有一个Component,称为"Event Loop"。
为什么它有这个名字是它使用无限循环接收
请求并处理它们。 -
Event Loop仅使用单线程。它是Node JS的核心
平台处理模型。 -
甚至循环检查任何客户端请求都放在事件队列中。如果
不,然后等待无限期的传入请求。 -
如果是,则从事件队列中选择一个客户端请求
- 启动客户端请求的流程
-
如果该客户端请求不需要任何阻止IO
操作,然后处理所有内容,准备响应并发送它
回到客户端。 -
如果该客户端请求需要一些阻止IO操作,如
然后与数据库,文件系统,外部服务进行交互
将遵循不同的方法 -
从内部线程池检查线程可用性
- 拾取一个线程并将此客户端请求分配给该线程。
-
该线程负责接收该请求,处理该请求,
执行阻止IO操作,准备响应并将其发回
到事件循环@Rambabu Posa非常好地解释了更多解释去扔这个链接