繁体   English   中英

Node.js 架构和性能

[英]Node js architecture and performance

我有一个关于 Node js 的架构和性能的问题。

我已经阅读了很多关于这个主题的文章(包括 Stack Overflow),但我仍然有几个问题。 我想做两件事:

  1. 半简洁地总结我从爬取许多不同来源中学到的东西,看看我的结论是否正确。
  2. 问几个关于 Node 的线程和性能的问题,我无法从我的研究中确定确切的答案。

Node 具有单线程、异步事件处理架构

单线程- 有一个单独的事件线程调度异步工作(结果通常是 I/O,但可以是计算)并执行回调执行(即处理异步工作结果)。

  • 事件线程在一个无限的“事件循环”中运行,完成上面的 2 个工作; a) 通过分派异步工作来处理请求,以及 b) 注意到先前的异步工作结果已准备就绪并执行回调来处理结果。

  • 这里常见的类比是餐厅点餐员:事件线程是一个超快的服务员,从餐厅接受订单(服务请求)并将订单交付到厨房准备(调度异步工作),但也通知当食物准备好(异步结果)并将其送回餐桌(回调执行)。

  • 服务员不做任何食物; 他的工作是尽快从餐厅来回厨房。 如果他在餐厅接受订单时陷入困境,或者如果他被迫回到厨房准备其中一顿饭,系统就会变得效率低下,系统吞吐量也会受到影响。

异步请求(例如 Web 请求)产生的异步工作流在逻辑上是一个链:例如

   FIRST [ASYNC: read a file, figure out what to get from the database] THEN 
   [ASYNC: query the database] THEN 
   [format and return the result].

上面标有“ASYNC”的工作是“厨房工作”,“FIRST []”和“THEN []”代表服务员参与发起回调。

像这样的链以 3 种常见方式以编程方式表示:

  • 嵌套函数/回调

  • 用 .then() 链接的 promise

  • 对异步结果进行 await() 的异步方法。

所有这些编码方法几乎是等效的,尽管 asynch/await 似乎是最干净的并且使有关异步编码的推理更容易。

这是我对正在发生的事情的心理想象……它是正确的吗? 非常感谢评论!

问题

我的问题涉及操作系统支持的异步操作的使用,谁实际执行异步工作,以及该架构比“每个请求生成一个线程”(即多个厨师)架构的性能更高的方式:

  1. 通过使用跨平台异步库 libuv,节点库已被设计为异步的,对吗? 这里的想法是 libuv 为节点(在所有平台上)提供一致的异步 I/O 接口,然后在引擎盖下使用依赖于平台的异步 I/O 操作吗? 在 I/O 请求“一直向下”到 OS 支持的异步操作的情况下,谁在“做”等待 I/O 返回并触发节点的工作? 它是内核,使用内核线程吗? 如果不是,是谁? 无论如何,这个实体可以处理多少个请求?

  2. 我读过 libuv 也在内部使用线程池(通常是 pthreads,每个内核一个?)。 这是为了将不会“一路向下”的操作“包装”为异步,以便可以使用线程坐下来等待同步操作,从而使 libuv 可以提供异步 API?

  3. 关于性能,用于解释类似节点的架构可以提供的性能提升的通常说明是:想象(可能更慢和更胖)线程每个请求的方法——产生延迟、CPU 和内存开销一堆线程只是坐在等待 I/O 完成(即使它们不忙于等待)然后将它们拆除,node 在很大程度上使这种情况消失,因为它使用了一个长期存在的事件线程来将异步 I/O 分派到操作系统/内核,对吗? 但是在一天结束时,某些东西在互斥锁上睡觉并在 I/O 准备好时被唤醒……是不是内核比用户线程更有效率? 最后,请求由libuv的线程池处理的情况如何……除了使用池的效率(避免启动和拆卸)之外,这似乎类似于每个请求的线程方法,但在这种情况下,当有很多请求并且池有积压时会发生什么?......延迟增加,现在你的表现比每个请求的线程差,对吧?

这里有关于 SO 的很好的答案,可以让您更清晰地了解架构。 但是,您有一些可以回答的具体问题。

谁在“做”等待 I/O 返回并触发节点的工作? 它是内核,使用内核线程吗? 如果不是,是谁? 无论如何,这个实体可以处理多少个请求?

实际上,线程和异步 I/O 都是在同一个原语之上实现的:操作系统事件队列。

多任务操作系统的发明是为了允许用户使用单个 CPU 内核并行执行多个程序。 是的,当时确实存在多核、多线程系统,但它们很大(通常有两到三间普通卧室的大小)且价格昂贵(通常是一两间普通房屋的成本)。 这些系统可以在没有操作系统帮助的情况下并行执行多个操作。 您所需要的只是一个简单的加载程序(称为执行程序,一种原始的类似 DOS 的操作系统),您可以在没有操作系统帮助的情况下直接在程序集中创建线程。

更便宜、更大规模生产的计算机一次只能运行一件事。 长期以来,这是用户可以接受的。 然而,习惯了分时系统的人希望从他们的计算机中获得更多。 因此发明了进程和线程。

但是在操作系统级别没有线程。 操作系统本身提供线程服务(嗯……从技术上讲,您可以将线程作为库来实现,而无需操作系统支持)。 那么操作系统是如何实现线程的呢?

中断。 它是所有异步处理的核心。

进程或线程只是一个等待 CPU 处理并由操作系统管理的事件。 这是可能的,因为 CPU 硬件支持中断。 任何等待 I/O 事件(来自鼠标、磁盘、网络等)的线程或进程都会被停止、暂停并添加到事件队列中,并且在等待时间内执行其他进程或线程。 CPU 中还内置了一个可以触发中断的定时器(令人惊讶的是,这种中断被称为定时器中断)。 这个定时器中断会触发操作系统的进程/线程管理系统,这样即使没有一个进程在等待 I/O 事件,您仍然可以并行运行多个进程。

这是多任务处理的核心。 除了操作系统设计、嵌入式编程(你经常需要在没有操作系统的情况下做类似操作系统的事情)和实时编程之外,通常不会教授这种编程(使用定时器和中断)。

那么,异步 I/O 和进程之间有什么区别?

除了操作系统向程序员公开的 API 之外,它们完全相同:

  • 进程/线程:嘿,程序员,假设您正在为单个 CPU 编写一个简单的程序,并假设您可以完全控制 CPU。 来吧,使用我的 I/O。 当我处理并行运行的混乱时,我会保持你控制 CPU 的错觉。

  • 异步I/O :你认为你比我更了解? 好的,我让您直接将事件侦听器添加到我的内部队列中。 但我不打算处理事件发生时调用哪个函数。 我只是粗鲁地唤醒你的过程,你自己处理所有这些。

在多核 CPU 的现代世界中,操作系统仍然执行这种进程管理,因为典型的现代操作系统运行数十个进程,而 PC 通常只有两个或四个内核。 多核机器还有另一个区别:

  • 进程/线程:由于我正在为您处理进程队列,我想您不会介意我分散您要求我在多个 CPU 上运行的线程的负载吧? 这样我会让硬件并行完成工作。

  • 异步 I/O :抱歉,我无法将所有不同的等待回调分布在不同的 CPU 上,因为我不知道你的代码到底在做什么。 单核给你!

我读过 libuv 也在内部使用线程池(通常是 pthreads,每个内核一个?)。 这是为了将不会“一直向下”的操作“包装”为异步吗?

是的。

实际上,据我所知,所有操作系统都提供了足够好的异步 I/O 接口,您不需要线程池。 自 80 年代以来,编程语言Tcl一直在处理异步 I/O 之类的节点,而无需线程池的帮助。 但它非常凌乱,并不那么简单。 Node 开发人员决定,当涉及到磁盘 I/O 时,他们不想处理这种混乱,而只是将经过充分测试的阻塞文件 API 与线程一起使用。

但是在一天结束时,某些东西正在互斥锁上睡觉并在 I/O 准备好时被唤醒

我希望我对(1)的回答也能回答这个问题。 但是,如果您想知道那是什么,我建议您阅读 C 中的select()函数。如果您了解 C 编程,我建议您尝试使用select()编写没有线程的 TCP/IP 程序。 谷歌“选择c”。 我在另一个答案中更详细地解释了这一切是如何在 C 级别工作的: 我知道回调函数异步运行,但为什么呢?

当有很多请求并且池有积压时会发生什么?......延迟增加,现在你的表现比线程每个请求更糟糕,对吧?

我希望一旦你理解了我对 (1) 的回答,你也会意识到即使你使用线程也无法摆脱积压。 硬件并不真正支持操作系统级线程。 硬件线程受限于内核数量,因此在硬件级别,CPU 是一个线程池。 单线程和多线程的区别很简单,多线程程序真正可以在硬件中并行执行多个线程,而单线程程序只能使用单个CPU。

异步 I/O 和传统多线程程序之间唯一真正的区别是线程创建延迟。 从这个意义上说,像 node.js 这样的程序比使用像 nginx 和 apache2 这样的线程池的程序没有任何优势。

但是,由于 CGI 的工作方式,像 node.js 这样的程序仍然具有更高的吞吐量,因为您不必为每个请求重新初始化解释器和程序中的所有对象。 这就是为什么大多数语言已经转移到作为 HTTP 服务(如 node 的 Express.js)或 FastCGI 之类的东西运行的 Web 框架。


注意:你真的想知道线程创建延迟有什么大不了的吗? 在 90 年代末/ 2000 年代初,有一个 Web 服务器基准测试。 Tcl 是一种众所周知的平均比 C 慢 500% 的语言(因为它基于像 bash 这样的字符串处理)设法胜过 apache(这是在 apache2 之前并触发了创建 apache2 的完整重新架构)。 原因很简单:tcl 有很好的异步 I/O api,所以程序员更有可能使用异步 I/O。 仅此一项就击败了用 C 编写的程序(不是说 C 没有异步 I/O,毕竟 tcl 是用 C 编写的)。

node.js 相对于 Java 等语言的核心优势不在于它具有异步 I/O。 正是异步 I/O 无处不在,而且 API(回调、承诺)易于使用,因此您可以使用异步 I/O 编写整个程序,而无需下拉到汇编或 C。

如果您认为回调很难使用,我强烈建议您尝试用 C 编写基于select()的程序。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM