深入探究 Node.js 架构
在本文中,我们将深入研究 Node.js 架构并了解 Node.js 的异步特性。
让我们深入研究一下。
Node.js 是一个单线程、异步、事件驱动的运行时环境,用于在服务器上运行 Javascript 代码。
单线程意味着 JavaScript 运行时在任何时间点都只同步执行一段代码(或语句)。它只有一个调用堆栈和一个堆内存。那么,运行时如何高效地处理多个异步操作呢?Node.js使用其事件驱动的方法高效地处理了这个问题。现在不用担心这个问题。我们稍后会再讨论 :)。
I/O(输入/输出)是计算机基本操作中最慢的。它涉及访问磁盘数据、读写文件、等待用户输入、进行网络调用、执行某些数据库操作等。它会在请求发送到设备的时刻和操作完成的时刻之间增加延迟。
在传统的阻塞 I/O 编程中,对应于 I/O 请求的函数调用会阻塞线程的执行,直到操作完成。因此,任何使用阻塞 I/O 实现的 Web 服务器都无法在同一个线程中处理多个连接。解决这个问题的方法是使用单独的线程(或进程)来处理每个并发连接。
大多数现代操作系统都支持另一种访问资源的机制,即非阻塞 I/O,其中系统调用总是立即返回,而无需等待 I/O 操作完成。为了有效地处理并发非阻塞资源,它使用了一种称为同步事件多路分解或事件通知接口的机制。同步事件多路分解监视多个资源,并在对其中一个资源执行的读取或写入操作完成时返回一个新事件(或一组事件)。这样做的好处是同步事件多路分解器是同步的,因此它会一直阻塞,直到有新的事件需要处理。
使用通用同步事件多路复用器从两个不同资源读取的算法的伪代码:
-
资源被添加到数据结构(在我们的例子中是 watchesList),并将每个资源与特定操作(例如读取)关联起来
-
解复用器已设置好要监视的资源组。对 demultiplexer.watch() 的调用是同步的,并且会阻塞,直到任何被监视的资源准备好读取为止。当读取完成时,事件解复用器会从调用中返回,并有一组新的事件可供处理。
-
事件解复用器返回的每个事件都会被处理。此时,每个事件关联的资源都保证已准备好读取,并且在操作期间不会阻塞。当所有事件都处理完毕后,流程将再次阻塞在事件解复用器上,直到有新的事件再次可供处理。这被称为神秘的事件循环。
你可能会注意到,在这个模式下,我们可以在一个线程内处理多个 I/O 操作。我们之所以说多路复用,是因为只使用一个线程,我们就可以处理多个资源。
多线程网络应用程序处理网络负载如下:
请求 ---> 产生一个线程
---> 等待数据库请求
----> 回答请求
请求 ---> 产生一个线程
---> 等待数据库请求
----> 回答请求
请求 ---> 产生一个线程
---> 等待数据库请求
----> 回答请求
因此,线程大部分时间都占用 0% 的 CPU 资源,等待数据库返回数据。这样做时,他们必须为线程分配所需的内存,其中包括为每个线程分配一个完全独立的程序堆栈等等。此外,他们还必须启动一个线程,虽然这不像启动一个完整进程那么昂贵,但仍然不算便宜。
既然我们大部分时间的 CPU 使用率为 0%,为什么不在 CPU 空闲的时候运行一些代码呢?这样,每个请求仍然可以获得与多线程应用程序相同的 CPU 时间,但我们不需要启动线程。所以在单线程环境中会发生以下情况:
请求 -> 发出 DB req
请求 -> 发出 DB req
请求 -> 发出 DB req
DB req 完成 -> 发送响应
DB req 完成 -> 发送响应
DB req 完成 -> 发送响应
我们可以看到,只使用一个线程并不会削弱我们并发运行多个 I/O 密集型任务的能力。这些任务会分散在各个时间节点,而不是分散在多个线程中。
现在让我来介绍一下Node.js 的核心——反应堆模式。
反应器模式背后的主要思想是为每个 I/O 操作关联一个处理程序。Node.js 中的处理程序由回调函数表示。一旦事件产生并被事件循环处理,该处理程序就会被调用。因此,反应器模式通过阻塞来处理 I/O,直到从一组观察到的资源中获取新的事件,然后通过将每个事件分派给关联的处理程序来做出响应。
反应器模式的结构如下图所示:
-
应用程序生成一个新的 I/O 操作,并将请求提交给事件多路复用器。应用程序还指定一个处理程序,该处理程序将在操作完成后调用。向事件多路复用器提交新的请求是一个非阻塞操作,它会立即将控制权返回给应用程序。
-
当一组 I/O 操作完成时,事件多路分解器会将一组相应的事件推送到事件队列中。
-
从事件多路分解器接收到一组事件后,事件循环将遍历事件队列的项目。
-
与每个处理程序关联的处理程序被调用。
-
处理程序是应用程序代码的一部分,它在执行完成后将控制权交还给事件循环(a)。
处理程序执行期间,它可以请求新的异步操作,这些操作会将新项目添加到事件多路复用器(b)。 -
当事件队列中的所有项目都被处理后,事件循环会再次阻塞在事件解复用器上,然后在有新事件可用时触发另一个循环。
当事件解复用器中不再有待处理的操作,并且事件队列中不再有需要处理的事件时,Node.js 应用程序将退出。
每个操作系统都有自己的事件多路复用器接口,并且每个 I/O 操作的行为会根据资源类型的不同而有很大差异,即使在同一个操作系统中也是如此。
- 为了处理这些不一致性,Node.js 核心团队创建了一个名为libuv的本机库,它是用 C++ 编写的。
- Libuv 代表 Node.js 的底层 I/O 引擎。它是对操作系统事件多路复用器的更高级别的抽象,这使得 Node.js 与所有主流操作系统兼容,并规范了不同类型资源的非阻塞行为。
- 它还实现了反应堆模式,从而提供了用于创建事件循环、管理事件队列、运行异步 I/O 操作和排队其他类型任务的 API。
- libuv 内部维护一个线程池,用于管理 I/O 操作以及诸如 crypto 和 zlib 之类的 CPU 密集型操作。这是一个大小有限的线程池,允许在其中进行 I/O 操作。如果线程池只包含四个线程,则同一时间只能读取四个文件。
Nodejs 最终的高层架构包括:
-
一组负责包装和向 Javascript 公开 libuv 和其他低级功能的绑定。
-
V8 是 Google 最初为 Chrome 浏览器开发的 JavaScript 引擎。这也是 Node.js 如此快速高效的原因之一。
-
实现高级 Node.js API 的核心 Javascript 库。
结论
Node.js 架构是后端面试的热门话题之一。对于所有 Node.js 开发者来说,深入了解 Node.js 的异步特性是高效编写代码的必备条件。我真心希望你喜欢这篇文章。如果你想了解更多关于 Node.js 的知识,我强烈推荐《Node.js 设计模式》这本书。在下一篇文章中,我们将进一步讨论事件循环。
参考:
-
Node.js 设计模式,作者 Mario Casciaro 和 Luciano Mammino
再见啦。再见 :)
文章来源:https://dev.to/altamashali/deep-dive-into-nodejs-architecture-5190