在 Node-RED 1.0 中,我们将消息在节点之间传递的方式从同步更改为异步。在某些情况下,这将改变消息在流中处理的相对顺序。本文将解释我们所说的同步和异步是什么意思,我们为什么要做这个改变,以及它将产生什么影响。
Node.js 事件循环
要理解消息是如何通过 Node-RED 流传递的,我们首先需要稍微绕道了解一下 Node.js 是如何工作的。
由于 JavaScript 是一种单线程语言,它一次只能做一件事。但有时它需要做一些耗时的事情,比如发出一个 HTTP 请求或向文件中写入内容。如果它在唯一的线程上执行这些工作,那么它将阻塞其他任何事情的发生,性能将会非常糟糕。
取而代之的是,Node.js 将这类操作传递给底层的多线程操作系统,并注册一个回调函数,该函数将在操作完成时被调用。这就是 Node.js 核心——事件循环 的思想。
简单来说,你可以把它想象成一个工作队列。每次事件循环运行时,它会取出下一个工作项并执行它,这反过来又可能导致新的事件被添加到队列中。
同步与异步
当我们谈论同步和异步代码时,你可以这样理解:
- 同步代码都在事件循环的单次传递中运行。
- 异步代码始于事件循环中的一个工作项,该工作项会向事件循环中添加另一个工作项,以便在未来的传递中运行。
需要理解的关键是,对于异步代码,一旦它向事件循环添加了更多的工作并且其当前的工作已经“完成”,其他的工作项就可以运行了。
同步消息传递
从 Node-RED 诞生之初,我们就一直在节点之间使用同步消息传递。
这意味着当一个节点调用 node.send(msg)
时,该调用会传递到下一个节点的 input
事件处理程序,该处理程序执行其工作并调用下一个节点的事件处理程序,依此类推。如果每个节点的事件处理程序都是纯粹的同步代码,那么一条消息将在事件循环的单次传递中贯穿整个流。
如果其中一个节点包含异步代码,例如 HTTP 请求节点,那么当前的传递在该节点结束,下一个工作项就可以开始。在这种情况下,如果第二条消息的异步工作在第一条消息之前完成,那么第二条消息也有可能超过第一条。
流的分支
当流发生分支时,对于同步流,每个分支将依次完成。
当您在流的每个点添加 Debug 节点时,这确实会导致一种稍微违反直觉的行为。在下图中,请注意消息到达 Debug 节点的顺序。
改为异步消息传递
在 Node-RED 1.0 中,我们将消息传递更改为异步。这意味着当一个节点调用 node.send(msg)
时,调用下一个节点的 input
事件处理程序的工作会被放入队列,以便在事件循环的后续传递中调用。
对于那些更熟悉事件循环的人来说,我们使用 setImmediate()
,所以它们实际上会在当前事件循环迭代的“检查”阶段被调用。
回顾我们开始时那个单分支、完全同步的流,现在消息将以同等进度通过流。
当一个流发生分支时,分支将“并行”地被评估。
这也意味着在每个点都有 Debug 节点的流,将按预期的顺序记录消息。
为什么需要这个改变?
将消息传递异步化是出于多种原因的需要。
可插拔的消息路由
在 1.0 版本之后,路线图上的一个功能是能够将自定义代码插入到消息路由路径中。该自定义代码可能需要进行异步工作——例如在分布式 Node-RED 环境中通过网络发送消息。
节点超时
我们正在研究运行时如何更好地监控通过流的消息,并提供一种标准方法来超时任何耗时过长的节点。
在当前的同步模型下,一个节点处理消息所需的时间是运行其自身代码的时间,加上每个后续节点处理其传递的消息所需的时间。
再次看一下这个分支流,假设我们希望每个节点处理消息的时间不超过 5 秒。
如果一切都是同步的,那么第二个节点直到黄色和红色的消息都到达它们的 Debug 节点后才算“完成”。如果每个节点处理消息需要 2 秒钟,那么第二个节点处理其消息将需要 10 秒钟,并会超时,即使没有任何单个节点花费的时间超过 5 秒。
通过异步消息传递,我们可以在节点为后续节点排队工作后立即停止计时。
在 1.0 之后还有更多工作要做来构建这个功能,但转向异步是关键的第一步。
我们很快会发布另一篇博客文章,介绍 1.0 中节点消息传递 API 的变化,以进一步支持这种超时行为。
更好的 I/O 调度
如果你还记得事件循环的目的是允许 Node.js 运行时在后台执行 I/O,并在有事情要做时回调。
但这些回调只有在事件循环能够正常进行时才能发生。如果你有一大段同步代码,那么你就在阻止这些回调被调用。这里存在一个权衡。纯同步代码会更快,但它会饿死事件循环。
通过将一个大的同步流分割成更小的异步块,它允许 Node.js 运行时更好地调度运行时中的所有其他活动。
这个改变会破坏我的流吗?
对于大多数流来说,这个改变不会以任何方式改变它们的行为。我们一直强调,一旦流发生分支,就不应该对顺序做任何假设。
话虽如此,肯定会有一些流利用了观察到的顺序并做出了一些这样的假设。因此,如果您知道您的流做了这样的假设,在升级时应格外小心。
这就是为什么我们把这个改变作为 1.0 版本的一部分。我们一直努力确保 Node-RED 在版本之间保持向后兼容,但有时我们不得不做出可能产生影响的改变。我们不会轻率地做出这些改变,而且我们只能在主要版本发布时这样做。
暂时保持同步
鉴于这个改变可能会改变*某些*流的行为,我们引入了一个新的设置,可以恢复同步传递模式。
runtimeSyncDelivery: true
考虑到路线图上需要异步传递的功能,这个设置*不是*一个长期的解决方案。可以认为它的使用是即刻被废弃的。它仅用作一个权宜之计,以便在受影响的流更新以处理新的异步模式之前,允许它们升级到 1.0。