Node.js架构解析:单线程事件循环机制详解

今天我们将深入研究Node JS的架构和单线程事件循环模型。在我们之前的文章中,我们已经讨论了Node JS的基础知识、组件和安装。

Node JS 架构在开始一些Node JS编程示例之前,了解Node JS的架构是很重要的。我们将在本文中讨论“Node JS是如何在底层工作的,它遵循什么类型的处理模型,如何通过单线程模型处理并发请求”等问题。

Node.js 单线程事件循环模型正如我们已经讨论过的那样,Node JS应用程序使用“单线程事件循环模型”架构来处理多个并发客户端。有许多Web应用程序技术,如JSP、Spring MVC、ASP.NET、HTML、Ajax、jQuery等。但是所有这些技术都遵循“多线程请求-响应”架构来处理多个并发客户端。我们已经很熟悉“多线程请求-响应”架构,因为大多数Web应用程序框架都使用它。但是为什么Node JS平台选择了不同的架构来开发Web应用程序?多线程和单线程事件循环架构之间的主要区别是什么?任何Web开发人员都可以轻松学习Node JS并开发应用程序。但是如果不了解Node JS内部机制,就不能很好地设计和开发Node JS应用程序。因此,在开始开发Node JS应用程序之前,我们首先将学习Node JS平台的内部机制。

Node JS 平台Node.js 平台采用 “单线程事件循环” 架构来处理多个并发客户端。那么它是如何处理并发客户端请求而不使用多个线程的呢?什么是事件循环模型?我们将一一讨论这些概念。在讨论 “单线程事件循环” 架构之前,我们先来了解一下著名的 “多线程请求-响应” 架构。

传统网络应用处理模式没有使用Node JS开发的任何Web应用程序通常遵循“多线程请求-响应”模型。简单来说,我们可以将这个模型称为请求/响应模型。客户端向服务器发送请求,然后服务器根据客户端的请求进行一些处理,准备响应并将其发送回客户端。该模型使用HTTP协议。由于HTTP是一种无状态协议,所以这个请求/响应模型也是无状态模型。因此,我们可以将其称为请求/响应无状态模型。然而,这个模型使用多线程来处理并发的客户端请求。在讨论这个模型的内部机制之前,请先看一下下面的图表。请求/响应模型处理步骤:

  • Clients Send request to Web Server.
  • Web Server internally maintains a Limited Thread pool to provide services to the Client Requests.
  • Web Server is in infinite Loop and waiting for Client Incoming Requests
  • Web Server receives those requests.Web Server pickup one Client Request
    Pickup one Thread from Thread pool
    Assign this Thread to Client Request
    This Thread will take care of reading Client request, processing Client request, performing any Blocking IO Operations (if required) and preparing Response
    This Thread sends prepared response back to the Web Server
    Web Server in-turn sends this response to the respective Client.

服务器在无限循环中等待并执行上述所有步骤,适用于n个客户端。这意味着该模型为每个客户端请求创建一个线程。如果更多客户端请求需要阻塞的IO操作,那么几乎所有的线程都会忙于准备他们的响应。然后剩下的客户端请求将需要等待更长的时间。图表描述:

  • Here “n” number of Clients Send request to Web Server. Let us assume they are accessing our Web Application concurrently.
  • Let us assume, our Clients are Client-1, Client-2… and Client-n.
  • Web Server internally maintains a Limited Thread pool. Let us assume “m” number of Threads in Thread pool.
  • Web Server receives those requests one by one.Web Server pickup Client-1 Request-1, Pickup one Thread T-1 from Thread pool and assign this request to Thread T-1

    Thread T-1 reads Client-1 Request-1 and process it
    Client-1 Request-1 does not require any Blocking IO Operations
    Thread T-1 does necessary steps and prepares Response-1 and send it back to the Server
    Web Server in-turn send this Response-1 to the Client-1

    Web Server pickup another Client-2 Request-2, Pickup one Thread T-2 from Thread pool and assign this request to Thread T-2

    Thread T-2 reads Client-1 Request-2 and process it
    Client-1 Request-2 does not require any Blocking IO Operations
    Thread T-2 does necessary steps and prepares Response-2 and send it back to the Server
    Web Server in-turn send this Response-2 to the Client-2

    Web Server pickup another Client-n Request-n, Pickup one Thread T-n from Thread pool and assign this request to Thread T-n

    Thread T-n reads Client-n Request-n and process it
    Client-n Request-n require heavy Blocking IO and computation Operations
    Thread T-n takes more time to interact with external systems, does necessary steps and prepares Response-n and send it back to the Server
    Web Server in-turn send this Response-n to the Client-nIf “n” is greater than “m” (Most of the times, its true), then server assigns Threads to Client Requests up to available Threads. After all m Threads are utilized, then remaining Client’s Request should wait in the Queue until some of the busy Threads finish their Request-Processing Job and free to pick up next Request. If those threads are busy with Blocking IO Tasks (For example, interacting with Database, file system, JMS Queue, external services etc.) for longer time, then remaining clients should wait longer time.

  • Once Threads are free in Thread Pool and available for next tasks, Server pickup those threads and assign them to remaining Client Requests.
  • Each Thread utilizes many resources like memory etc. So before going those Threads from busy state to waiting state, they should release all acquired resources.

请求/响应无状态模型的缺点:

  • Handling more and more concurrent client’s request is bit tough.
  • When Concurrent client requests increases, then it should use more and more threads, finally they eat up more memory.
  • Sometimes, Client’s Request should wait for available threads to process their requests.
  • Wastes time in processing Blocking IO Tasks.

Node.js 架构 – 单线程事件循环

这是文章《Node.js 架构 – 单线程事件循环》的第2部分(共3部分)。

内容片段:Node.js 平台不遵循传统的请求/响应多线程状态模型,而是采用单线程事件循环模型。Node.js 的处理模型主要基于 JavaScript 的事件模型和回调机制。读者应该对 JavaScript 事件和回调机制有基本的了解。如果不熟悉,建议先阅读相关文章或教程,在继续学习前掌握这些概念。由于 Node.js 采用这种架构,它能够轻松处理越来越多的并发客户端请求。在深入探讨该模型内部机制之前,请参考下方的图表。笔者尝试设计此图表以解释 Node.js 内部的每一个细节。Node.js 处理模型的核心是”事件循环”。理解了这一点,掌握 Node.js 内部工作原理就会变得非常容易。单线程事件循环模型的处理步骤如下:

  • 客户端向 Web 服务器发送请求。
  • Node.js Web 服务器内部维护一个有限的线程池,为客户端请求提供服务。
  • Node.js Web 服务器接收这些请求并将它们放入一个队列中。这个队列被称为”事件队列”(Event Queue)。
  • Node.js Web 服务器内部有一个组件,称为”事件循环”(Event Loop)。之所以这样命名,是因为它使用无限循环来接收和处理请求。(请参阅下方的 Java 伪代码以理解这一点)。
  • 事件循环仅使用单线程。它是 Node.js 平台处理模型的核心。
  • 事件循环检查是否有客户端请求被放入事件队列。如果没有,则无限期地等待传入的请求。
  • 如果有,则从事件队列中取出一个客户端请求并开始处理该请求:
    如果该客户端请求不需要任何阻塞 I/O 操作,则处理所有内容,准备响应并将其发送回客户端。
    如果该客户端请求需要一些阻塞 I/O 操作,如与数据库、文件系统、外部服务交互,那么它将采用不同的处理方式:

    从内部线程池检查可用线程
    选择一个线程并将该客户端请求分配给该线程。
    该线程负责接收请求,处理它,执行阻塞 I/O 操作,准备响应并将其发送回事件循环
    然后,事件循环将该响应发送给相应的客户端。

图表描述:

  • 这里有”n”个客户端向 Web 服务器发送请求。假设它们正在并发访问我们的 Web 应用程序。
  • 假设我们的客户端是客户端-1、客户端-2……和客户端-n。
  • Web 服务器内部维护一个有限的线程池。假设线程池中有”m”个线程。
  • Node.js Web 服务器接收客户端-1、客户端-2……和客户端-n 的请求,并将它们放入事件队列中。
  • Node.js 事件循环逐一获取这些请求。事件循环获取客户端-1的请求-1:

    检查客户端-1的请求-1是否需要任何阻塞 I/O 操作或需要更多时间进行复杂计算任务。
    由于此请求是简单计算和非阻塞 I/O 任务,因此不需要单独的线程来处理它。
    事件循环处理客户端-1请求-1操作中提供的所有步骤(这里的操作指的是 JavaScript 的函数)并准备响应-1
    事件循环将响应-1发送给客户端-1

    事件循环获取客户端-2的请求-2:

    检查客户端-2的请求-2是否需要任何阻塞 I/O 操作或需要更多时间进行复杂计算任务。
    由于此请求是简单计算和非阻塞 I/O 任务,因此不需要单独的线程来处理它。
    事件循环处理客户端-2请求-2操作中提供的所有步骤并准备响应-2
    事件循环将响应-2发送给客户端-2

    事件循环获取客户端-n的请求-n:

    检查客户端-n的请求-n是否需要任何阻塞 I/O 操作或需要更多时间进行复杂计算任务。
    由于此请求是非常复杂的计算或阻塞 I/O 任务,事件循环不处理此请求。
    事件循环从内部线程池中选择线程 T-1,并将此客户端-n请求-n分配给线程 T-1
    线程 T-1读取并处理请求-n,执行必要的阻塞 I/O 或计算任务,最后准备响应-n
    线程 T-1将此响应-n发送给事件循环
    事件循环进而将此响应-n发送给客户端-n

在这里,客户端请求是对一个或多个 JavaScript 函数的调用。JavaScript 函数可能会调用其他函数,也可能利用其回调函数的特性。因此,每个客户端请求的样式如下所示:例如:

function1(function2,callback1);
function2(function3,callback2);
function3(input-params);

注意:

  • 如果您不理解这些函数是如何执行的,那么您可能不熟悉 JavaScript 函数和回调机制。
  • 我们应该对 JavaScript 函数和回调机制有一定的了解。在开始我们的 Node.js 应用程序开发之前,请阅读一些在线教程。

Node.js 架构 – 单线程事件循环的优势

  1. 处理越来越多的并发客户请求非常容易。
  2. 尽管 Node.js 应用程序收到越来越多的并发客户请求,但由于事件循环机制,不需要创建越来越多的线程。
  3. Node.js 应用程序使用较少的线程,因此只占用较少的资源或内存。

事件循环伪代码

这是文章《Node.js 架构 – 单线程事件循环》的第3部分(共3部分)。

作为一名Java开发者,我将尝试用Java术语来解释”事件循环是如何工作的”。以下代码并非纯粹的Java实现,而是为了帮助每个人都能理解事件循环的原理。如果您在理解过程中遇到任何问题,欢迎留言交流。

下面这段伪代码展示了Node.js事件循环的基本工作原理:

public class EventLoop {
    // 事件循环持续运行
    while(true) {
        // 检查事件队列是否接收到JavaScript函数调用
        if(EventQueue.receivesJavaScriptFunctionCall()) {
            // 从事件队列获取客户端请求
            ClientRequest request = EventQueue.getClientRequest();
            
            // 判断请求是否需要阻塞IO或需要大量计算
            if(request.requiresBlockingIO() || request.takesMoreComputationTime()) {
                // 将请求分配给线程T1处理
                assignRequestToThreadT1(request);
            } else {
                // 直接处理请求并准备响应
                processAndPrepareResponse(request);
            }
        }
    }
} 

以上内容就是关于Node.js架构和Node.js单线程事件循环的全部讲解。

bannerAds