我有一个关于 Node js 的架构和性能的问题。
我已经阅读了很多关于这个主题的文章(包括 Stack Overflow),但我仍然有几个问题。我想做两件事:
半简洁地看看我的结论是否正确。
Node 具有单线程、异步事件处理架构
单线程
- 有一个事件线程分派(dispatch)异步工作(结果通常是 I/O,但可以是计算)并执行回调执行(即处理异步工作结果)。
异步
由请求(例如 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 种常见方式以编程方式表示:
所有这些编码方法几乎是等效的,尽管 asynch/await 似乎是最干净的并且使有关异步编码的推理更容易。
这是我对正在发生的事情的心理想象……它是正确的吗?非常感谢评论!
问题
我的问题涉及操作系统支持的异步操作的使用,谁实际执行异步工作,以及该架构比“每个请求生成一个线程”(即多个厨师)架构的性能更高的方式:
最佳答案
这里有关于 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 的现代世界中,操作系统仍然执行这种进程管理,因为典型的现代操作系统运行数十个进程,而 PC 通常只有两个或四个内核。多核机器还有另一个区别:
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/