Node.JS无限并发/ TCP流反压

13
据我理解,Node的事件驱动IO模型的一个后果是,在您连接了接收事件处理程序(或以其他方式开始监听数据)后,无法告诉正在通过TCP套接字接收数据的Node进程阻塞。如果接收方无法快速处理传入的数据,则可能会产生“无限并发”,Node在底层会继续尽可能快地从套接字读取数据,调度新的数据事件到事件循环中,而不是在套接字上阻塞,直到进程最终耗尽内存并停止运行。接收方无法要求node减慢其读取速度,否则TCP内置的流量控制机制将会起作用,并提示发送方需要降低速度。
首先,我所描述的是否准确?是否有我遗漏的东西可以让node避免这种情况?
Node Streams自动处理背压(backpressure)是其中一个受热捧的特性。据我所知,可写流(tcp套接字)判断自己是否需要放慢速度的唯一方式是查看socket.bufferSize(表示已写入但尚未发送到套接字的数据量)。鉴于Node在接收端始终以尽可能快的速度读取数据,这只能表明发送方和接收方之间存在缓慢的网络连接,而不能表明接收方无法跟上发送方的速度。
其次,在这种情况下,Node Streams自动背压是否能够处理无法跟上速度的接收方?
似乎这个问题也会影响通过Websockets接收数据的浏览器,原因类似于Websockets API没有提供一种机制来告诉浏览器放慢对套接字的读取速度。
那么,解决这个问题的唯一方法是Node(和使用Websockets的浏览器)在应用程序级别实现手动流控制机制,明确告诉发送进程要放慢速度吗?

7
请查看流文档,并注意highWaterMark选项。这是如何处理背压的:当Node填满其输入缓冲区时,它会减慢读取速度;如果输出缓冲区已满,则还将停止发送数据。 Node会以尽可能快的速度读取...直到填满其缓冲区。在缓冲区再次为空之前,它不会读取任何更多的数据。我建议研究此示例的“流缓冲区长度”(Stream Buffer Length)。 - Paul Mougel
3
“_the process eventually runs out of memory and dies_”:这种情况不会发生,因为例如你实现一个转换流,在处理完一个数据块后必须调用回调函数来表示已经完成处理。只要回调函数没有被调用,Node 就不会读取更多的数据(前提是达到了 highWaterMark)。 - Paul Mougel
除非有人实现了他们的流子类错误; )。你的评论对我来说已经足够作为一个答案了。 - B T
1个回答

8
为回答你的第一个问题,我认为你的理解并不准确——至少在流之间传输数据时不是这样的。实际上,如果你阅读 pipe() 函数 的文档,你会发现它明确表示它会自动管理流量,以便“目标不被快速可读流淹没”。 pipe() 的底层实现将为你处理所有繁重的任务。输入流(一个可读流)将继续发出数据事件,直到输出流(一个可写流)已满。顺便说一句,如果我记得正确的话,在你尝试写入当前无法处理的数据时,流将返回 false。此时,pipe 将暂停可读流,这将防止其发出进一步的数据事件。因此,事件循环不会填满和耗尽内存,也不会发出被简单丢失的事件。相反,可读流会保持暂停状态,直到可写流发出排空事件。此时,pipe 将恢复可读流。
这个“秘密酱料”是将一个流导入另一个流,它会自动为你管理背压。这可以回答你的第二个问题,即 Node 可以通过简单地管道化流来自动管理背压。
最后,除非你正在从头编写新流,否则实际上没有必要手动实现它,因为它已经为你提供了:)
处理所有这些并不容易,正如在 Node 博客文章中所承认的那样,该文章介绍了 Node 中的streams2 API。这是一个很好的资源,肯定提供了比我这里更多的信息。然而,有一个不完全明显的小细节你应该知道,来自于文档这里和出于向后兼容性的原因:

如果你附加了一个数据事件监听器,那么它将切换流到流动模式,并且数据将在可用时立即传递给你的处理程序。

请注意,为了观察流中的某些内容而附加数据事件监听器将从根本上改变流的处理方式。问我怎么知道


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