前言
前面我们学了 RabbitMQ 的集群,虽然集群可以缓解服务器的压力,并且在有 RabbitMQ 挂掉的时候也不会对整个 RabbitMQ 服务产生很大的影响。但是呢有一个问题,就是当 RabbitMQ 节点挂掉之后,那么由该节点创建的队列中的消息也就都会消息,这时就会对服务产生较大的影响,为了解决这个问题就需要用到 “仲裁队列” 了。
什么是仲裁队列
仲裁队列(Quorum Queues)是RabbitMQ在3.8版本及以后引入的一种现代队列类型,它主要基于Raft共识算法实现了一个持久化、复制的FIFO(先进先出)队列。仲裁队列的设计目标是为了提供更安全、更简单、定义明确的故障处理语义,从而使用户在设计和操作系统时更加容易理解。使用仲裁队列可以在 RabbitMQ 节点间进行队列数据的复制,从而达到在一个节点宕机时,队列仍然可以提供服务的效果。
这是 RabbitMQ 仲裁队列的官方文档 https://www.rabbitmq.com/docs/quorum-queues
仲裁队列是RabbitMQ 3.8版本最重要的改动。 他是镜像队列的替代方案。 在RabbitMQ 3.8版本问世之前,镜像队列是实现数据高可用的唯一手段,但是它有一些设计上的缺陷,这也是RabbitMQ提供仲裁队列的原因。经典镜像队列已被弃用,并计划在将来版本中移除。如果当前使用经典镜像队列的RabbitMQ安装需要迁移,可以参考官方提供的迁移指南 https://www.rabbitmq.com/docs/migrate-mcq-to-qq。
仲裁队列与镜像队列的区别:
- 仲裁队列是RabbitMQ 3.8版本及以后引入的,旨在取代和优化原有的镜像队列功能。
- 仲裁队列在数据一致性和故障处理方面提供了更加清晰和可预测的语义。
- 仲裁队列在设计和实现上更加关注数据安全和可预测的恢复,因此在某些功能上与镜像队列存在差异。
既然提到了 Raft 共识算法,那么我们来了解一下什么是 Raft 共识算法。
Raft协议
Raft是一种用于管理和维护分布式系统一致性的协议,它是一种共识算法,旨在实现高可用性和数据的持久性。Raft通过在节点间复制数据来保证分布式系统中的一致性,即使在节点故障的情况下也能保证数据不会丢失。
在分布式系统中,为了消除单点故障提高系统可用性,通常会使用副本来进行容错,但这会带来另一个问题,即如何保证多个副本之间的一致性?
共识算法(Consensus Algorithm)就是用来解决这个问题的,它允许多个分布式节点就某个值或一系列值达成一致性协议。即使在一些节点发生故障、网络分区或其他问题的情况下,共识算法也能保证系统的一致性和数据的可靠性。
Raft 协议原文 https://web.stanford.edu/~ouster/cgi-bin/papers/raft-atc14
Raft 基本概念
Raft使用Quorum机制来实现共识和容错,即Raft集群的操作必须得到大多数(> N/2)节点的同意才能提交。
在分布式系统中,节点是指一个独立成员。当我们向Raft集群发起一系列读写操作时,集群内部会进行一系列复杂的操作来确保数据的一致性和可靠性。
Raft集群必须存在一个主节点(Leader),客户端向集群发起的所有操作都必须经由主节点处理。因此,Raft核心算法的第一部分就是选主(Leader Election)。没有主节点,集群就无法正常工作。首先,需要选出一个主节点,然后再考虑其他事情。
主节点会负责接收客户端发来的操作请求,将操作包装为日志,并同步给其他节点。在保证大部分节点都同步了本次操作后,就可以安全地给客户端回应了。这一部分工作在Raft核心算法中被称为日志复制(Log Replication)。
由于主节点的责任非常重大,所以只有符合条件的节点才可以当选主节点。为了保证集群对外展现的一致性,主节点在处理操作日志时也必须谨慎。这部分在Raft核心算法中被称为安全性(Safety)。
Raft算法将一致性问题分解为三个子问题:Leader选举、日志复制和安全性。
选主
下面详细介绍下Raft的选主过程:
当集群中没有主节点时(例如,所有节点都刚启动或主节点故障),或者当前主节点失去联系时(例如,网络分区或主节点崩溃),集群中的节点会开始进入选举状态。
- 每个候选节点会增加自己的任期号(Term),并向其他节点发送投票请求(RequestVote RPC)。
- 节点在收到投票请求时,会根据一系列条件(如任期号、日志的完整性和新鲜度)来决定是否投票给请求者。
- 候选节点需要收集到集群中大多数节点的投票才能成为主节点。
- 一旦成为主节点,它会向其他节点发送心跳消息(Heartbeat RPC)以维持其领导地位,并防止其他节点进入选举状态。
通过这种方式,Raft确保了即使在部分节点故障的情况下,集群也能快速恢复并继续工作,从而保证了分布式系统的高可用性和数据一致性。
在 Raft 算法中每个节点都处于以下三种角色之一:
节点角色
Raft节点角色
-
Leader(领导者):
- 负责处理所有客户端请求,并将这些请求作为日志项复制到所有Follower。
- 定期向所有Follower发送心跳消息,以维持其领导者地位,防止Follower进入选举过程。
-
Follower(跟随者):
- 接收来自Leader的日志条目,并在本地应用这些条目。
- 跟随者不直接处理客户端请求。
-
Candidate(候选者):
- 当跟随者在一段时间内没有收到来自Leader的心跳消息时,它会变得不确定Leader是否仍然可用。
- 在这种情况下,跟随者会转变角色成为Candidate,并开始尝试通过投票过程成为新的Leader。
在很多情况下吗,集群中只存在一个 leader,剩下的节点的身份都是 follower,下面这个图展示了身份的转换关系:
任期
Raft将时间划分成任意长度的任期(Term)。每个任期从一次选举开始,在这个时候会有一个或多个Candidate尝试去成为Leader。成功完成一次Leader Election之后,一个Leader就会一直管理集群直到任期结束。
在某些情况下,一次选举可能无法选出Leader(例如,由于网络分区或节点故障导致无法收集到足够的投票),这个时候这个任期会以没有Leader而结束。随后,一个新的任期(包含一次新的选举)会很快重新开始。
通过这种方式,Raft能够确保在分布式系统中,即使出现节点故障或网络问题,集群也能够通过不断的选举和任期更替来恢复正常的操作,从而保持数据的一致性和系统的可用性。
Term(任期)的作用
Term更像是一个逻辑时钟(logic clock)的作用,用于发现哪些节点的状态已经过期。每个节点都保存一个current term(当前任期号),该任期号会随着时间单调递增。在节点之间的通信过程中,会带上这个term的值。
- 当节点之间进行通信时,会交换当前任期号。
- 如果一个节点的当前任期号比其他节点小,那么它会将自己的任期号更新为较大的那个值。
- 如果一个candidate或leader发现自己的任期号过期了(即收到了来自更高任期号的合法RPC请求),它就会立即回到follower状态。
- 如果一个节点接收了一个带着过期的任期号的请求,那么它会拒绝这次请求。
Raft算法中的RPC通信
Raft算法中服务器节点之间采用RPC(远程过程调用)进行通信,主要有两类RPC请求:
- RequestVote RPCs:请求投票,由candidate在选举过程中发出。当candidate想要成为新的leader时,它会向其他节点发送RequestVote RPC请求投票。
- AppendEntries RPCs:追加条目,由leader发出,用于做日志复制和提供心跳机制。Leader通过向follower发送AppendEntries RPC来复制日志条目,并确保follower的日志与leader保持一致。同时,这些RPC也作为心跳消息,帮助leader维持其领导地位。
选举过程
- 初始化:
- 所有节点在启动时都处于 follower 跟随者状态。
- 每个节点随机生成一个选举超时时间(大约150ms至300ms)。
- 超时触发选举:
- 当一个跟随者的选举超时时间到达且未收到领导者的心跳信息时,它转变为候选人状态。
- 候选人先给自己投一票,并向集群中的其他节点发送选举投票请求。
- 投票过程:
- 其他节点在收到选举投票请求时,如果它们尚未在本任期内投票,并且请求的任期编号不小于它们当前的任期编号,则它们会投给该候选人一票。
- 如果候选人收到了集群中大多数节点的投票(N/2 + 1票),则它成为领导者。
- 领导者任期:
- 领导者会定期向其他节点发送心跳信息,以保持其领导地位并防止新的选举发生。
- 如果领导者发生故障或失去与其他节点的通信,则其他节点将重新进入选举过程。
- 任期更新:
- 在选举过程中,如果节点发现自己的任期编号比其他节点小,它会更新自己的任期编号到较大的值。
- 这有助于解决潜在的冲突和确保集群中的一致性。
对于 candidate 选举过程中可能会出现三种情况:
- candidate 赢得选举,成为 leader(包括自己的一票)
- 其他 candidate 赢得了选举,它自行切换到 follower
- 一段时间没有收到 majority 投票,保持 candidate 状态,重新发出选举
对于前面两种情况都比较容易理解,第一种就是自己最先达到超时时间,最先发出投票,然后得到了半数以上的票,成为 leader;而第二种就是在该 candidate 发出投票之前,有 candidate 率先发出投票成为 leader;那么第三种情况是如何产生的呢?比如存在四个节点 A B C D,A 和 B 同时到达超时时间,然后同时发出投票,A 的 RequestVote RPCs 先到达 C,然后 C 看自己的任期小于 A,就投给 A 一票,B 的 RequestVote RPCs 先到达 D,D 也投给 B 一票,然后 A 的 RequestVote RPCs 到达 D,但是 D 已经投给 B 了,所以就投的拒绝票,C 同样投给 B 拒绝票,那么此时 A 和 B 加上自己的一票,都是 2 票,未达到半数以上的票。当这种情况发生的时候,每个candidate都会进行一次超时响应,然后通过自增任期号来开启一轮新的选举,并启动另一轮的RequestVote RPCs。如果没有额外的措施,这种无结果的投票可能会无限重复下去。
这里 GitHub 有动画视频 https://raft.github.io/:
这个视频更加详细 https://thesecretlivesofdata.com/raft/
当遇到这种极端情况的时候如何解决呢?为了解决上述问题,Raft采用随机选举超时时间(randomized election timeouts)来确保很少产生无结果的投票,并且就算发生了也能很快地解决。为了防止选票一开始就被瓜分,选举超时时间是从一个固定的区间(比如,150-300ms)中随机选择。这样可以把服务器分散开来以确保在大多数情况下会只有一个服务器率先结束超时,那么这个时候,它就可以赢得选举并在其他服务器结束超时之前发送心跳。
Raft协议下的消息复制
每个仲裁队列都有多个副本,它包含一个主副本和多个从副本。replication factor为5的仲裁队列将会有1个主副本和4个从副本。每个副本都部署在不同的RabbitMQ节点上。
客户端(生产者和消费者)只会与主副本进行交互,主副本再将这些命令复制到从副本。当主副本所在的节点下线时,其中一个从副本会被选举成为新的主副本,继续提供服务。
消息复制和主副本选举的操作,需要超过半数的副本同意。当生产者发送一条消息,需要超过半数的队列副本都将消息写入磁盘以后才会向生产者进行确认,这意味着少部分比较慢的副本不会影响整个队列的性能。
仲裁队列的使用
创建仲裁队列:
- 使用 Spring 框架创建
@Bean("quorumQueue")
public Queue quorumQueue() {
return QueueBuilder.durable("quorum.queue").quorum().build();
}
- 使用 amqp-client 创建
Map<String, Object> param = new HashMap<>();
param.put("x-queue-type", "quorum");
channel.queueDeclare("quorum_queue",true,false,false,param);
- 使用管理平台创建
创建完成之后,会发现除了主副本之外,该节点所在的集群中的其他的节点中也存在副本:
仲裁队列的默认镜像数为 5,即一个主副本,四个副本,如果集群中节点的数量小于 5,那么就是一个主副本,剩下的都是副本,如果集群中节点的数量大于 5 个的话,也是会创建出 1 个主副本,四个副本。
然后我们向仲裁队列中发送消息并且演示宕机的情况:
这里的主副本是 rabbit,所以我们将 rabbit 挂掉:
登录到该集群中的其他节点的管理平台查看状态;
可以发现,当主副本挂掉之后,仲裁队列中的消息没有丢失,并且主副本从 rabbit 自动转换成了 rabbit2。
当 rabbit 再次启动的时候,他不会成为 leader,而是会变成 follower:
在分布式集群中,普通队列虽然在集群中的节点中都可以访问,但是实际上这个队列只存在于一个节点中,当其他节点访问的时候只是把请求转发到队列所在的节点进行的操作,一旦队列所在的节点宕机,那么队列中的消息就会丢失,也就是说普通队列只是提高了并发能力,并没有实现高可用,而仲裁队列即实现了高并发也实现了高可用。
结论
感谢各位朋友们能够看到结尾,找不到工作?某直聘已读不回?不妨看看这里超快回复,助力每一位程序员早日找到理想的工作