Node.js 架构与性能

8
我有一个关于Node js的架构和性能的问题。
我已经阅读了很多相关的资料(包括Stack Overflow),但仍有几个问题。我的目标是:
1.从多个不同来源中提炼出总结,以便确认我的结论是否正确。 2.询问一些关于Node线程和性能的问题,这些问题我在研究中没有找到确切的答案。
Node具有单线程、异步事件处理架构。
单线程: - 有一个单一的事件线程来分派异步任务(通常是I/O操作,也可能是计算),并执行回调函数(即处理异步任务结果)。
  • 事件线程在无限制的“事件循环”中运行,完成上述两个工作: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 []”表示服务员启动回调的参与。
这样的链在程序上有三种常见的表示方式:
1. 嵌套函数/回调 2. 使用.then()链接的promises 3. 调用异步结果的异步方法。
所有这些编程方法基本上是等效的,尽管异步/等待似乎是最干净的,并且使得关于异步编程的推理更容易。
以下是我对正在发生的事情的心理描绘...是否正确?非常感谢您的评论!
问题:
我的问题涉及使用操作系统支持的异步操作,实际上谁执行异步工作以及这种架构比“针对每个请求生成一个线程”(即多个厨师)架构更高效的方式:
1. Node库已经通过使用跨平台异步库libuv来设计为异步的,是吗?这里的想法是libuv向node(在所有平台上)提供一致的异步I/O接口,但在底层使用平台相关的异步I/O操作。在I/O请求“一直下去”到受操作系统支持的异步操作的情况下,谁“做工作”等待I/O返回并触发节点?是内核,使用内核线程吗?如果不是,那是谁?在任何情况下,这个实体可以处理多少请求?
2. 我读到libuv在内部也使用线程池(通常是pthread,每个核心一个?)。这是为了“包装”不“一直下去”的操作,以便线程可以用来等待同步操作,以便libuv可以提供异步API吗?
3. 关于性能,通常给出的说明是解释类似node架构可以提供的性能提升:想象一下(可能更慢和更胖)的针对每个请求生成一个线程的方法——存在延迟、CPU和内存开销,因为产生了一堆只是坐在那里等待I/O完成的线程(即使它们没有忙等待),然后拆除它们,而node主要通过使用长期活动的事件线程将异步I/O分派给OS/内核来消除这些问题,是吗?但归根结底,有些东西正在互斥锁上睡觉并在I/O准备好时被唤醒...这个想法是如果是内核,那么比用户空间线程更有效率?最后,如果请求由libuv的线程池处理,会怎样呢?这似乎类似于针对每个请求生成一个线程的方法,除了使用池的效率(避免启动和拆除),但在这种情况下,当有许多请求和池有积压时会发生什么?...延迟增加,现在你比针对每个请求生成一个线程的方法做得更差了,是吗?
1个回答

5
在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也在内部使用线程池(通常是pthread,每个内核一个?)来“包装”没有完全作为异步进行的操作。这是真的吗?

是的。

实际上据我所知,所有操作系统都提供足够好的异步I/O接口,您不需要线程池。编程语言Tcl自80年代以来一直像node一样处理异步I/O而无需线程池。但是它非常混乱,不太简单。Node开发人员决定在涉及磁盘I/O时不想处理这个混乱,而是使用更经过测试的阻塞文件API和线程。

但是说到底,某些东西正在互斥锁上睡觉,并在I/O准备就绪时被唤醒

我希望我的第一个答案也回答了这个问题。但是如果您想知道那些东西是什么,我建议您阅读C中的select()函数。如果您了解C编程,我建议您尝试使用select()编写一个没有线程的TCP/IP程序。Google“select c”。在另一个答案中,我对C级别上的所有这些操作方式都有更详细的解释:I know that callback function runs asynchronously, but why?

当请求很多且线程池有积压时会发生什么?延迟会增加,现在你比起每个请求一个线程的情况更糟了,对吧?
我希望一旦你理解了我的第一个问题的答案,你也会意识到即使使用线程,还是无法避免积压。硬件实际上并不支持操作系统级别的线程。硬件线程数量受限于核心数量,因此在硬件层面上,CPU 就是一个线程池。单线程和多线程之间的区别只是多线程程序可以在硬件中真正并行地执行多个线程,而单线程程序只能使用一个CPU。
异步 I/O 和传统的多线程程序之间唯一的真正区别是线程创建的延迟。从这个意义上说,像 node.js 这样的程序并没有优于使用线程池的程序,如 nginx 和 apache2。
然而,由于 CGI 的工作方式,像 node.js 这样的程序仍然会有更高的吞吐量,因为您不必为每个请求重新初始化解释器和程序中的所有对象。这就是为什么大多数语言已经转向运行为 HTTP 服务的 Web 框架(例如 node 的 Express.js)或类似 FastCGI 的东西。
注意:您真的想知道线程创建延迟的重要性吗?在 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(回调、promises)易于使用,因此您可以编写整个程序而无需降到汇编或 C 的级别。
如果您认为回调很难使用,我强烈建议您尝试使用 C 编写那个基于 select() 的程序。

1
@RandyCasburn,除非你正在编写操作系统或在微控制器上编写无操作系统的嵌入式程序,或者是那种不能忍受不知道魔法如何工作的人,否则你永远不需要了解这些内容。 - slebetman
@slebman - 当然可以 - 这只是对您和您传授知识能力的薄弱赞赏尝试。感谢您抽出时间。 - Randy Casburn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接