首页 > 其他分享 >How quorum queues deliver locally while still offering ordering guarantees

How quorum queues deliver locally while still offering ordering guarantees

时间:2022-10-26 21:57:06浏览次数:79  
标签:quorum offering 队列 SQ Follower m1 C2 C1 guarantees

标题:How quorum queues deliver locally while still offering ordering guarantees
原文:https://blog.rabbitmq.com/posts/2020/06/quorum-queues-local-delivery/
时间:2020-06-23

团队最近被问及,仲裁队列从本地队列副本(领导者或跟随者)将消息复制到其他节点,这是否能提供和经典队列一样的消息顺序保证,是如何实现的?镜像队列总是从master复制消息到其他节点,因此从任何队列副本向外传递看起来都可能影响顺序保证。

这就是本文的主题。请注意,对于分布式系统爱好者来说,这篇文章是一篇对技术的深度挖掘。我们将了解仲裁队列如何从任何队列副本(leader或follower)复制消息,而不需要依赖Raft之外的协调,又能维护消息排序保证。

TLDR

所有队列,包括仲裁队列,对单个channel中未发生redeliveries的消息提供顺序保证。最简单的观测方法是,如果队列只有一个消费者,那么该消费者将获得按FIFO顺序传递的消息。一旦在一个队列上有两个消费者,那么这些保证将变为对non-redelivered消息提供单调顺序保证。也就是说,可能会有差距(因为消费者存在竞争),但一个消费者永远不会在较早的消息之前收到较晚的消息(这不是重新投递)。

如果你需要所有消息(包括重新投递的消息)的全局FIFO排序保证,那么你需要使用prefetch为1的Single Active Consumer。在下一条消息投递之前,任何重新投递的消息都会被添加回队列,以此来保持FIFO顺序。

仲裁队列提供与经典队列具有相同的排序保证。它恰好也能够从任何本地副本投递消息,也就是与消费者直接连接的副本。如果您想了解仲裁队列是如何管理的,请继续阅读。

The Cost of Proxying Traffic

RabbitMQ试图通过允许任何客户端连接到集群中的任何节点来简化事情。如果消费者连接到broker 1,但队列存在于broker 2上,则通信路径将从broker 1代理到broker 2,反之亦然。消费者不知道队列托管在其他节点上。

这种灵活性和易用性是有代价的。在三节点集群上,最坏的情况是发布者连接到broker 1,其消息路由到broker 2上的单副本经典队列上,然后消费者连接broker 3消费该队列。为了处理这些消息,所有三个broker都被捆绑到其中,这当然效率较低。

如果队列存在多个副本,则必须通过proxy在broker间传输一次消息,然后再进行复制。除此之外,我们还介绍了镜像队列算法在每条消息多次发送时的效率低下问题。

图1 蓝色代表镜像队列复制流量,其他为proxy发布者和消费者流量

链接文章中说明了镜像队列每次复制时,master会向mirror复制两次消息。

如果消费者能够从他们连接的地方获得消息,而不是从位于不同broker上的领导者那里获得消息,那将是一件好事——这将提高网络利用率,并减轻队列领导者的压力。

图2 仲裁队列复制流量为蓝色,外加proxy产生的发布流量,以及消费者直接从跟随者产生的消费流量

The Cost of Coordination

对于镜像队列,所有消息都由master传递(并可能通过另一个broker发送给消费者)。这很简单,并且不需要master与其他镜像之间的协调。

在quorum队列中,我们增加了领导者和追随者之间的协调,以实现local delivery。领导者和追随者之间的通信将协调谁投递了哪些信息,因为我们不能让一条信息被消费两次或根本不被消费。不幸的事情可能会发生,例如消费失败、broker失效、网络分区等等,协调需要处理所有这些。

上面这段话的意思是,仲裁队列不仅复制消息,还要复制消费进度。

但协调对性能不利。协调的方式会使local delivery变得非常复杂,也会对性能产生极大的影响。我们需要另一种方式,幸运的是,我们所需要的一切都已经包含在Raft协议中了。

Coordination No, Determinism Yes

在分布式系统中避免协调的一种常用方法是使用确定性。如果集群中的每个节点都以相同的顺序获取相同的数据,并仅基于该数据做出决策,那么每个节点将在日志中的某个位置做出相同的决策。

确定性决策要求每个节点以相同的顺序提供相同的数据。仲裁队列构建在Raft上,Raft是一个复制的提交日志,是一个有序的操作序列。因此,只要执行local delivery所需的所有信息都写入到有序的操作日志中,那么每个副本(领导者或追随者)都将知道谁应该投递每条消息,而无需相互交流。

事实证明,即使是leader-only deliveries,我们仍然需要将消费者的来来往往添加到日志中。如果一个broker失败,而另一个追随者被提升为领导者,那么它需要了解集群中幸存的消费者,以便能够向他们传递消息。此信息还支持无需协调的本地消费。

Effects and the Local flag

仲裁队列构建在称为Ra的Raft实现上(也由RabbitMQ团队开发)。Ra是一种可编程状态机,用于复制操作日志。它将不同操作区别对待,对于指令操作(commands),所有副本都需要执行,对于外部操作(effects),只有领导者需要执行。这些commands、states和effects由开发人员编程。仲裁队列有自己的指令、状态和效果。

Command和effect的一个很好的例子是键值存储。添加、更新和删除数据应由所有副本执行。每个副本都需要拥有相同的数据,因此当一个领导者失败时,跟随者可以用相同的数据接管。因此,数据修改是command。但是,通知客户端程序key发生了变更只应该发生一次。如果客户端程序要求在key更新时收到通知,则它不希望收到领导者和所有副本的通知。因此,只有领导者才需要执行effects。

Ra支持local effect。在仲裁队列的情况下,只有send_msg的effect是本地的。其工作方式是,所有副本都知道存在哪些channels以及连接的哪些节点。当消费者注册上来时,这些信息会添加到日志中,同样,客户端离开时的信息也会添加到日志中。

只有一个节点需要执行send_msg,因此它是一个effect。

每个副本按顺序应用日志中的每个已提交(多数已复制)command。应用enqueue命令将消息添加到队列中,应用consumer down命令将该consumer从Service Queue中删除(下面将详细介绍),并将其挂起的所有消息返回队列以重新投递。

消费者被添加到Service Queue(SQ)中,该队列是确定性维护的,这意味着所有副本在日志中的任何给定点都具有相同的SQ状态。每个消费者将使用与所有其他副本完全相同的SQ评估任何尚未交付的给定消息,并将消费者从SQ中出队。如果该消费者是本地的(意味着其channel进程与副本位于同一个broker上),则副本将向该本地channel发送消息。然后,该channel将其发送给消费者。如果消费者channel不是本地的,则副本将不会直接发送消息给消费者,而是跟踪其状态(哪个broker负责发送消息、是否已确认等)。需要注意的一点是,如果没有消费者channel本地的副本,则领导者会将其发送到该通道(proxying方法)。

结合下面的例子更好理解,大致意思是经过相同的操作序列,所有副本的SQ具有相同的状态。

如果你仍然觉得这很有趣,但很难概念化,那么我不怪你。我们需要的是图表和一系列事件来证明这一点。

An example with diagrams

我会将事件集分组到每个图中,以尽可能减少图的数量。

每个图由三个副本组成,一个领导者和两个追随者。我们可以看到日志的状态、service queue、队列表示和应用操作。每一个操作的格式为command term:offset data。例如E 1:1 m1是入队command,添加到第一个term中,具有偏移量1,消息为m1。Terms和offsets是Raft算法术语,对于理解local delivery来说并不特别重要(但如果你觉得有趣,我建议你阅读Raft算法论文)。

Group 1 (event sequence 1)

  • 发布者为消息m1添加enqueue m1命令。

Group 2 (event sequence 2-3)

  • 接1-1,领导者将命令enqueue m1复制到跟随者A
  • 接1-1,领导者将命令enqueue m1复制到跟随者C

Group 3 (event sequence 4-5)

  • 消费者1连接到Follower A,并添加一条subscribe c1命令

    注意,从下图可以看出,该命令先添加到了leader B。

  • 接2-2,Leader B应用enqueue m1命令:

    1. 领导者将消息加入队列中
    2. 领导者通知发布者消息已提交

Group 4 (event sequence 6-9)

  • 接3-1,领导者将subscribe c1命令复制到跟Follower A
  • 接3-1,领导者将subscribe c1命令复制到跟Follower C
  • 接3-2,Follower C应用enqueue m1命令:
    1. 将消息加入队列
    2. 没有看到消费者,因此不会发生消息投递
  • 接3-2,Follower A应用enqueue m1命令:
    1. 将消息加入队列
    2. 没有看到消费者,因此不会发生消息投递

消费者当然存在,但副本只有在其日志中应用subscribe命令时才能感知到消费者。他们的日志中确实有这些命令,但尚未应用它们(4-1、4-2已经将订阅命令发送到了跟随者,但尚未应用)。

Group 5 (event sequence 10-12)

  • 接4-2,Leader B应用subscribe c1命令:
    1. 将C1加入Service Queue
    2. 评估尚未投递的消息m1,将C1从SQ中出队,但发现它不是本地的,因此不会将消息发送给C1,而是只跟踪C1处理的m1,将C1重新加入SQ队列
  • C2将一条subscribe c2命令加入到Leader B(C2实际从Follower C消费)
  • 发布者为消息m2添加一条enqueue m2命令(添加到Leader B中)

Group 6 (event sequence 13-16)

  • 接5-2,Leader B将命令subscribe c2复制到Follower A
  • 接5-2,Leader B将命令subscribe c2复制到Follower C
  • 接5-1,Follower C应用subscribe c1命令:
    1. 将C1加入Service Queue
    2. 针对SQ中的第一个消费者评估消息m1,但该通道不是本地的,将C1重新加入SQ
  • 接5-1,Follower A应用subscribe c1命令:
    1. 将C1加入Service Queue
    2. 针对SQ中的第一个消费者评估消息m1,并发现通道是本地的,因此将消息发送到该本地通道,将C1重新加入SQ

Group 7 (event sequence 17-20)

  • 接6-2,Leader B应用subscribe c2命令
    1. 将C2加入SQ
  • 接5-3,Leader B将enqueue m2命令复制到Follower A
  • 接6-4.2,Leader B收到来自consumer 1的acknowledge m1命令
  • 接7-1,Follower A应用subscribe c2命令
    1. 将C2加入SQ

请注意,Follower A和Leader B都处于日志中的相同位置(绿色箭头指向相同位置),并且具有相同的Service Queues。

Group 8 (event sequence 21-23)

  • 接5-3,Leader B将enqueue m2命令复制到Follower C

  • 接7-1,Follower C应用subscribe c2命令:

    1. 将C2加入SQ
  • 接8-1,Leader B应用enqueue m2命令:

    1. 将消息m2加入队列
    2. C1从SQ出队,发现通道不是本地的,C1入队SQ队尾,跟踪消息m1的状态(C1会在随后处理这个消息)
    3. 通知发布者这条消息已经被提交

此时,Leader B的SQ不同于追随者(C1在SQ队列尾部),但这只是因为它领先其他两个跟随者一个命令。

acknowledge m1命令也复制到了Follower A。

Group 9 (event sequence 24-26)

  • 接7-3,Leader B应用acknowledge m1命令:
    1. 从队列中删除消息m1
  • 接8-3,Follower C应用enqueue m2命令:
    1. 消息m2加入队列
    2. C1从SQ出队列,但是C1不在本地,C1重新进入SQ队尾
  • 接8-3,Follower A应用enqueue m2命令:
    1. 消息m2加入队列
    2. 将C1从SQ中取出,并看到C1是本地的,因此向C1发送消息m2,C1重新加入SQ

到此,m1、m2两条消息都被C1消费掉了。

请确保service queues的相互匹配——两个跟随者处于相同的偏移量,领先者领先一个偏移量,但acknowledgements不会影响service queues。

Group 10 (event sequence 27)

  • 托管Leader B的broker失败或关闭。

Group 11 (event sequence 28)

  • 发生领导者选举,Follower A获胜,因为它的操作日志中具有最高的epoch:offset值

Group 12 (event sequence 29-30)

  • 发布者添加enqueue m3命令
  • Leader A将acknowledge m1命令复制到Follower C

acknowledge m1命令在Leader A由未提交变成了已提交。

Group 13 (event sequence 31-33)

  • 接12-1,Leader A将enqueue m3命令复制到Follower C
  • 接12-2,Leader A应用acknowledge m1命令
    1. 从队列中删除m1
  • 接13-2,Follower C应用acknowledge m1命令
    1. 从队列中删除m1

Group 14 (event sequence 34-35)

  • 接13-1,Leader A应用enqueue m3命令
    1. 将m3加入队列
    2. 从SQ中取出C2,但不是本地通道,因此将C2重新入队SQ并跟踪m3状态
  • 接14-1,Follower C应用enqueue m3命令
    1. 将m3加入队列
    2. 从SQ中取出C2,发现C2连接的本地,因此将m3发送出去,C2重新入队SQ

因此,我们看到,在副本之间没有额外协调的情况下,我们实现了local delivery,同时保持了先进先出顺序,即使在leadership故障转移后也是如此。

但是,如果消费者在收到跟随者投递的消息后失败了会怎样?是否会检测到这种情况,并由其他broker将消息重新投递到另一个消费者?

Alternate scenario - Consumer Failure

我们将继续在Group 6中断的内容——m1已经交付给C1,但尚未确认。

Group 7 - Alternate (event sequence 17-20)

  • C1离开(无论什么原因)
  • Leader B复制enqueue m2命令到Follower A
  • Leader B应用subscribe c2命令
    1. C2入队SQ
  • Follower A应用subscribe c2命令
    1. C2入队SQ
  • Leader B监控发现C1离开了,新增一条down c1命令到本地日志

Group 8 - Alternate (event sequence 21-25)

  • Leader B复制down c1命令到Follower A
  • Leader B复制enqueue m2命令到Follower C
  • Follower C应用subscribe c2命令:
    1. C2入队SQ
  • Leader B应用enqueue m2命令:
    1. C1从SQ出队,但C1不在本地,C1重新入队SQ
  • Follower A应用enqueue m2命令:
    1. C1从SQ出队,C1是本地的,尝试发送消息m2(但C1已经不存在了),C1重新入队SQ

Group 9 - Alternate (event sequence 26-28)

  • Leader B应用down c1命令:

    1. 从SQ中删除C1

    2. 将之前投递给C1但是未确认的消息m1和m2返回到队列中

      由于之前跟踪了非本地投递的m1、m2,所以此时Leader B是知道有哪些消息被C1消费且未确认的。

    3. 为了重新投递m1,C2从SQ出队,但C2不是本地,C2重新入队SQ

  • Follower C应用enqueue m2命令:

    1. C1从SQ出队,但C1不是本地的,重新入队C1,并跟踪m2状态
  • Follower A应用down c1命令:

    1. 从SQ中删除C1
    2. 将之前投递给C1但是未确认的消息m1和m2返回到队列中
    3. 为了重新投递m1,C2从SQ出队,但C2不是本地,C2重新入队SQ,并跟踪m1状态

Group 10 - Alternate (event sequence 29-31)

  • Follower A处理下一条未送达的消息m2,将C2从SQ中出队,但C2不是本地,C2重新入队SQ,并跟踪m2状态
  • Leader B处理下一条未送达的消息m2,将C2从SQ中出队,但C2不是本地,C2重新入队SQ,并跟踪m2状态
  • Follower C应用down c1命令:
    1. 从SQ中删除C1
    2. Follower C处理下一条未送达的消息m1,将C2从SQ中出队,发现C2是本地的,将m1发送出去,C2重新入队SQ

Group 11 - Alternate (event sequence 32)

  • Follower C处理下一条未送达的消息m2,将C2从SQ中出队,发现C2是本地的,将m2发送出去,C2重新入队SQ

仲裁队列处理consumer 1故障时没有任何问题,并且仍然可以从本地副本投递消息,无需额外协调。关键是确定性决策,这要求每个节点仅使用日志中的数据来通知其决策,并且其日志中的已提交条目不存在分歧(这都是由Raft来处理的)。

Final Thoughts

仲裁队列具有与其他任何队列相同的排序保证,同时也能够从本地副本投递消息。他们如何实现这一点很有趣,但与开发人员或管理员无关。了解其中的原理对选择仲裁队列而不是镜像队列提供了依据。我们之前描述了镜像队列背后低效的算法,现在你已经看到,通过仲裁队列可以大大优化了网络利用率。

然而,从跟随者副本中消费不仅可以提高网络利用率,我们还可以更好地隔离发布者和消费者负载。发布者可以影响消费者,反之亦然,因为他们在同一资源上存在竞争——队列。通过允许消费者从不同的broker那里消费,我们得到了更好的隔离性。这篇文章表明,即使面临大量队列积压和来自消费者的额外压力,仲裁队列也可以保持较高的生产速率,而镜像队列则更容易受到影响。

标签:quorum,offering,队列,SQ,Follower,m1,C2,C1,guarantees
From: https://www.cnblogs.com/oyld/p/16830209.html

相关文章