原文地址:http://www.journaldev.com/7462/node-js-architecture-single-threaded-event-loop
原作者:RAMBABU POSA
之前我们已经讨论过Node JS基础,Node JS组件和Node JS安装。今天让我们来了解Node JS的体系架构和单线程事件循环模型。
Node JS体系架构
在开始学习Node JS编程示例前,了解Node JS的体系架构是十分重要的。我们将要讨论,Node JS的底层工作原理是什么,它遵循怎样的处理模型,以及它是如何使用单线程模型处理来自客户端的并发请求。
Node JS 单线程事件循环模型
之前提到,Node JS应用使用“单线程事件循环模型”(Single Threaded Event Loop Model)处理来自客户端的多并发请求。
现在有很多的Web应用开发技术,例如JSP,Spring MVC,ASP.NET,HTML,Ajax,jQuery等。但是所有的这些技术都是遵循“多线程请求/响应”(Multi-Threaded Request-Response)的结构去处理来自客户端的多并发请求。
由于“多线程请求/响应“结构已经在大量的web应用框架中使用,因此我们应该已经对其有所了解。但是为什么Node JS却选择了与这些框架不同的体系结构呢。多线程结构和单线程事件循环结构的主要区别是什么呢。
任何一个开发者都可以轻松的学会Node JS并使用它开发程序。但是如果不了解Node JS的内部机制,则将会无法更好地设计和开发Node JS应用。因此在开发前,有必要先了解Node JS平台的内部机理。
Node JS 平台
Node JS平台使用“单线程事件循环模型”处理来自客户端的多并发请求。但是它是如何在不使用多线程的请胯下处理多并发请求呢?事件循环模型又是什么呢。我们将逐一进行讲解。
在讨论“单线程事件循环模型”之前,首先来回顾下“多线程请求-响应”结构。
传统的Web应用处理模型
当今大多数未使用Node JS进行开发的Web应用,基本上都是遵循“多线程请求-响应”结构,简称为”请求/响应模型“(Request/Response Model)。
客户端将请求发送给服务端,服务端根据请求进行处理,准备响应(数据),并将其返回给客户端。
这个模型使用HTTP协议。由于HTTP是无状态协议,因此”请求/响应模型“也是无状态的模型。所以我们又可以称其为”无状态请求/响应模型“(Request/Response Stateless Model)。
简而言之,这个模型使用的是多线程来处理客户端的多并发请求。在讨论其内部机理前,我们先通过下面的图进行概览。
”请求/响应模型“的处理步骤
- 客户端向服务端发送请求
- Web服务端在内部维护一个有个数限制的线程池,为客户端的请求提供服务
- Web服务端循环监听来自客户端的请求
- Web服务端收到请求
- Web服务端选择一个客户端请求
- 从线程池(Thread pool)选择一个线程(Thread)
- 将该线程分配给刚选择的请求
- 该线程将会负责读取,处理客户端请求,执行任何I/O阻塞的操作(如果需要的话),以及准备响应的内容
- 该线程将准备就绪的响应发送给Web服务端
- Web服务端依次将响应回执给客户端
服务器采用无线循环监听客户端的请求,针对所有的客户端请求执行上述的所有步骤。这意味着对于每一个客户端请求,该模型(请求/响应模型)都要为其创建一个线程。
如果更多的客户端请求需要I/O阻塞操作的化,那么所有的线程将在准备响应阶段处于繁忙状态。这就意味着后续的客户端请求需要等待更长的时间才能得到响应。
图示描述:
- 这里有“n“个客户端向服务端发送请求,假设它们是并发进入Web应用程序
- 假设客户端分别为Client-1,Client-2……,Client-n
- Web服务端维护一个有个限的线程池。假设在线程池中的线程个数是”m”
- Web服务端依次接收客户端发送的请求
- Web服务端选择客户端Client-1的Request-1请求,并且从线程池中选择线程T-1作为处理该请求的线程。
- 线程T-1读取客户端Client-1的Request-1请求,进行处理
- Request-1请求不需要I/O阻塞的操作
- 线程T-1执行必须的操作,准备响应Response-1,并将其发送给服务端
- 服务端依照响应次序将Response-1回执给Client-1
- Web服务端选择客户端Client-2的Request-2请求,并且从线程池中选择线程T-2作为处理该请求的线程。
- 线程T-2读取客户端Client-2的Request-2请求,进行处理
- Request-2请求不需要I/O阻塞的操作
- 线程T-2执行必须的操作,准备响应Response-2,并将其发送给服务端
- 服务端依照响应次序将Response-2回执给Client-2
- Web服务端选择客户端Client-n的Request-n请求,并且从线程池中选择线程T-n作为处理该请求的线程。
- 线程T-n读取客户端Client-2的Request-n请求,进行处理
- Request-n请求需要较重的I/O阻塞操作和运算操作
- 线程T-n会花费更多的时间,和外部系统进行交互,并执行必须的操作,准备响应Response-n,并将其发送给服务端
- 服务端依照响应次序将Response-n回执给Client-n
如果n>m(大多情况都是这样),也就是说需要分配给请求的线程数要大于可用的线程数。当所有的线程被使用时,那么剩余的客户端请求就在队列中等待,直到一些处于繁忙状态的线程完成对负责线程的处理任务,改变状态为空闲。
如果线程都长时间处于I/O繁忙的状态(例如,与数据库(Database),文件系统(file system),JMS队列(JMS Queue),外部服务( external services)等)。则剩余未处理的请求则将等待很长时间。
- 当线程池的一些线程已经为执行下个任务准备就绪时,服务端将这些线程分配给剩余的客户端请求
- 每个线程都要使用许多的资源,例如内存等。所以在线程从繁忙状态变为空闲状态时,需要释放所有占用的资源
”请求/响应模型“的缺点
- 在处理大量增加的客户端请求时效率较差
- 当并发的客户端请求增加时,就需要更多的线程,会导致大量的内存被占用
- 有时候,客户端请求需要等待可用的线程来处理它们的请求
- 浪费大量的时间处理I/O阻塞的任务
Node JS体系结构——单线程事件循环
Node JS平台不遵循”多线程无状态的请求/响应模型“,而是采用单线程事件循环模型。它的处理模型主要是基于JavaScript基本事件模型和回调函数机制的结合。
你应该对JavaScript事件和回调函数机制如何运作有了较好的了解。如果没有,请在阅读下面的内容前优先了解基本内容,以帮助理解。
由于Node JS遵循这个体系(单线程事件循环)。因此它可以轻松地处理越来越多的客户端并发请求。在讨论其内部机理前,我们先通过下面的图进行概览。
其处理的主要核心是“事件循环”(Event Loop)。如果了解了这个,那么明白它内部机制就会相对轻松一些。
”单线程事件循环模型“的处理步骤
- 客户端向服务端发送请求
- Node JS服务端在内部维护一个有个数限制的线程池,为客户端的请求提供服务
- Node JS服务端循环监听来自客户端的请求,并将它们放置到一个队列中,这个队列被称为“事件队列”(Event Queue)。
- Node JS内部存在一个叫做“事件循环”(Event Loop)的组件。它通过无限循环来接收请求并进行处理。(可以通过下面的Java伪代码加深了解)
- 事件循环组件只使用单线程。它是Node JS平台处理模型的核心
- 事件循环组件会检查所有在事件队列中的客户端请求。如果没有,则继续等待请求。
- 如果存在请求,则从事件队列中提取一个请求
- 开始处理该请求
- 如果该请求不需要I/O阻塞的操作,则处理必须的工作,准备响应的内容,并将准备就绪的响应发送给客户端
- 如果该请求需要I/O阻塞的操作,如与数据库(Database),文件系统(file system),外部服务( external services)等,则使用不同的处理流程
- 检测内部线程池是否存在可用线程
- 如果有,则从内部线程池提取一个线程,并将其分配处理客户端的请求
- 线程负责读取,读取,处理客户端请求,执行任何I/O阻塞的操作,准备响应的内容,并将准备就绪的响应发送给事件循环组件
- 该线程将会负责读取,处理客户端请求,执行任何I/O阻塞的操作(如果需要的话),以及准备响应的内容
- 该线程将准备就绪的响应发送给Web服务端
- 事件循环组件依次将响应回执给客户端
图示描述:
- 这里有“n“个客户端向服务端发送请求,假设它们是并发进入Web应用程序
- 假设客户端分别为Client-1,Client-2……,Client-n
- Web服务端维护一个有个限的线程池。假设在线程池中的线程个数是”m”
- Node JS服务端接收请求Client-1,Client-2……,Client-n,并将它们放入事件队列
- Node JS的事件循环组件依次提取这些请求
- 事件循环组件选择客户端Client-1的Request-1请求
- 检测该请求是需要I/O阻塞操作还是复杂的计算任务
- 如果只是简单的计算而无I/O阻塞任务,则不需要额外的线程去处理
- 事件循环组件执行请求中提供的所有运算(这里的运算是指JavaScript的方法)并且准备响应Response-1
- 事件循环组件将Response-1回执给Client-1
- 事件循环组件选择客户端Client-2的Request-2请求
- 检测该请求是需要I/O阻塞操作还是复杂的计算任务
- 如果只是简单的计算而无I/O阻塞任务,则不需要额外的线程去处理
- 事件循环组件执行请求中提供的所有运算(这里的运算是指JavaScript的方法)并且准备响应Response-2
- 事件循环组件将Response-2回执给Client-2
- 事件循环组件选择客户端Client-n的Request-n请求
- 检测该请求是需要I/O阻塞操作还是复杂的计算任务
- 由于该请求是复杂的运算或I/O阻塞任务,因此事件循环组件不去处理
- 事件循环组件从内部线程池提取线程T-1,并分配其去处理请求Request-n的任务
- 线程T-1处理请求Request-n,执行必要的I/O阻塞和计算任务,并准备响应Response-n
- 线程T-1将响应Response-n回执给事件循环组件
- 事件循环组件按照次序将Response-n回执给Client-n
这里的客户端请求是一个或多个JavaScript函数。该函数可以调用其他函数或是使用回调函数(Callback function)
每个客户端请求格式如下所示:
function(other-functioncall, callback-function)
例如:
function1(function2,callback1);
function2(function3,callback2);
function3(input-params);
注意:
- 如果你不理解这些函数是如何执行的,那么可能你对JavaScript的函数和回调机制不够熟悉
- 我们应该对JavaScript函数和回调机制有所了解。因此在开始开发Node JS应用前请先通过网上的相关教程进行学习
Node JS体系结构——单线程事件循环的优点
- 能够轻松处理越来越多的客户端请求
- 因为有事件循环组件,因此即使Node JS应用接收到越来越多的客户端并发请求时,也无需创建更多的线程
- 由于使用较少的线程,因此可以减少资源和内存的使用
事件循环的伪代码
由于我是一个Java开发人员,我尝试使用Java术语解释“事件循环是如何工作的”。这并非是纯正的Java代码,我猜每个人都可以明白。如果你在理解上有任何问题,请在评论中告诉我。
public class EventLoop {
while(true){
if(Event Queue receives a JavaScript Function Call){
ClientRequest request = EventQueue.getClientRequest();
If(request requires BlokingIO or takes more computation time)
Assign request to Thread T1
Else
Process and Prepare response
}
}
}
这就是关于Node JS体系结构和单线程事件循环的全部内容。