支持基于路由器的消息总线¶
https://blueprints.launchpad.net/oslo?searchtext=amqp-dispatch-router
本规范提出对现有的 AMQP 1.0 驱动程序进行更改,以允许通过无代理的 AMQP 1.0 消息总线传输消息。原始驱动程序的蓝图可以在这里找到 [1],原始架构在 [2] 中描述。本规范描述的更改是对原始设计的重构。修改后的驱动程序仍将遵守 AMQP 1.0 协议标准,以便保持对特定消息总线实现的无关性。因此,将保留对现有的基于 AMQP 1.0 代理的消息传递后端的支持。
问题描述¶
在开发原始驱动程序时,Qpid C++ Broker 是唯一支持 AMQP 1.0 协议的消息传递服务。因此,原始驱动程序仅针对基于代理的消息总线部署进行设计和测试。
AMQP 1.0 标准与它的前身不同,不再需要代理中间件。这使得非基于代理的 AMQP 1.0 消息传递系统成为可能。一种这样的无代理消息传递解决方案是消息路由网格。该消息传递解决方案由多个互连的消息路由器组成,它们使用最短路径算法将消息路由到目的地。把它想象成一个互联网路由器,但作用于消息地址级别而不是 IP 地址级别。
消息路由器不是代理。它与代理的不同之处在于以下几个重要方面
它与其他路由器一起部署在网格拓扑中
路由器是无队列的
它从不接受消息的所有权
任何确认都是端到端的
可用性通过冗余提供,而不是集群
它旨在提供通信路径的可用性,而不是单个消息的可用性
它允许其他消息服务插入网格
这包括代理
与代理不同,由单个路由器组成的消息网格是次优的。路由器被设计为作为互连路由器节点的网格部署。每个路由器都与其他一个或多个对等路由器建立专用的 TCP 连接。客户端通过使用单个 TCP 连接连接到其中一个路由器来连接到网格。当客户端创建订阅时,用于订阅的地址会传播到本地连接的路由器之外。网格中的所有路由器都会意识到该订阅并计算从每个路由器到消耗客户端的最佳路径。当另一个客户端发布到该订阅时,路由器会沿着适当的路径将消息转发到订阅者,订阅者消耗该消息。
以下是四路由器网格的示意图
+-----------+ +-----------+
|RPC Server1| |RPC Caller1|
+-----+-----+ +----+------+
| |
| |
+---v----+ +---v----+
|Router A+#########+Router B|
+---+----+ +----+---+ <------+
# ## ## # |
# ## ## # +---+--+
# ## # |Broker|
# ## ## # +------+
# ## ## #
+---+----+ +-----+--+ <------+
|Router C+#########+Router D| +-----+-----+
+---+----+ +-----+--+ |RPC Caller2|
^ ^ +-----------+
| |
| |
+------+----+ ++----------+
|RPC Caller3| |RPC Server2|
+-----------+ +-----------+
‘#’ 表示路由器 A、B、C 和 D 之间的路由器间 TCP 连接。路由器 A 附有一个 RPC 服务器客户端。路由器 B 既附有一个 RPC 调用者又附有一个代理。另一个 RPC 调用者附在路由器 C 上。一个 RPC 服务器和一个 RPC 调用者附在路由器 D 上。
由于使用了最短路径路由,RPC 调用者3 到 RPC 服务器1 的 RPC 调用/广播消息将仅通过路由器 A 和路由器 C 传输。同样,从调用者1 到服务器2 的 RPC 调用将通过路由器 B 和路由器 D 传输。RPC 调用者2 调用服务器2 上的方法将仅通过路由器 D 传输。在此示例中,请注意,只有两个通信端点之间的最短路径上的路由器参与消息传输。这提供了一种使用单个代理消息传递服务无法实现的一定程度的并行性。
请注意,当消息在其到达最终目的地时经过路由器时,不会进行排队。具体来说,路径上的任何路由器都不会声明对消息的所有权 - 也就是说,路由器不会确认消息。相反,来自消耗客户端的确认会通过网格传播回原始发送者。换句话说,路由器网格执行端到端确认。与此相反,代理确实声明对消息的所有权。但是,来自代理的确认并不能保证消息将被消耗。它仅仅表明消息已被排队。
由于路由器计算消息的最佳路径,因此可以配置跨消息传递网格的冗余路径。通过在物理冗余网络上部署路由器,可以在不影响消息传递服务的情况下承受一定程度的基础设施丢失。由于路由器本身是无状态的,因此不需要像在代理部署中那样进行集群功能。虽然由于基础设施故障可能导致网格分区,但由于连接的路由器之间没有主/从关系,因此不可能发生“脑裂”。所有路由器都是对等的。
路由器网格的一个限制是,由于路由器是无队列的,它无法提供消息存储和转发服务。但是,代理提供出色的存储和转发功能,并且可以集成到路由器网格中以实现该目的。
对集成代理的支持不是 Newton 版本的目标。因此,当使用路由网格作为消息传递后端时,将仅提供对通知流量的有限支持。由于网格无法存储通知消息,因此任何尝试发布没有活动订阅者的通知都将导致消息传递失败。对于那些不能容忍丢失通知消息的应用程序,建议使用代理作为通知服务的消息传递后端。由于可以为 RPC 和通知配置不同的消息传递后端,因此可以使用网格进行 RPC 流量,并使用代理进行通知流量。
提议的变更¶
需要解决驱动程序中的三个问题才能支持通过路由消息网格进行 RPC
最佳消息寻址
信用管理
无法送达的消息
寻址¶
当前的寻址方案将消息传递语义嵌入到地址的前缀中。Qpid C++ Broker 使用此前缀自动配置适当的节点 - 队列或主题 - 用于给定的发布/订阅请求。有关详细信息,请参阅 [2]。
qpidd 代理在连接设置期间识别自身,因此可以引入新的寻址语法,同时保留现有的语法以实现向后兼容性。当连接到 qpidd 时,使用的原始地址语法将保持不变,以允许滚动升级。新的地址语法仅在驱动程序连接到路由器网格时使用。如果需要,将提供一个配置选项以手动选择两种寻址模式中的一种。
在比较地址时,路由器采用最长前缀匹配算法。与此相反,传统的代理倾向于使用模式匹配或简单的精确匹配。路由器地址语法将使用地址的前缀作为地址空间分类。
对于可路由寻址,还有一些额外的考虑因素不适用于代理寻址
其他应用程序可能驻留在同一网格上。寻址方案应设计为避免与完全独立的应用程序集使用的地址空间发生冲突。
应用程序感知路由。应该能够在地址级别区分 RPC 相关流量和通知流量。这将允许路由器将通知流量路由到代理(们),而 RPC 消息可以点对点传输。
可配置的路由子域。应该能够进一步基于每个项目对流量进行分区。这可以为项目之间提供一些流量隔离,并允许在同一网格上并行使用 oslo.messaging 总线。
并且,与现有的地址结构一样,必须将传递语义(例如,扇出、直接、共享等)提供给网格,以确保使用所需的传递模式。
对于 RPC 服务,寻址必须允许以下 4 种消息传递模式
扇出
直接发送到给定的 RPC 服务器
共享订阅(例如,共享队列)
RPC 响应
对于通知服务,只有一种传递模式:共享订阅。
为上述模式(RPC 响应除外)提出了以下地址语法
使用 |
格式 |
RPC 扇出 |
openstack.org/om/rpc/multicast/$EXCHANGE/$TOPIC |
RPC 服务器 |
openstack.org/om/rpc/unicast/$EXCHANGE/$TOPIC/$SERVER |
RPC 共享 |
openstack.org/om/rpc/anycast/$EXCHANGE/$TOPIC |
通知 |
openstack.org/om/notify/anycast/$EXCHANGE/$TOPIC.$PRIORITY |
前缀‘openstack.org’ 建立了地址空间的根域,仅供 OpenStack 应用程序使用。将地址空间前缀为字符串‘openstack.org/om’保留给 oslo.messaging 使用,将避免与其他应用程序也使用路由网格时发生冲突。
‘rpc’ 和 ‘notify’ 段对服务地址空间进行分类。网格将使用 ‘rpc’ 和 ‘notify’ 标签来识别服务。这意味着网格可以将通知流量发送到代理,而 RPC 流量可以点对点发送。
‘unicast’、‘multicast’ 和 ‘anycast’ 关键字确定需要应用于消息传递的语义。‘unicast’ 导致消息传递到一个订阅者。‘anycast’ 导致消息传递到多个订阅者中的一个 [Scheduling]。对于 ‘multicast’,路由器会将消息的副本传递给所有订阅者。
在为给定队列提供多个订阅者时,大多数代理采用轮询分发策略。由于代理是唯一的分布点,因此代理可以保证消息均匀地分发给每个订阅者。消息网格中没有单个“中央分发器”,因此网格采用不同的方法进行 ‘anycast’ 分发。例如,网格将根据最低路径成本优先进行传递。这意味着消息将首先传递给具有最低链路成本/更少路由器跳数的订阅者。网格还可以监视所有消费者上的信用水平,并检测到单个消费者是否跟不上消息到达速率(由于消息处理延迟)。这允许路由器将消息传递给不同的客户端 - 一个没有表现出如此高的积压的客户端。
值 $EXCHANGE、$TOPIC 和 $SERVER 都来自订阅的目标(或调用/广播时的目的地)。可以使用 $EXCHANGE 值根据应用程序的配置提供进一步的流量隔离。
RPC 响应的寻址将不使用上述任何地址格式。RPC 响应寻址将像今天一样工作:RPC 响应地址由消息总线(代理或路由器)动态分配给驱动程序,并被视为不透明值。驱动程序在发送消息之前会将 RPC 调用消息的 reply-to 字段设置为此值。传入消息将使用此 reply-to 值作为回复消息的地址。
将为每个 TCP 连接到总线使用单个回复地址,就像今天一样。RPC 调用消息将被分配一个唯一的消息标识符,该标识符将被写入消息头部的 ‘id’ 字段。RPC 服务器会将此标识符放置在回复消息的 ‘content-id’ 字段中,然后再发送。接收到的回复消息将使用消息的 ‘content-id’ 值进行解复用,并发送到适当的等待者。
信用管理¶
由于路由器网格无法排队消息,因此除非有准备好接受该消息的消费者,否则它不应接受来自发布者的消息。网格可以通过控制授予发布者的消息信用量来限制发布者可以发送到网格的消息数量。网格为它将接受的每条消息提供一个信用。除非发布者拥有信用,否则它不能将消息发送到网格。
网格本身不会创建信用,因为它没有存储消息的能力。订阅客户端提供信用。订阅者授予网格信用 - 为它愿意消耗的每条消息提供一个信用。网格将信用“代理”给想要发布到订阅者正在消耗的地址的客户端。因此,路由器仅在至少有一个订阅者订阅了消息的地址并授予了网格信用时,才会授予消息发布者的信用。这允许网格阻止发送者,直到消费者准备好从其接收为止。
驱动程序将为创建的每个订阅提供信用。每个服务器为其目标维护自己的订阅。还有一个用于共享 RPC 响应地址的订阅。每个订阅将向路由器网格提供信用,网格将反过来将其分发给发布到这些地址的客户端。
共享 RPC 响应订阅始终需要有可用的信用,以供来自 RPC 服务器的回复使用。否则,RPC 服务器可能会阻止尝试向特定客户端发送回复。这将导致所有 RPC 调用到该服务器也阻塞(即,队头阻塞)。幸运的是,RPC 调用模式是自限的:调用者在收到回复(或调用超时)之前被阻止发送任何进一步的请求。这意味着通过回复订阅信用对 RPC 调用者进行反压可能是不必要的。因此,驱动程序将授予回复订阅大量信用。驱动程序将监视信用水平,因为消息到达并传递给客户端,并在需要时补充信用。信用额度将是可配置的,默认值为 200 个信用。此默认值可能会在调整驱动程序性能的过程中更改。
RPC 服务器和通知订阅不能像回复订阅那样慷慨地提供信用。服务器需要在它跟不上传入消息流时应用一些反压。否则,太多的传入消息将在驱动程序中缓冲。目标是在最大限度地减少性能影响的同时,限制每个订阅的内部消息缓冲量。
默认情况下,每个 RPC 服务器和通知订阅将获得 100 个信用的批次。此默认值是可配置的,并且可以在调整过程中进行调整。当信用额度降至默认额度的一半以下时,驱动程序会将信用额度恢复到默认级别。信用额度检查将在客户端确认消息时执行。这将限制 RPC 服务器(直接、扇出和共享)的最坏情况缓冲到大约 300 条消息,而每个通知主题和优先级(共享)的缓冲限制为 100 条消息。
为了进行调整,驱动程序将维护一个检测到的信用阻塞计数——当驱动程序无法补充信用额度之前,未使用的信用额度达到零时。当检测到此事件时,驱动程序将发出调试日志消息。
发布者端也必须考虑信用额度。如果没有可用的信用额度,驱动程序不得允许发布者发送消息。否则,将无法阻止无限数量的消息在驱动程序中缓冲,等待发送到网格。因此,驱动程序将遵守网格施加的信用额度限制,并阻止发送者,直到信用额度可用为止。
RPC 调用者已经阻塞,直到收到回复或调用超时。如果在超时期间未提供任何信用额度,驱动程序将简单地使调用操作失败,就像未及时收到回复一样。
与 RPC 调用不同,RPC 广播不等待回复。在广播时,如果未提供任何信用额度,客户端也将被阻塞。对于非扇出广播,调用者还将等待从目标接收到确认(而非网格)。
扇出广播的行为与广播情况相同,只是确认来自网格本身而不是目标。这是网格的一种行为,旨在防止回复确认“风暴”。
当发送 RPC 响应时,驱动程序也将遵守信用规则。如果未提供任何信用额度,RPC 服务器在响应时将被阻塞。
还有一个与信用相关的需要由驱动程序解决的问题:如果信用额度没有及时提供怎么办?或者由于缺少消费者而信用额度从未到达怎么办?
现有的基于代理的解决方案通过响应发布请求自动创建队列来解决缺少消费者的問題。通过自动创建队列,代理允许应用程序“度过”任何消费者到达的延迟。即使消费者从未到达,代理也将接受该消息。这意味着只要存在代理,发布者就不必等待消费者到达。
有多少应用程序依赖于这种行为,无法估量。除非驱动程序以某种方式对此进行补偿,否则很可能会出现严重问题。
驱动程序将通过强制对发送到路由器网格的每条消息设置超时来解决此问题。如果在超时时间内未到达信用额度,将引发异常,指示操作失败。
上述的唯一例外是 oslo.messaging API 并不强制在使用超时发送消息。基础 Driver 类中有两种发送方法:send() 和 send_notification()。此外,IncomingMessage 类有一个 reply() 方法,用于发送 RPC 响应。只有 send() 方法接受超时参数,其余方法不接受。在 API 未提供超时值的情况下,驱动程序将应用默认值。如果调用 send() 方法时未提供超时值,则驱动程序将应用默认超时值。
RPC 调用和广播(无论有或没有扇出)的建议默认超时时间为 30 秒。如果超时到期前未到达任何信用额度,将引发 MessagingTimeout 或 MessageDeliveryFailure 异常。只有在提供了超时值的 send() 调用中才会引发 MessagingTimeout 异常。在所有其他情况下,将引发 MessageDeliveryFailure。
请注意,超时时间还将包括等待 RPC 响应到达所花费的时间。
通过 reply() 方法发送回复时,至关重要的是 RPC 服务器永远不要无限期地阻塞,等待来自 RPC 客户端的信用额度。这将导致整个 RPC 服务器挂起,影响其他具有待处理请求的客户端。
尽管 RPC 客户端的驱动程序将向回复订阅授予大量的信用额度,但仍然存在客户端在处理 RPC 调用后已变得不可访问的可能性。客户端可能已崩溃,或者网格可能已丢失连接。为了防止这种情况,默认超时也将应用于 RPC reply() 调用。与 RPC 广播一样,如果在超时到期之前未提供任何信用额度或未从对等方接收到任何确认,则回复将失败。与 RPC 广播不同,不会引发任何异常,因为应用程序无法恢复。相反,将记录错误。
无法传递的消息¶
消息的目标可能在消息通过路由器网格传输时变得不可访问。消费者可能已崩溃,或者可能发生网络故障,导致消息无处可去。在这种情况下,路由器网格将向原始发送者发送否定确认。这采用 AMQP 1.0 disposition performative 的形式,其终端传递状态为 MODIFIED 或 RELEASED。这些状态通知原始发送者消息从未传递。因此,消息可以在以后重新传输,而无需担心重复。但是,无法保证在重新传输时消息顺序将被保留。
重新传递将不是 Newton 版本的重点。驱动程序将简单地将接收到 RELEASED 或 MODIFIED disposition 视为消息传递失败。这可以在未来的版本中以不同的方式解决。
[实际上,由于广播不保证严格的消息顺序,因此可能可以安全地重新发送 RPC 广播。目前,这还有待确定]
驱动程序将使用 disposition 来实现 oslo.messaging 定义的可选“重新排队”功能。当消费者调用传入消息上的 requeue 方法时,驱动程序将发送具有 RELEASED 终端状态的 disposition 回到发布者。
释放的消息实际上会发生什么取决于消息总线的能力。代理将简单地重新排队消息。网格可能会将消息转发到另一个消费者(如果存在),或者将 RELEASED 状态代理回发布者。如前所述,驱动程序将在 Newton 中将 RELEASED 状态视为消息传递失败。
备选方案¶
还有其他替代消息传递后端,乍一看似乎提供与路由网格类似的功能。例如,ZeroMQ 也是一种非代理解决方案。ZeroMQ 还提供了一种点对点消息传递模式,可用于 RPC 服务。但是,ZeroMQ 和路由网格之间存在一些重要差异。
首先,路由网格的客户端只需要与路由器网格建立一个 TCP 连接,而 ZeroMQ 实现则使用每个目标的 TCP 连接。换句话说,ZeroMQ RPC 服务器需要与每个与之通信的 RPC 客户端建立一个 TCP 连接。因此,随着 RPC 服务器和客户端数量的增加,ZeroMQ 将需要更多的 TCP 相关资源。
其次,在路由器网格中,不需要所有客户端都可以通过 TCP 访问,这是 ZeroMQ 所要求的。路由器网格执行消息层路由,而不是 IP 地址路由。这允许位于单独的私有 IP 子网上的客户端和服务器使用路由器网格作为桥梁进行互操作。这些子网不需要通过 IP 网格彼此可见。例如,RPC 服务器可能位于公司 A 的私有子网中,而 RPC 客户端位于公司 B 的不同私有子网中。可以使用 ZeroMQ 来实现这一点,但需要正确配置防火墙和 NAT。
它还提供了更好的负载平衡,即在对服务组进行调用时。客户端不负责确定服务组中的哪个服务实例应该接收该消息。
最后,路由器网格固有地提供服务发现。不需要专门的服务发现组件。
联邦代理网络是另一种与路由器网格有些相似的消息传递总线。但是,路由网格是分布式消息传递的更好解决方案。事实上,消息路由是专门开发出来,以提供比代理联合更好的分布式消息传递解决方案。AMQP 1.0 之前的版本需要代理中介。因此,在 1.0 之前分发消息的唯一方法是开发基于代理的路由解决方案。协议的 1.0 版本取消了此代理中介要求,并使路由消息网格成为可能。由轻量级、无状态消息交换机增强的代理网络,在需要存储和转发的地方,可以完全取代基于联邦代理的部署的功能。
通过代理联合传输的消息在每个节点上排队,而路由网格则不然。这会增加延迟,降低吞吐量,并消除端到端确认的可能性。这使得“客户端”更难知道何时重新发送请求,而“服务”更难知道何时重新发送响应。这可能会导致尝试使代理复制所有消息,这会使扩展更加困难。
路由器网格并非所有部署的理想解决方案。如果单个代理可以满足部署的消息传递要求,那么它是一个更简单的解决方案。ZeroMQ 将是那些不受路由或 TCP 资源限制的分布式部署的不错选择。同样,在某些部署中,路由器网格将是最佳的消息传递后端。
Impact on Existing APIs¶
现有的 API 不需要任何更改。这些更改将保留与现有 qpidd-based 部署的兼容性。
安全影响¶
从驱动程序角度来看,安全模型不会发生变化。驱动程序已经支持 TLS 服务器和客户端身份验证。它还支持基于 SASL 的身份验证,包括 Kerberos 支持。驱动程序符合 AMQP 1.0 规范定义的安全模型,并可以与任何符合规范的消息传递服务一起工作。
性能影响¶
任何性能影响都应仅限于使用 AMQP 1.0 驱动程序的用户。其他驱动程序(如 RabbitMQ)的用户不受影响。当前 Qpid 代理的性能可能会受到影响,但是将尽一切努力将此影响降到最低。
Configuration Impact¶
将添加新的配置项,用于信用额度和超时持续时间。这些选项的默认值将在驱动程序针对性能进行调整时确定。这些项目包括
回复订阅的信用额度(默认值:200)
服务器订阅的信用额度(默认值:每个订阅 100)
驱动程序级别的默认 RPC 调用/广播超时时间(默认值:30 秒)
驱动程序级别的默认 RPC 响应超时时间(默认值:10 秒)
驱动程序级别的默认通知超时时间(默认值:30 秒)
寻址模式(默认值:动态)
开发人员影响¶
添加到 oslo.messaging 的任何需要通过驱动程序修改来实现的新功能也需要在此驱动程序中实现。如果这些新功能需要特定于代理后端的行为,那么在使用路由网格时可能无法支持它们。
Testing Impact¶
将使用 Qpid Dispatch Router 作为消息传递后端进行测试。有关此路由器的更多信息,请参见此处 [3]。
为了完全测试此驱动程序,需要在 CI 环境中提供 Qpid Dispatch Router。这取决于将路由器包及其依赖项包含到 Debian 测试中。Apache Qpid 社区正在将以下包提交到 Debian 中包含
qpid-dispatch-router
qpid-dispatch-router-tools
python-qpid-proton
qpid-proton-c
pyngus
这些包已在 EPEL 存储库中可用。
驱动程序必须通过现有的 amqp1 驱动程序测试。
驱动程序必须通过 gate-oslo.messaging-src-dsvm-full 和 gate-oslo.messaging-dsvm-functional 测试。
Devstack 已经支持使用独立路由器 [4]。为了通过上述门测试,可能需要将 qpidd 代理添加为通知传输。
实现¶
负责人¶
- 主要负责人
kgiusti@gmail.com (kgiusti 在 IRC 上)
- 其他贡献者
里程碑¶
完成目标里程碑:newton
工作项¶
实现新的寻址语法
实现信用处理
实现新的配置项
Update documentation
功能测试集成
上游 CI 集成
孵化¶
无。
采用¶
这种驱动程序不太可能在大多数用例中被采用,因为单个代理通常就足够了。在那些在分布式网格拓扑中部署中到大型云的部署中,采用的可能性更高。
库¶
oslo.messaging
预计 API 稳定¶
无
文档影响¶
需要更新库的 AMQP 1.0 文档,以获取新的后端 [5]。
依赖项¶
驱动程序不需要额外的依赖项。
参考资料¶
注意
本作品采用知识共享署名 3.0 非移植许可协议授权。 http://creativecommons.org/licenses/by/3.0/legalcode