diff --git a/locale/zh-cn/docs/guides/dont-block-the-event-loop.md b/locale/zh-cn/docs/guides/dont-block-the-event-loop.md index bf091ff7cc971..601cc594c1d86 100644 --- a/locale/zh-cn/docs/guides/dont-block-the-event-loop.md +++ b/locale/zh-cn/docs/guides/dont-block-the-event-loop.md @@ -1,110 +1,111 @@ --- -title: 不要阻塞你的事件轮询(或是工作池) +title: 不要阻塞你的事件循环(或是工作线程池) layout: docs.hbs --- -# 不要阻塞你的事件轮询(或是工作池) +# 不要阻塞你的事件循环(或是工作线程池) ## 你是否应该读这篇指南? -如果你写出的代码不是一行那么简单,阅读本篇指南可以帮助你写出高质量、更安全的程序。 +如果你写出的代码并不是一行命令调用那么简单,那么阅读本篇指南可以帮助你写出高性能、更安全的程序。 -此文档是用节点服务器编写的,但这些概念也适用于复杂的节点应用程序。 -在特定于不同的操作系统,请此文档以 Linux 为中心。 +此文档是从 Node 服务器开发的角度编写的,但这些概念也同样适用于复杂的 Node 应用程序。 +文章中如有涉及到不同操作系统的细节,仅以 Linux 系统为代表。 ## TL; DR -Node.js 通过事件循环机制(初始化和回调)的方式运行 JavaScript 代码,并且提供了一个工作池处理诸如 I/O 高成本的任务。 -Node 自我会调节尺度,有时甚至比更重量级的 Apache 服务器都要好。 -Node 可伸缩性的秘密在于它使用了一些小线程处理许多客户端。 -如果 Node 用更少的线程做这些事,或许将占用工作在客户端更多的系统时间和内存,而不是为这些线程(内存,上下文交换)消耗更多的内存和时间。 -但是 Node 只有少量线程,你必须重构你的程序,有智慧地在程序中使用它们。 +Node.js 通过事件循环机制(初始化和回调)的方式运行 JavaScript 代码,并且提供了一个线程池处理诸如 文件 I/O 等高成本的任务。 +Node 的伸缩性非常好,某些场景下它甚至比类似 Apache 等更重量级的解决方案表现更优异。 +Node 可伸缩性的秘诀在于它仅使用了极少数的线程就可以处理大量客户端连接。 +如果 Node 只需占用很少的线程,那么它就可以将更多的系统 CPU 时间和内存花费在客户端任务而不是线程的空间和时间消耗上(内存,上下文切换)。 +但是同样由于 Node 只有少量线程,你必须非常小心的组织你的应用程序以便合理的使用它们。 -这里有一个很好的经验法则,能使您的节点服务器变快: +这里有一个很好的经验法则,能使您的 Node 服务器变快: *在任何时候,当分配到每个客户端的任务是“少量”的情况下,Node 是非常快的。* -这条法则可以应用于事件轮询中的回调机制,以及在工作池上的任务。 +这条法则可以应用于事件轮询中的回调任务,以及在工作线程池上的任务。 -## 为什么不要阻塞你的事件轮询(或是工作池)? -Node 是用一组少量的线程来处理许多客户端请求的。 -在 Node 中,有两种类型的线程:一个事件循环(即主循环,主线程,事件线程等)。另外一个是在工作池里的 `k` 个工作线程(即线程池)。 +## 为什么不要阻塞你的事件轮询(或是工作线程池)? +Node 是用很少量的线程来处理大量客户端请求的。 +在 Node 中,有两种类型的线程:一个事件循环线程(也被称为主循环,主线程,事件线程等)。另外一个是在工作线程池里的 `k` 个工作线程(也被称为线程池)。 -如果一个线程执行一个回调函数(事件轮询)或者任务(工作线程)需要耗费很长时间,我们称为“阻塞”。 -当一个线程在一个客户端上被阻塞了,它也就无法处理其它客户端的请求了。 -这里给出两个不能阻塞事件轮询和工作池的理由: +如果一个线程执行一个回调函数(事件轮询线程)或者任务(工作线程)需要耗费很长时间,我们称之为“阻塞”。 +当一个线程在处理某一个客户端请求时被阻塞了,它就无法处理其它客户端的请求了。 +这里给出两个不能阻塞事件轮询线程和工作线程的理由: -1. 性能:如果你定期通过任意的某种形式线程处理繁重的任务,你的服务器将面临 *吞吐量*(请求/秒)的考验。 -2. 安全性:如果对于特定的输入,你其中的一个线程阻塞了,那么恶意攻击者可以提交如此的“邪恶输入”,故意让你的线程阻塞,然后使得其它客户端得不到处理。这就是 [拒绝式攻击](https://en.wikipedia.org/wiki/Denial-of-service_attack)。 +1. 性能:如果你在任意类型的线程上频繁处理繁重的任务,那么你的服务器的 *吞吐量*(请求/秒)将面临严峻考验。 +2. 安全性:如果对于特定的输入,你的某种类型的线程可能会被阻塞,那么恶意攻击者可以通过构造类似这样的“恶意输入”,故意让你的线程阻塞,然后使其它客户端请求得不到处理。这就是 [拒绝服务攻击](https://en.wikipedia.org/wiki/Denial-of-service_attack)。 ## 对 Node 的快速回顾 -Node 使用事件驱动机制:它有一个事件轮询的编排,和一个为高消费任务的处理工作池。 +Node 使用事件驱动机制:它有一个事件轮询线程负责任务编排,和一个专门处理繁重任务的工作线程池。 -### 什么代码是在事件轮询上运行的? -当 Node 程序运行时,程序首先完成初始化部分,`需要` 模块为事件注册回调函数。 -然后,Node 应用程序进入事件轮询中,通过执行对应的回调函数对客户端请求做出回应。 -此回调将异步执行,并且可能在完成之后异步注册请求继续处理。 -对于这些异步请求的回调也会在事件轮询中被处理。 +### 哪种代码运行在事件轮询线程上? +当 Node 程序运行时,程序首先完成初始化部分,即处理 `require` 加载的模块和注册事件回调。 +然后,Node 应用程序进入事件循环阶段,通过执行对应回调函数来对客户端请求做出回应。 +此回调将同步执行,并且可能在完成之后继续注册新的异步请求。 +这些异步请求的回调也会在事件轮询线程中被处理。 -事件轮询通过回调函数完成非阻塞异步请求,如网络的 I/O。 +事件循环中同样也包含很多非阻塞异步请求的回调,如网络 I/O。 -总而言之,事件轮询执行为事件而注册的回调函数,并且负责对完成诸如网络 I/O 一样的非阻塞异步请求。 +总体来说,事件轮询线程执行事件的回调函数,并且负责对处理类似网络 I/O 的非阻塞异步请求。 -### 什么代码又运行在工作池上呢? -Node 的工作池通过 libuv ([相关文档](http://docs.libuv.org/en/v1.x/threadpool.html)) 来实现的,它对外提供了一个通用的任务提交 API。 +### 哪种代码运行在工作线程池? +Node 的工作线程池是通过 libuv ([相关文档](http://docs.libuv.org/en/v1.x/threadpool.html)) 来实现的,它对外提供了一个通用的任务处理 API。 -Node 使用工作池来处理“高消费”的任务。 -这包含为一个操作系统执行非阻塞版本的 I/O 操作,同时也包含对 CPU 密集任务的处理。 +Node 使用工作线程池来处理“高成本”的任务。 +这包括一些操作系统并没有提供非阻塞版本的 I/O 操作,以及一些 CPU 密集型的任务。 -Node 模块中有些 API 用到了工作池: +Node 模块中有如下这些 API 用到了工作线程池: 1. I/O 密集型任务: 1. [DNS](https://nodejs.org/api/dns.html):`dns.lookup()`,`dns.lookupService()`。 - 2. [文件系统](https://nodejs.org/api/fs.html#fs_threadpool_usage):所有的文件系统 API。除 `fs.FSWatcher()` 和那些显式同步使用 libuv 的线程池函数之外。 + 2. [文件系统](https://nodejs.org/api/fs.html#fs_threadpool_usage):所有的文件系统 API。除 `fs.FSWatcher()` 和那些显式同步调用的 API 之外,都使用 libuv 的线程池。 2. CPU 密集型任务: 1. [Crypto](https://nodejs.org/api/crypto.html):`crypto.pbkdf2()`,`crypto.randomBytes()`,`crypto.randomFill()`。 - 2. [Zlib](https://nodejs.org/api/zlib.html#zlib_threadpool_usage):所有 Zlib 相关函数,除那些显式同步使用 libuv 的线程池函数之外。 + 2. [Zlib](https://nodejs.org/api/zlib.html#zlib_threadpool_usage):所有 Zlib 相关函数,除那些显式同步调用的 API 之外,都适用 libuv 的线程池。 -在许多 Node 应用程序中,对于工作池而言这些 API 只是任务源。使用了 [C++ 插件](https://nodejs.org/api/addons.html) 的应用程序和模块可以向工作池提交其它任务。 +在许多 Node 应用程序中,这些 API 是工作线程池任务的唯一来源。此外应用程序和模块可以使用 [C++ 插件](https://nodejs.org/api/addons.html) 向工作线程池提交其它任务。 -为了完整性考虑,我们注意到当你从事件轮询机制里的一个回调函数中调用这些 API 函数中的某一个时,事件轮询机制将花费少量的开销,因为这已经进入了为那个函数绑定的 Node C++ 中,并对工作池提交了一个任务。 -和整个任务相比,这些开销微不足道。这就是为什么事件轮询总是要卸载它的原因。 -当向工作池中提交了这些任务中的某个的时候,Node C++ 绑定提供了指向这些相关函数的指针。 +为了完整性考虑,我们必须要说明,当你在事件轮询线程的一个回调中调用这些 API 时,事件轮询线程将不得不为此花费少量的额外开销,因为它必须要进入对应 API 与 C++ 桥接通讯的 Node C++ binding 中,从而向工作线程池提交一个任务。 +和整个任务的成本相比,这些开销微不足道。这就是为什么事件循环线程总是将这些任务转交给工作线程池。 +当向工作线程池中提交了某个任务,Node 会在 C++ binding 中为对应的 C++ 函数提供一个指针。 ### Node 怎么决定下一步该运行哪些代码? -抽象来说,事件轮询机制和工作池为事件等待、任务等待维护了多个队列。 +抽象来说,事件轮询线程和工作池线程分别为等待中的事件回调和等待中的任务维护一个队列。 -实际上,事件轮询机制本身并不维护队列,它拥有一堆文件描述符,要求操作系统监视、使用诸如 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) (Linux),[kqueue](https://developer.apple.com/library/content/documentation/Darwin/Conceptual/FSEvents_ProgGuide/KernelQueues/KernelQueues.html) (OSX),event ports (Solaris) 或者 [IOCP](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365198.aspx) (Windows) 的机制。 -当操作系统确定某个文件的描述符准备完毕,时间轮询机制将把它转换成合适的事件,然后通过那个事件触发相对应的回调函数。 +而事实上,事件轮询线程本身并不维护队列,它持有一堆要求操作系统使用诸如 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) (Linux),[kqueue](https://developer.apple.com/library/content/documentation/Darwin/Conceptual/FSEvents_ProgGuide/KernelQueues/KernelQueues.html) (OSX),event ports (Solaris) 或者 [IOCP](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365198.aspx) (Windows) 等机制去监听的文件描述符。 +这些文件描述符可能代表一个网络套接字,一个监听的文件等等。 +当操作系统确定某个文件的描述符发生变化,事件轮询线程将把它转换成合适的事件,然后触发与该事件对应的回调函数。 你可以通过 [这里](https://www.youtube.com/watch?v=P9csgxBgaZ8) 学习到更多有关这个过程的知识。 -相比较而言,工作池使用一个真实的队列,里边装的都是要被处理的任务。 -一个工作线程从这个队列中取出一个任务,开始处理它。当完成之后这个工作线程在事件循环机制中发出一个“至少有一个任务处于完成状态”的信息。 +相对而言,工作线程池则使用一个真实的队列,里边装的都是要被处理的任务。 +一个工作线程从这个队列中取出一个任务,开始处理它。当完成之后这个工作线程向事件循环线程中发出一个“至少有一个任务完成了”的消息。 ### 对于应用设计而言,这意味着什么? -在“来一个客户,开辟一个线程”的应用中,诸如 Apache,每个等待的客户被分配到对应的线程中去处理。 -如果处理某个客户端的线程阻塞了,操作系统会终止它,并给予下一个客户端一个机会去处理。 -操作系统必须确保客户端是需要少量的工作,而不是被需要更多工作的客户端惩罚。 +在类似 Apache 这种“一个客户端连接一个线程”的系统中,每个处理中的客户端都被分配了一个独立的线程。 +如果处理某个客户端的线程阻塞了,操作系统会中断它,并给予下一个客户端请求执行的机会。 +操作系统必须确保一个只需要少量开销的客户端请求不会被其他需要大量开销的客户端请求影响。 -因为 Node 用少量的线程处理许多客户端,因此如果在处理某个客户端的时候阻塞了,它不能给予其它客户端机会,而是一直等待直到完成全部的回调函数或者任务。 -*因此,对待每个请求客户都应该公平,这是你程序的责任。*. -这意味着,对于每个客户端,在任何简单的回调函数或者任务中,你不应该做太多的事情。 +因为 Node 用少量的线程处理许多客户端连接,如果在处理某个客户端的时候阻塞了,在该客户端请求的回调或任务完成之前,其他等待中的任务可能都不会得到执行机会。 +*因此,保证每个客户端请求得到公平的执行机会变成了应用程序的责任。* +这意味着,对于任意一个客户端,你不应该在一个回调或任务中做太多的事情。 -这是为什么 Node 能够那么成规模地处理,同时也意味着你有义务确保公平排程。 -下一部分将探讨如何对事件循环和工作池进行公平排程。 +这既是 Node 服务能够保持良好伸缩性的原因,同时也意味应用程序必须自己确保公平调度。 +下一部分将探讨如何确保事件循环线程和工作线程池的公平调度。 -## 不要阻塞你的时间轮询 -时间轮询关注着每个新的客户端连接,协调产生一个回应。 -所有这些进入的请求和输出的应答都要通过事件轮询机制。 -这意味着如果你的时间轮询在某个地方花费太多的时间,所有现在和新的客户端请求都得不到机会了。 +## 不要阻塞你的事件轮询线程 +事件轮询线程关注着每个新的客户端连接,协调产生一个回应。 +所有这些进入的请求和输出的应答都要通过事件轮询线程。 +这意味着如果你的事件轮询线程在某个地方花费太多的时间,所有当前和未来新的客户端请求都得不到处理机会了。 -所以,你应该保证你绝不阻塞时间轮询。 -换句话说,每个 JavaScript 回调应该很快就可以完成。 +因此,你应该保证永远不要阻塞事件轮询线程。 +换句话说,每个 JavaScript 回调应该快速完成。 这些当然对于 `await`,`Promise.then` 也同样适用。 -一个能确保做到这一点的方法是解释关于你回调任何的 ["计算复杂度"](https://en.wikipedia.org/wiki/Time_complexity)。 -如果你的回调函数接受固定几个步骤可以完成任务,无论你的参数是什么,那么你总能保证每个等候的请求者一个公平的轮询。 -如果回调根据其参数采取不同的步骤, 则应考虑根据参数导致多长时间执行完该任务。 +一个能确保做到这一点的方法是分析关于你回调代码的 ["计算复杂度"](https://en.wikipedia.org/wiki/Time_complexity)。 +如果你的回调函数在任意的参数输入下执行步骤数量都相同,那么你总能保证每个等待中的请求得到一个公平的执行机会。 +如果回调根据其参数不同所需要的执行步骤数量也不同, 则应深入考虑参数复杂度增长的情况下请求的可能执行时间增长情况。 -例子 1:固定的回调。 +例子 1:固定执行时间的回调。 ```javascript app.get('/constant-time', (req, res) => { @@ -146,36 +147,36 @@ app.get('/countToN2', (req, res) => { ### 你应当注意些什么呢? Node 使用谷歌的 V8 引擎处理 JavaScript,对于大部分操作确实很快。 -对于这个规则的例外是正则表达式以及 JSON 的处理,下面会讨论。 +但有个例外是正则表达式以及 JSON 的处理,下面会讨论。 -但是,对于复杂的任务你应当考虑限定输入范围,拒绝会导致太长时间执行的输入。 -那样的话,即便你的输入相当长而且复杂,因为你限定了输入范围,你也可以确保回调函数在最长的那个参数输入时不会花费比最糟糕的可接受执行时间还要长。 -你甚至可以评估此回调函数最糟糕执行时间,根据你的上下文决定此运行时间是否可以接受。 +但是,对于复杂的任务你应当考虑限定输入范围,拒绝会导致太长执行时间的输入。 +那样的话,即便你的输入相当长而且复杂,因为你限定了输入范围,你也可以确保回调函数的执行时间在你预估的最差情况范围之内。 +然后你可以评估此回调函数的最糟糕执行时间,根据你的业务场景决定此运行时间是否可以接受。 ### 阻塞事件轮询:REDOS -一个灾难性地阻塞事件轮询的普遍方法是使用“脆弱”的 [正则表达式](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)。 +一个灾难性地阻塞事件轮询的常见错误是使用“有漏洞”的 [正则表达式](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)。 -#### 避免脆弱的正则表达式 -一个正则表达式能够根据一定的规则匹配到一个输入的字符串。 -我们通常认为正则表达式匹配要求单通过输入字符串—— `O(n)` 时间,其中 `n` 是输入字符串的长度。 -这在大部分情况下的确是这样。 -不幸的是,在某些情况下,正则表达式匹配随着输入字符串呈指数增长——时间是 `O(2^n)`。 -一个指数的旅行意味着如果引擎需要 `x` 时间行程来确定匹配;如果我们只增加一个字符到输入字符串,它将需要 `2 * x` 的时间。 -由于行程数与所需时间呈线性关系,因此,此评估的效果将是阻止事件循环。 +#### 避免易受攻击的正则表达式 +一个正则表达式是一定的规则去尝试匹配一个输入的字符串。 +我们通常认为正则表达式的匹配需要扫描一次输入字符串—— `O(n)` 时间,其中 `n` 是输入字符串的长度。 +在大部分情况下,一次扫描的确足够。 +不幸的是,在某些情况下,正则表达式匹配扫描随着输入字符串呈指数增长——时间是 `O(2^n)`。 +指数级的扫描时间消耗意味着如果引擎需要 `x` 时间来确定匹配;我们的输入仅仅只增加一个字符,它将需要 `2 * x` 的时间。 +由于扫描的消耗与所需时间呈线性关系,因此,这种正则匹配将阻塞事件循环。 -*易受攻击的正则表达式* 是正则表达式引擎可能需要指数时间的一种, 它使您在“恶意输入” [REDOS](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS)上呈现“邪恶输入”。 -无论您的正则表达式模式是易受攻击的(例如,正则表达式引擎可能会在其上使用指数时间),这实际上是一个很难回答的问题。并且根据您是用 Perl、Python、Ruby、Java、JavaScript 等等来决定的,但这里有在所有这些语言中应用的一些经验法则: +*易受攻击的正则表达式* 是指执行时间随输入指数级增长的情况, 它使您的应用程序在“恶意输入”的情况下面临 [REDOS](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS) (正则表达式拒绝服务攻击)的风险。 +一个正则表达式是否易受攻击的(例如,正则表达式引擎需要指数级的时间复杂度来执行),这实际上是一个很难回答的问题。并且根据您是用 Perl、Python、Ruby、Java、JavaScript 等不同的语言情况也有所不同,但这里有在一些在所有语言里都适用的一些经验法则: -1. 避免嵌套量词,如 `(a+)*`。节点的正则表达式引擎可以快速处理其中的一些,但其它的则是易受攻击的。 -2. 避免带有“或”的重叠情况,如 `(a|a)*`。同样,这些有时是快速的。 -3. 避免使用回溯,如 `(a.*) \1`。没有正则表达式引擎可以保证在线性时间内评估这些。 -4. 如果您正在进行简单的字符串匹配,请使用 `indexOf` 或本地等效项。这将是更便宜,永远不会超过 `O(n)` 的时间。 +1. 避免嵌套量词,如 `(a+)*`。Node 的正则表达式引擎可能可以快速处理某些例子,但某些则可能是易受攻击的。 +2. 避免带有“或”的重叠情况,如 `(a|a)*`。同样,Node 只能保证某些场景下可以快速匹配。 +3. 避免使用回溯,如 `(a.*) \1`。没有正则表达式引擎可以保证在线性时间内执行这种匹配。 +4. 如果您只需要做简单的字符串匹配,请使用 `indexOf` 或其他等价 API。这些是更好的选择,它们永远不会超过 `O(n)` 的时间。 -如果您不确定正则表达式是否易受攻击,请记住:即使对于易受攻击的正则表达式和长输入字符串,该节点通常也不会有 *匹配* 的问题报告。 -当存在不匹配的情况时,指数行为要触发;但 Node 是无法确定的,直到它通过输入字符串尝试了许多路径。 +如果您不确定正则表达式是否易受攻击,请记住:即使对于一个易受攻击的正则表达式和长输入字符串,Node 通常也仍然可以确保结果正确匹配。 +而指数爆炸的场景是出现在用户输入并不匹配正则特征,但是 Node 必须要尝试去执行非常多次的扫描才能最终得出结论。 #### 一个 REDOS 例子 -下面是一个易受攻击的示例。 正则表达式将其服务器暴露给 REDOS: +下面是一个给服务器带来 REDOS 风险的示例示例: ```javascript app.get('/redos-me', (req, res) => { @@ -193,60 +194,61 @@ app.get('/redos-me', (req, res) => { }); ``` -在这个例子中,用脆弱的正则表达式检查一个合法的路径是 (糟糕的!)。 +这个有漏洞的正则例子是一个(糟糕的!)检查 Linux 上合法路径的例子。 它匹配的字符串是以 "/" 分隔的名称序列,如 "/a/b/c"。 -这是危险的,因为它违反了规则 1:它有一个双嵌套的量词。 +这是非常危险的,因为它违反了规则 1:它有一个双重嵌套的量词。 -如果客户端查询的是路径 `///.../\n` (100个“/”后跟正则表达式的“.”不匹配的换行符),则事件循环将有效地永久执行,并阻止事件循环。 -此客户端的 REDOS 攻击会导致所有其它客户端在此正则表达式匹配完成之前得不到机会执行了。 +假设客户端查询的是路径 `///.../\n` (100个“/”后跟一个正则表达式的“.”不匹配的换行符),则事件循环线程将持续执行且无法停止,从而阻止事件循环。 +这类客户端发起的 REDOS 攻击会导致所有其它客户端在此正则表达式匹配完成之前得不到任何执行机会。 -因此,您应该警惕使用复杂的正则表达式来验证用户输入。 +因此,您应该警惕使用复杂的正则表达式来验证用户输入的场景。 -#### 抵制 REDOS 资源 +#### 关于如何抵制 REDOS 的资源 这里提供了你一些工具帮助你检查你的正则表达式是否安全,像: -- [ 安全的正则表达式 ](https://github.com/substack/safe-regex) -- [rxxr2](http://www.cs.bham.ac.uk/~hxt/research/rxxr2/). -但是上述都并不能保证能够捕获全部的脆弱正则表达式。 +- [safe-regex](https://github.com/substack/safe-regex) +- [rxxr2](http://www.cs.bham.ac.uk/~hxt/research/rxxr2/) + +但是上述模块都无法保证能够捕获全部的正则表达式漏洞。 另一个方案是使用一个不同的正则表达式引擎。 你可以使用 [node-re2](https://github.com/uhop/node-re2) 模块,它使用谷歌的超快正则表达式引擎 [RE2](https://github.com/google/re2)。 -但注意,RE2 对 Node 正则表达式不是 100% 兼容,所以如果你想用 node-re2 模块来处理你的正则表达式的话,请检查你的表达式。 -这里尤其值得提醒的是,编译过的正则表达式不被 node-re2 支持。 +但注意,RE2 对 Node 正则表达式不是 100% 兼容,所以如果你想用 node-re2 模块来处理你的正则表达式的话,请检仔细查你的表达式。 +这里尤其值得提醒的是,一些特殊的复杂正则表达式不被 node-re2 支持。 如果你想匹配一些较为“明显”的东西,如网络路径或者是文件路径,请在 [正则表达式库](http://www.regexlib.com) 中寻找到对应例子,或者使用一个 npm 的模块,如 [ip-regex](https://www.npmjs.com/package/ip-regex)。 ### 阻塞事件轮询:Node 的核心模块 -一些 Node 的核心模块有同步的高消费的 API 方法,包含: -- [加密](https://nodejs.org/api/crypto.html) -- [压缩](https://nodejs.org/api/zlib.html) -- [文件系统](https://nodejs.org/api/fs.html) -- [子进程](https://nodejs.org/api/child_process.html) +一些 Node 的核心模块有同步的高开销的 API 方法,包含: +- [crypto 加密](https://nodejs.org/api/crypto.html) +- [zlib 压缩](https://nodejs.org/api/zlib.html) +- [fs 文件系统](https://nodejs.org/api/fs.html) +- [child_process 子进程](https://nodejs.org/api/child_process.html) -这些 API 是高消费的,因为它们包括了非常巨大的计算(体现在加密、压缩上),需要 I/O(体现在文件 I/O),或者两者都有潜在包含(体现在子进程处理上)。这些 API 是为脚本提供方便,并非让你在服务器上下文中使用。如果你在事件循环中使用它们,则需要花费比一般的 JavaScript 更长的执行时间而且阻塞事件轮询。 +这些 API 是高开销的,因为它们包括了非常巨大的计算(如加密、压缩上),需要 I/O(如文件 I/O),或者两者都有潜在包含(如子进程处理)。这些 API 是为脚本提供方便,并非让你在服务器上下文中使用。如果你在事件循环中使用它们,则需要花费比一般的 JavaScript 更长的执行时间从而可能导致阻塞事件轮询。 对于一个服务器而言,*你不应当使用以下同步的 API 函数*: - 加密: - `crypto.randomBytes`(同步版本) - `crypto.randomFillSync` - `crypto.pbkdf2Sync` - - 同时你应当小心地对加密和解密给予大数据输入。 + - 同时你应当非常小心对加密和解密给予大数据输入的情况。 - 压缩: - `zlib.inflateSync` - `zlib.deflateSync` - 文件系统: - - 不能使用同步文件系统方法 API 函数。举个例子,如果你在一个[分布式文件系统](https://en.wikipedia.org/wiki/Clustered_file_system#Distributed_file_systems),像 [NFS](https://en.wikipedia.org/wiki/Network_File_System),则访问次数会发生很大变化。 + - 不能使用同步文件系统方法 API 函数。举个例子,如果你的程序运行于一个[分布式文件系统](https://en.wikipedia.org/wiki/Clustered_file_system#Distributed_file_systems),像 [NFS](https://en.wikipedia.org/wiki/Network_File_System),则访问时间会发生很大变化。 - 子进程: - `child_process.spawnSync` - `child_process.execSync` - `child_process.execFileSync` -此列表在 Node 9 时是合理完成的。 +此列表对于 Node 9 都是有效的。 ### 阻塞事件循环:JSON DOS -`JSON.parse` 以及 `JSON.stringify` 是其它潜在高消费的操作。 -当这些输入的长度是 `O(n)` 时,对于大型的 `n` 消耗的时间惊人的长。 +`JSON.parse` 以及 `JSON.stringify` 是其它潜在高开销的操作。 +这些操作的复杂度是 `O(n)` ,对于大型的 `n` 输入,消耗的时间可能惊人的长。 -如果您的服务器操作 JSON 对象(特别是来自客户端),则应谨慎处理在事件循环上使用的对象或字符串的大小。 +如果您在服务器上操作 JSON 对象(特别是来自客户端的输入),则应谨慎处理在事件循环线程上消费的对象或字符串的大小。 关于 JSON 阻止事件循环的示例:我们创建一个大小为 2^21 的 JSON 的对象,然后用 `JSON.stringify` 序列化它;在此字符串上运行 `indexOf` 函数,然后使用 JSON.parse 解析它。 `JSON.stringify` 字符串为 50MB。字符串化对象耗时 0.7 秒,对这个 50MB 的字符串使用 indexOf 函数耗时 0.03 秒,用了 1.3 秒解析字符串。 @@ -257,7 +259,7 @@ var niter = 20; var before, res, took; for (var i = 0; i < len; i++) { - obj = { obj1: obj, obj2: obj }; // Doubles in size each iter + obj = { obj1: obj, obj2: obj }; // 每个循环里面将对象 size 加倍 } before = process.hrtime(); @@ -277,18 +279,18 @@ console.log('JSON.parse took ' + took); ``` 有一些 npm 的模块提供了异步的 JSON API 函数,参考: -- [JSONStream](https://www.npmjs.com/package/JSONStream),有关于流的函数。 -- [Big-Friendly JSON](https://github.com/philbooth/bfj),使用下面概述的事件循环模式,具有流 api 以及标准 JSON api 的异步版本。 +- [JSONStream](https://www.npmjs.com/package/JSONStream),有流式操作的 API。 +- [Big-Friendly JSON](https://github.com/philbooth/bfj),有流式 API 和使用下文所概述的任务拆分思想的异步 JSON 标准 API。 -### 复杂的计算而不阻塞的事件循环 +### 不要让复杂的计算阻塞事件循环 假设你想在 JavaScript 处理一个复杂的计算,而又不想阻塞事件循环。 -你有两种选择:分区或卸载。 +你有两种选择:任务拆分或任务分流。 -#### 分区 -你可以把你的复杂计算 *拆分开*,然后让每个计算分别运行在事件循环中,不过你要定期地让其它一些等待的事件有机会执行。 -在 JavaScript 中,存储一个在闭包中的持续任务很容易,请看例子 2。 +#### 任务拆分 +你可以把你的复杂计算 *拆分开*,然后让每个计算分别运行在事件循环中,不过你要定期地让其它一些等待的事件执行就会。 +在 JavaScript 中,用闭包很容易实现保存执行的上下文,请看如下的 2 个例子。 -举个例子,假设你想计算 `1` to `n` 的平均值。 +举个例子,假设你想计算 `1` 到 `n` 的平均值。 例子1:不分区算平均数,开销是 `O(n)` ```javascript @@ -298,7 +300,7 @@ let avg = sum / n; console.log('avg: ' + avg); ``` -例子2:分区算平均值,每个 `n` 开销为 `O(1)`。 +例子2:分区算平均值,每个 `n` 的异步步骤开销为 `O(1)`。 ```javascript function asyncAvg(n, avgCB) { // Save ongoing sum in JS closure. @@ -327,146 +329,147 @@ asyncAvg(n, function(avg){ }); ``` -你可以把此规则应用到数组迭代和其它等方面。 +这个原则也可以应用到数组迭代和其它类似场景。 -#### 卸载 -如果你需要做更复杂的任务,分区不是一个好选项。这是因为分区只能在事件循环中使用,并且你不会受益于多个核心几乎肯定可以在您的机器上。 -*请记住,时间轮询只是协调客户端的请求,而不是完成它们各自的任务。* -对一个复杂的任务,把它从事件循环中转义到工作池上。 +#### 任务分流 +如果你需要做更复杂的任务,拆分可能也不是一个好选项。这是因为拆分之后任务仍然在事件循环线程中执行,并且你无法利用机器的多核硬件能力。 +*请记住,事件循环线程只负责协调客户端的请求,而不是独自执行完所有任务。* +对一个复杂的任务,最好把它从事件循环线程转移到工作线程池上。 -##### 如何进行卸载? -对目标工作池而言,在此你有二个选择决定到底选择哪个来卸载工作。 -1. 你可以通过开发 [C++ 插件](https://nodejs.org/api/addons.html) 的方式使用内置的 Node 工作池。稍早之前的 Node 版本,通过使用 [NAN](https://github.com/nodejs/nan) 的方式编译你的 C++ 插件,在新版的 Node 上使用 [N-API](https://nodejs.org/api/n-api.html)。 [node-webworker-threads](https://www.npmjs.com/package/webworker-threads) 提供了你一个仅用 JavaScript 就可以访问 Node 的工作池的方式。 -2. 您可以创建和管理自己专用于计算的工作池,而不是节点的 I/O 主题工作池。最直接的方法就是使用 [子进程](https://nodejs.org/api/child_process.html) 或者是 [集群](https://nodejs.org/api/cluster.html)。 +##### 如何进行任务分流? +你有两种方式将任务转移到工作线程池执行。 +1. 你可以通过开发 [C++ 插件](https://nodejs.org/api/addons.html) 的方式使用内置的 Node 工作池。稍早之前的 Node 版本,通过使用 [NAN](https://github.com/nodejs/nan) 的方式编译你的 C++ 插件,在新版的 Node 上使用 [N-API](https://nodejs.org/api/n-api.html)。 [node-webworker-threads](https://www.npmjs.com/package/webworker-threads) 提供了一个仅用 JavaScript 就可以访问 Node 的工作池的方式。 +2. 您可以创建和管理自己专用于计算的工作线程池,而不是使用 Node 自带的负责的 I/O 的工作线程池。最直接的方法就是使用 [Child Process](https://nodejs.org/api/child_process.html) 或者是 [cluster](https://nodejs.org/api/cluster.html)。 -你 *不* 应该为任何请求都创建一个[ 子进程 ](https://nodejs.org/api/child_process.html)。 -你可以快速地接受客户端的全部请求,而不是创建和管理这些子进程,否则你的服务器就变成了一个 [Fork 炸弹](https://en.wikipedia.org/wiki/Fork_bomb)。 +你 *不* 应该直接为每个请求都创建一个[ 子进程 ](https://nodejs.org/api/child_process.html)。 +因为客户端请求的频率可能远远高于你的服务器能创建和管理子进程的频率,这种情况你的服务器就变成了一个 [Fork 炸弹](https://en.wikipedia.org/wiki/Fork_bomb)。 -##### 卸载方法的缺陷 -卸载方法的缺点是它以 *通信成本* 的形式招致开销。 -仅允许事件循环查看应用程序的“命名空间”(JavaScript 状态)。 -从工作线程中,您不能在事件循环的命名空间中操作 JavaScript 对象。 -相反地,您必须序列化和反序列化要共享的任何对象。 -然后,该工作线程可以对其自己的这些对象的副本进行操作,并将修改后的对象(或“补丁”) 返回到事件循环。 +##### 转移到工作线程池的缺陷 +这种方法的缺点是它增大了 *通信开销* 。 +因为 Node 仅允许事件循环线程去查访问应用程序的“命名空间”(保存着 JavaScript 状态)。 +在工作线程中是无法操作事件循环线程的命名空间中的 JavaScript 对象的。 +因此,您必须序列化和反序列化任何要在线程间共享的对象。 +然后,工作线程可以对属于自己的这些对象的副本进行操作,并将修改后的对象(或“补丁”) 返回到事件循环线程。 有关序列化问题,请参阅 JSON 文档部分。 -##### 一些关于卸载的建议 +##### 一些关于分流的建议 您可能希望区分 CPU 密集型和 I/O 密集型任务,因为它们具有明显不同的特性。 -CPU 密集型任务只有在计划工作时才会取得进展,并且必须将该工作人员安排到您的计算机的 [逻辑核心](https://nodejs.org/api/os.html#os_os_cpus)中。 +CPU 密集型任务只有在该 Worker 线程被调度到时候才得到执行机会,并且必须将该任务分配到机器的某一个 [逻辑核心](https://nodejs.org/api/os.html#os_os_cpus)中。 -如果你有 4 个逻辑核心和 5 名工作线程,这些工作线程中的一个不能取得进展。 -因此,您要为该工作线程支付开销(内存和计划成本),并且无法弥补。 +如果你的机器有 4 个逻辑核心和 5 个工作线程,那这些工作线程中的某一个则无法得到执行。 +因此,您实质上只是在为该工作线程白白支付开销(内存和调度开销),却无法得到任何返回。 -I/O 密集型任务包括查询外部服务提供程序(DNS、文件系统等)并等待其响应。 -当具有 I/O 密集型任务的工作线程正在等待其响应时,它没有其它工作可做,并且可以由操作系统取消计划,从而使另一个工作人员有机会提交他们的请求。 -因此,*I/O 密集型任务即使关联线程没有运行,也将取得进展*。 -像数据库和文件系统这样的外部服务提供程序经过高度优化,可以同时处理许多挂起的请求。 -例如,文件系统将检查一大组挂起的写入和读取请求,以合并冲突更新并以最佳顺序检索文件(请参阅 [这些幻灯片](http://researcher.ibm.com/researcher/files/il-AVISHAY/01-block_io-v1.3.pdf))。 +I/O 密集型任务通常包括查询外部服务提供程序(DNS、文件系统等)并等待其响应。 +当 I/O 密集型任务的工作线程正在等待其响应时,它没有其它工作可做,并且可以被操作系统重新调度,从而使另一个 Worker 有机会提交它的任务。 +因此,*即使关联的线程并没有被保持,I/O 密集型任务也可以持续运行*。 +像数据库和文件系统这样的外部服务提供程序已经经过高度优化,可以同时处理许多并发的请求。 +例如,文件系统会检查一大组并发等待的写入和读取请求,以合并冲突更新并以最佳顺序读取文件(请参阅 [这些幻灯片](http://researcher.ibm.com/researcher/files/il-AVISHAY/01-block_io-v1.3.pdf))。 -如果只依赖一个工作池(例如 Node 工作池),则 CPU 绑定和 I/O 绑定的工作由于不同特性可能会损害应用程序的性能。 +如果只依赖一个工作线程池(例如 Node 工作池),则 CPU 密集和 I/O 密集的任务的不同特效性可能会损害应用程序的性能。 -因此,您可能希望维护单独的计算工作池。 +因此,您可能希望一个维护单独的计算工作线程池。 -#### 卸载:总结 -对于简单的任务:比如遍历任意长数组的元素,分区可能是一个很好的选择。 -如果计算更加复杂,则卸载是一种更好的方法:通信成本(即在事件循环和工作池之间传递序列化对象的开销)被使用多个内核的好处抵消。 -但是,如果服务器严重依赖复杂的计算,则应该考虑 Node 是否真的很适合?Node 擅长于 I/O 绑定工作,但对于昂贵的计算,它可能不是最好的选择。 +#### 分流:总结 +对于简单的任务:比如遍历任意长数组的元素,拆分可能是一个很好的选择。 +如果计算更加复杂,则分流是一种更好的方法:通信成本(即在事件循环线程和工作线程之间传递序列化对象的开销)被使用多个物理内核的好处抵消。 +但是,如果你的服务器严重依赖复杂的计算,则应该重新考虑 Node 是否真的很适合该场景?Node 擅长于 I/O 密集型任务,但对于昂贵的计算,它可能不是最好的选择。 -如果采用卸载方法,请参阅“不阻塞工作池”一节。 +如果采用分流方法,请参阅“不阻塞工作线程池”一节。 -## 不要阻塞你的工作池 -Node 由 `k` 个工作线程组成了工作池。 -如果您使用上面讨论过的卸载范式,则可能有一个单独的计算工作池,同样的原则也适用。 -在这两种情况下,让我们假设 `k` 比您可能同时处理的客户端数量要小得多。 -这与节点的“一个线程为许多客户端”的哲学是一致的,这是它的可伸缩性秘诀。 +## 不要阻塞你的工作线程池 +Node 由 `k` 个工作线程组成了工作线程池。 +如果您使用上面讨论过的任务分流思想,则可能有一个单独的计算工作池,它也适用于该原则。 +在这两种情况下,让我们假设 `k` 比您可能需要同时处理的客户端请求数量要小得多。 +这与 Node 的“一个线程处理许多客户端连接”的哲学是一致的,这也是它的可伸缩性秘诀。 -如上所述:每个工作线程完成其当前任务,然后再继续执行工作池队列中的下一项。 +正如在上文中讨论的,每个工作线程必须完成其当前任务,才能继续执行工作线程池队列中的下一项。 -现在,处理客户请求所需的任务成本将发生变化。 -有些任务可以快速完成(例如读取短文件或缓存文档,或者生成少量的随机字节),而另一些则需要更长的时间(即读取较大或缓存的文件,或生成更多的随机字节)。 -您的目标应该是 *最小化任务时间的变化*,并且您应该使用 *任务分区* 来完成此工作。 +那么,处理客户请求所需的任务成本将会在不同的客户端输入场景下发生很大变化。 +有些任务可以快速完成(例如读取小文件或缓存文档,或者生成少量的随机字节),而另一些则需要更长的时间(例如读取较大或未缓存的文件,或生成更多的随机字节)。 +您的目标应该是使用 *任务拆分* 来 *尽量缩小不同请求任务执行时间的动态变化*, ### 最小化任务时间的变化 -如果工作线程的当前任务比其它任务昂贵得多,则无法处理其它未决任务。 -换言之,*每个相对长的任务有效地减少了工作池的大小,直到完成*。 +如果工作线程的当前任务比其它任务开销大很多,则他无法处理其它等待中任务。 +换言之,*每个相对长的任务会直接减少了工作线程池的可用线程数量,直到它的任务完成*。 这是不可取的。因为从某种程度上说,工作池中的工作线程越多,工作池吞吐量(任务/秒)就越大,因此服务器吞吐量(客户端请求/秒)就越大。 -一个具有相对昂贵任务的客户端将减少工作池的吞吐量,从而降低服务器的吞吐量。 +一个具有相对昂贵开销任务的客户端请求将减少工作线程池整体的吞吐量,从而降低服务器的吞吐量。 -为避免这种情况,应尽量减少提交给工作池的任务长度的变化。 -虽然将 I/O 请求(DB、FS 等)访问的外部系统视为黑盒是适当的;但您应该知道这些 I/O 请求的相对成本,并应避免提交您可能期望特别长的请求。 +为避免这种情况,应尽量减少提交给工作池的不同任务在执行时间上的变化。 +虽然将 I/O 请求(DB、FS 等)访问的外部系统视为黑盒在某种角度是适当的;但您应该知道这些 I/O 请求的相对成本,并应避免提交您预估可能特别耗时的任务。 -两个示例应说明任务时间可能发生的变化。 +下面的两个示例应该能说明任务时间可能发生的变化。 -#### 变体示例: 长时间运行的文件系统读取 -假设您的服务器必须读取文件以处理某些客户端请求。 +#### 动态执行时间示例: 长时间运行的文件系统读取 +假设您的服务器必须读取文件来处理某些客户端请求。 在了解 Node 的 [文件系统](https://nodejs.org/api/fs.html) 的 API 之后,您选择使用 `fs.readFile()` 进行简单操作。 -但是,`fs.readFile()` 是([当前](https://github.com/nodejs/node/pull/17054))未分区的:它提交一个 `fs.read()` 任务来跨越整个文件。 -如果您为某些用户阅读较短的文件,并为其它人读取较长的文件,`fs.readFile()` 可能会在任务长度上引入显著的变化,从而损害工作池吞吐量。 +但是,`fs.readFile()` 是([当前](https://github.com/nodejs/node/pull/17054))未拆分任务的:它提交一个 `fs.read()` 任务来读取整个文件。 +如果您为某些用户读取较短的文件,并为其它人读取较长的文件,`fs.readFile()` 可能会在任务长度上引入显著的变化,从而损害工作线程池吞吐量。 -对于最坏的情况,假设攻击者可以说服您的服务器读取 *任意* 文件(这是一个 [目录遍历漏洞](https://www.owasp.org/index.php/Path_Traversal))。 +对于最坏的情况,假设攻击者可以促使您的服务器读取 *任意* 文件(这是一个 [目录遍历漏洞](https://www.owasp.org/index.php/Path_Traversal))。 如果您的服务器运行的是 Linux,攻击者可以命名一个非常慢的文件:[`/dev/random`](http://man7.org/linux/man-pages/man4/random.4.html)。 对于所有实际的目的,`/dev/random` 是无限缓慢的;每个工作线程都被要求读取 `/dev/random`,这样下去将永远不会完成这项任务。 -然后,攻击者提交 `k` 个请求,其中一个用于每个工作线程,而使用该工作池的其它客户端请求也不会取得进展。 +然后,攻击者提交 `k` 个请求,每一个被分配给一个工作线程,则其它需要使用工作线程的客户端请求将得不到执行机会。 -#### 变体示例: 长时间运行的加密操作 -假设您的服务器使用 [`crypto.randomBytes()`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) 来生成加密的安全随机字节。`crypto.randomBytes()` 是不分区的:它创建单个 `randomBytes()` 任务,以生成您请求的字节数。 -如果为某些用户创建的字节数较少,并且其它字节数较多;则 `crypto.randomBytes()` 是任务长度变化的另一个来源。 +#### 动态执行时间示例: 长时间运行的加密操作 +假设您的服务器使用 [`crypto.randomBytes()`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) 来生成密码学上安全的随机字节。`crypto.randomBytes()` 是不拆分任务的:它创建单个 `randomBytes()` 任务,以生成您请求的字节数。 +如果为某些用户创建的字节数较少,并且其它请求创建字节数较多;则 `crypto.randomBytes()` 是任务长度变化的另一个来源。 -### 任务分区 +### 任务拆分 具有可变时间成本的任务可能会损害工作池的吞吐量。 -为了尽量减少任务时间的变化,应尽可能将每个任务 *划分* 为类似成本的子任务。 +为了尽量减少任务时间的丛台变化,应尽可能将每个任务 *划分* 为开销接近一致的子任务。 当每个子任务完成时,它应该提交下一个子任务;并且当最终的子任务完成时,它应该通知提交者。 -要继续使用 `fs.readFile()` 示例,应改用 `fs.read()`(手动分区)或 `ReadStream`(自动分区)。 +继续使用 `fs.readFile()` 的示例,更好的方案是使用 `fs.read()`(手动拆分)或 `ReadStream`(自动拆分)。 -同样的原理也适用于 CPU 绑定任务; `asyncAvg` 示例可能不适用于事件循环,但它非常适合于工作池。 +同样的原理也适用于 CPU 密集型任务; `asyncAvg` 示例可能不适用于事件循环,但它非常适合于工作行哦啊线程池。 -将任务划分为子任务时,较短的任务将扩展为少量的子任务,而更长的任务将扩展到更多的子任务。 -在较长任务的每个子任务之间,分配给它的工作线程可以从另一个更短的任务中处理子任务,从而提高工作池的总体任务吞吐量。 +将任务拆分为子任务时,较短的任务将拆分为少量的子任务,而更长的任务将拆分为更多的子任务。 +在较长任务的每个子任务之间,分配给它的工作线程可以调度执行另一个更短的任务拆分出来的子任务,从而提高工作池的总体任务吞吐量。 -请注意:完成的子任务数对于辅助池的吞吐量不是一个有用的度量。 +请注意:完成的子任务数对于工作线程池的吞吐量不是一个有用的度量指标。 相反,请关注完成的 *任务* 数。 -### 避免任务分区 -记得任务分区的目的是尽量减少任务时间的变化。 -如果可以区分较短的任务和较长的任务(例如,求和数组与排序数组),则可以为每个任务类创建一个工作池。 -将较短的任务和更长的任务路由到单独的工作池是减少任务时间变化的另一种方法。 +### 避免任务拆分 +我们需要明确任务拆分的目的是尽量减少任务执行时间的动态变化。 +但是如果你可以人工区分较短的任务和较长的任务(例如,对数组求和或排序),则可以手动为每个类型的任务创建一个工作池。 +将较短的任务和更长的任务分别路由到各自的工作线程池,也是减少任务时间动态变化的另一种方法。 -为支持此方法,分区任务会招致开销(创建工作池任务表示法和操作工作池队列的成本),而避免分区会为您节省额外旅行到工作池的成本。 +建议这种方案的原因是,做任务拆分会导致额外的开销(创建工作线程,表示和操作线程池任务队列),而避免拆分会为您节省这些外成本,同时也会避免你在拆分任务的时候犯错误。 -这种方法的缺点是:所有这些工作池中的工作线程都将承担空间和时间开销,并将在 CPU 时间内相互竞争。 -请记住:每个 CPU 绑定任务只在计划时才会取得进展。 -因此,您应该只在仔细分析后才考虑此方法。 +这种方案的缺点是:所有这些工作池中的工作线程都将消耗空间和时间开销,并将相互竞争 CPU 时间片。 +请记住:每个 CPU 密集任务只在它被调度到的时候才会得到执行。 +因此,您应该再仔细分析后才考虑此方案。 -### 工作池:总结 -无论您只使用节点工作池还是维护单独的工作池,都应优化池的任务吞吐量。 +### 工作线程池:总结 +无论您只使用 Node 工作线程池还是维护单独的工作线程池,都应着力优化线程池的任务吞吐量。 -为此,请使用任务分区最小化任务时间的变化。 +为此,请使用任务拆分最小化任务执行时间的动态变化范围。 ## 使用 npm 模块的风险 -虽然 Node 核心模块为各种应用程序提供了构建块,但有时还需要更多的内容。Node 的开发人员从 [npm 生态系统](https://www.npmjs.com/) 中获益良多,有成百上千个模块提供功能以加速您的开发过程。 +虽然 Node 核心模块为各种需求提供了基础支持,但有时还需要更多的功能。Node 的开发人员从 [npm 生态系统](https://www.npmjs.com/) 中获益良多,有成百上千个模块可以为你的应用开发提效。 + +但是,请记住,这些模块中的大多数是由第三方开发人员编写的;它们通常只能保证尽力做到很好。使用 npm 模块的开发人员应该关注如下两件事,尽管后者经常被遗忘。 +1. 它是否拥有优秀的 API 设计? +2. 它的 API 可能会阻塞事件循环线程或工作线程吗? -但是,请记住,这些模块中的大多数是由第三方开发人员编写的;通常只用最努力的保证发布。使用 npm 模块的开发人员应该关注两件事,尽管后者经常被遗忘。 -1. 它是否尊重其 API? -2. 它的 api 可能会阻止事件循环或工作线程吗? -许多模块对它们 API 开销没有产生任何影响,这对社区不利。 +许多模块对它们 API 的开销没有任何考虑,这对社区使用者是不利的。 -对于简单的 API,您可以估计 API 的成本;字符串操作的成本并不难捉摸。 -但在许多情况下却不清楚 API 可能花费多少。 +对于简单的 API,您可以估计 API 的成本;如字符串操作的成本并不难预估。 +但在许多情况下却很难预估 API 可能的开销。 -*如果您调用的 API 可能会做一些昂贵的事情,请加倍检查成本;要求开发人员记录它,或者自己检查源代码(并提交一个公关记录成本)。* +*如果您调用的 API 可能会做一些昂贵的事情,请着重检查成本;或者请求开发人员给出相关文档,或者自己检查源代码(并提交一个 PR 说明开销)。* -请记住:即使 API 是异步的,您也不知道它可能在每个分区的工作线程或事件循环上花费多少时间。 -例如,假设在上面给出的 `asyncAvg` 示例中,每个对 helper 函数的调用都概括了数字的一半而不是其中的一个。 -然后这个函数仍然是异步的,但每个分区的成本将是 `O(n)` 而不是 `O(1)`, 使它更不安全地使用任意值 `n`。 +请记住:即使 API 是异步的,您也可能无法预估它的每个拆分的子任务需要在工作线程或事件循环线程上花费多少时间。 +例如,假设在上面给出的 `asyncAvg` 示例中,每个对 helper 函数的调用都累加一半的数字而不只是其中的一个。 +那么这个函数仍然是异步的,但每个子任务的成本将是 `O(n)` 而不是 `O(1)`, 就使得对于任意的输入 `n` 不再那么安全。 -## 最后总结 -Node 有两种类型的线程:一个事件循环和一个由 `k` 个工作线程。 -事件循环负责 JavaScript 回调和非阻塞 I/O,工作人员执行与 C++ 代码对应的、完成异步请求的任务,包括阻塞 I/O 和 CPU 密集型工作。 -两种类型的线程一次只处理一个活动。 -如果任何回调或任务需要很长时间,则运行它的线程将变为 *阻止*。 -如果应用程序阻止回调或任务,这可能会导致吞吐量下降(客户端/秒),并且在最坏情况下完全拒绝服务。 +## 总结 +Node 有两种类型的线程:一个事件循环线程和 `k` 个工作线程。 +事件循环负责 JavaScript 回调和非阻塞 I/O,工作线程执行与 C++ 代码对应的、完成异步请求的任务,包括阻塞 I/O 和 CPU 密集型工作。 +着两种类型的线程一次都只能处理一个活动。 +如果任意一个回调或任务需要很长时间,则运行它的线程将被 *阻塞*。 +如果你的应用程序发起阻塞的回调或任务,在好的情况下这可能只会导致吞吐量下降(客户端/秒),而在最坏情况下可能会导致完全拒绝服务。 -要编写高吞吐量、防 DoS 攻击的 web 服务,您必须确保在良性和恶意输入的情况下,您的事件循环和您的工作线程都不会阻塞。 +要编写高吞吐量、防 DoS 攻击的 web 服务,您必须确保不管在良性或恶意输入的情况下,您的事件循环线程和您的工作线程都不会阻塞。