javascript - Node.js 架构和性能

标签 javascript node.js multithreading asynchronous async-await

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

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

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


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

    单线程
    - 有一个事件线程分派(dispatch)异步工作(结果通常是 I/O,但可以是计算)并执行回调执行(即处理异步工作结果)。
  • 事件线程在一个无限的“事件循环”中运行,完成上面的 2 个工作; a) 通过分派(dispatch)异步工作来处理请求,以及 b) 注意到先前的异步工作结果已准备就绪并执行回调来处理结果。
  • 这里常见的类比是餐厅点餐员:事件线程是一个超快的服务员,从餐厅接受订单(服务请求)并将订单交付到厨房准备(调度异步工作),但也通知当食物准备好(异步结果)并将其送回餐 table (回调执行)。
  • 服务员不做任何食物;他的工作是尽快从餐厅来回厨房。如果他在餐厅接受订单时陷入困境,或者如果他被迫回到厨房准备其中一顿饭,系统就会变得效率低下,系统吞吐量也会受到影响。

  • 异步
    由请求(例如 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 似乎是最干净的并且使有关异步编码的推理更容易。

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

    问题

    我的问题涉及操作系统支持的异步操作的使用,谁实际执行异步工作,以及该架构比“每个请求生成一个线程”(即多个厨师)架构的性能更高的方式:
  • 通过使用跨平台异步库 libuv, Node 库已被设计为异步,对吗?这里的想法是 libuv 为 Node (在所有平台上)提供一致的异步 I/O 接口(interface),然后在引擎盖下使用依赖于平台的异步 I/O 操作吗?在 I/O 请求“一直向下”到 OS 支持的异步操作的情况下,谁在“做”等待 I/O 返回并触发 Node 的工作?它是内核,使用内核线程吗?如果不是,是谁?无论如何,这个实体可以处理多少个请求?
  • 我读过 libuv 也在内部使用线程池(通常是 pthreads,每个内核一个?)。这是为了将不会“一路向下”的操作“包装”为异步,以便可以使用线程坐下来等待同步操作,从而使 libuv 可以提供异步 API?
  • 关于性能,用于解释类似 Node 的架构可以提供的性能提升的通常说明是:想象(可能更慢和更胖)线程每个请求的方法——产生延迟、CPU 和内存开销一堆线程只是坐在等待 I/O 完成(即使它们不忙于等待)然后将它们拆除,node 在很大程度上使这种情况消失,因为它使用了一个长期存在的事件线程来将异步 I/O 分派(dispatch)到操作系统/内核,对吗?但是在一天结束时,某些东西在互斥锁上 sleep 并在 I/O 准备好时被唤醒……是不是内核比用户线程更有效率?最后,请求由libuv的线程池处理的情况如何……除了使用池的效率(避免启动和拆除)之外,这似乎类似于每个请求的线程方法,但在这种情况下,当有很多请求并且池有积压时会发生什么?......延迟增加,现在你的表现比每个请求的线程差,对吧?
  • 最佳答案

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

    Who is "doing the work" of waiting for the I/O to return and triggering node? Is it the kernel, using a kernel thread? If not, who? In any case, how many requests can this entity handle?



    实际上,线程和异步 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 上,因为我不知道你的代码到底在做什么。单核给你!

  • I've read that libuv also makes use of a thread pool (typically pthreads, one per core?) internally. Is this to 'wrap' operations that do not "go all the way down" as async



    是的。

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

    But at the end of the day, SOMETHING is sleeping on a mutex and getting woken up when the I/O is ready



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

    What happens when there's many requests and the pool has a backlog?...latency increases and now you're doing worse than the thread-per-request, right?



    我希望一旦你理解了我对 (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(回调、 promise )易于使用,因此您可以使用异步 I/O 编写整个程序,而无需降级到汇编或 C。

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

    关于javascript - Node.js 架构和性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49101877/

    相关文章:

    javascript - Opera 提交时 javascript 问题

    node.js - 如何将 32 位 node.js(8.5.0) 安装到 64 位 ubuntu(17.0)?

    multithreading - Jersey ContainerResponseFilter 中的@Context HttpServletRequest 范围

    javascript - 将值从 'this' 传递到 Promise 的 'then' 函数的正确方法

    java - 如何停止 ExecutorService 上所有已提交的任务

    Java: "please wait"GlassPane 未正确加载

    c# - 在模型上添加动态只读属性

    javascript - 使用 React-Native 导航传递数据

    java - 在 javascript 库 (ExtJS) 中编写 Web 应用程序的完整 View 逻辑的优缺点?

    javascript - 如何从派生自 Nan::ObjectWrap 的类返回 native 对象?