标题: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间传输一次消息,然后再进行复制。除此之外,我们还介绍了镜像队列算法在每条消息多次发送时的效率低下问题。
链接文章中说明了镜像队列每次复制时,master会向mirror复制两次消息。
如果消费者能够从他们连接的地方获得消息,而不是从位于不同broker上的领导者那里获得消息,那将是一件好事——这将提高网络利用率,并减轻队列领导者的压力。
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
命令:- 领导者将消息加入队列中
- 领导者通知发布者消息已提交
Group 4 (event sequence 6-9)
- 接3-1,领导者将
subscribe c1
命令复制到跟Follower A - 接3-1,领导者将
subscribe c1
命令复制到跟Follower C - 接3-2,Follower C应用
enqueue m1
命令:- 将消息加入队列
- 没有看到消费者,因此不会发生消息投递
- 接3-2,Follower A应用
enqueue m1
命令:- 将消息加入队列
- 没有看到消费者,因此不会发生消息投递
消费者当然存在,但副本只有在其日志中应用subscribe
命令时才能感知到消费者。他们的日志中确实有这些命令,但尚未应用它们(4-1、4-2已经将订阅命令发送到了跟随者,但尚未应用)。
Group 5 (event sequence 10-12)
- 接4-2,Leader B应用
subscribe c1
命令:- 将C1加入Service Queue
- 评估尚未投递的消息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
命令:- 将C1加入Service Queue
- 针对SQ中的第一个消费者评估消息m1,但该通道不是本地的,将C1重新加入SQ
- 接5-1,Follower A应用
subscribe c1
命令:- 将C1加入Service Queue
- 针对SQ中的第一个消费者评估消息m1,并发现通道是本地的,因此将消息发送到该本地通道,将C1重新加入SQ
Group 7 (event sequence 17-20)
- 接6-2,Leader B应用
subscribe c2
命令- 将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
命令- 将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
命令:- 将C2加入SQ
-
接8-1,Leader B应用
enqueue m2
命令:- 将消息m2加入队列
- C1从SQ出队,发现通道不是本地的,C1入队SQ队尾,跟踪消息m1的状态(C1会在随后处理这个消息)
- 通知发布者这条消息已经被提交
此时,Leader B的SQ不同于追随者(C1在SQ队列尾部),但这只是因为它领先其他两个跟随者一个命令。
acknowledge m1
命令也复制到了Follower A。
Group 9 (event sequence 24-26)
- 接7-3,Leader B应用
acknowledge m1
命令:- 从队列中删除消息m1
- 接8-3,Follower C应用
enqueue m2
命令:- 消息m2加入队列
- C1从SQ出队列,但是C1不在本地,C1重新进入SQ队尾
- 接8-3,Follower A应用
enqueue m2
命令:- 消息m2加入队列
- 将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
命令- 从队列中删除m1
- 接13-2,Follower C应用
acknowledge m1
命令- 从队列中删除m1
Group 14 (event sequence 34-35)
- 接13-1,Leader A应用
enqueue m3
命令- 将m3加入队列
- 从SQ中取出C2,但不是本地通道,因此将C2重新入队SQ并跟踪m3状态
- 接14-1,Follower C应用
enqueue m3
命令- 将m3加入队列
- 从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
命令- C2入队SQ
- Follower A应用
subscribe c2
命令- 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
命令:- C2入队SQ
- Leader B应用
enqueue m2
命令:- C1从SQ出队,但C1不在本地,C1重新入队SQ
- Follower A应用
enqueue m2
命令:- C1从SQ出队,C1是本地的,尝试发送消息m2(但C1已经不存在了),C1重新入队SQ
Group 9 - Alternate (event sequence 26-28)
-
Leader B应用
down c1
命令:-
从SQ中删除C1
-
将之前投递给C1但是未确认的消息m1和m2返回到队列中
由于之前跟踪了非本地投递的m1、m2,所以此时Leader B是知道有哪些消息被C1消费且未确认的。
-
为了重新投递m1,C2从SQ出队,但C2不是本地,C2重新入队SQ
-
-
Follower C应用
enqueue m2
命令:- C1从SQ出队,但C1不是本地的,重新入队C1,并跟踪m2状态
-
Follower A应用
down c1
命令:- 从SQ中删除C1
- 将之前投递给C1但是未确认的消息m1和m2返回到队列中
- 为了重新投递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
命令:- 从SQ中删除C1
- 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