RabbitMQ
的仲裁队列是一种基于 Raft
一致性算法实现的持久化,复制的 FIFO
队列。
RabbitMQ
节点间进行队列数据的复制,从而达到在一个节点宕机时,队列仍然可以提供服务的效果仲裁队列时 RabbitMQ 3.8
版本最重要的改动,它是镜像队列的替代方式。在 RabbitMQ 3.8
版本问世之前,镜像队列是实现数据高可用的唯一手段,但是它有一些设计的缺陷,这也是 RabbitMQ
提供仲裁队列的原因
RabbitMQ
安装需要迁移,可以参考官方提供的迁移指南什么是 Raft
官方文档:
Raft
是一种用于管理和维护分布式系统的一致性的协议,它是一种共识算法,旨在实现高可用性和数据的持久性。Raft
通过在节点间复制数据来保证分布式系统中的一致性,即使在节点故障的情况下也能保证数据不会丢失在分布式系统中,为了消除单点提高系统可用性,通常会使用副本来进行容错,但这会带来另一个问题,即如何保证多个副本之间的一致性?
Consensus Algorithm
)就是做这个事的,它允许多个分布式节点就某个值或一系列值达成一致性协议常见的共识算法有:
Raxos
:一种经典的共识算法,用于解决分布式系统中的一致性问题Raft
:一种较新的公式算法,Paxos
不易实现,Raft
是对 Paxos
算法的简化和改进,旨在易于理解和实现Zab
:ZooKeeper
使用的共识算法,基于 Paxos
算法,大部分和 Raft
相同,主要区别是对于 Leader
的任期。Raft
叫做 term
,Zab
叫做 epoch
。状态复制的过程中,Raft
的心跳从 Leader
向 Follower
发送,而 Zab
则相反Gossip
:其每个节点都是对等的,即没有角色之分。Gossip
算法中的每个节点都会将数据改动告诉其他节点(类似于传八卦)Raft
使用 Quorum
机制来实现共识和容错,我们将对 Raft
集群的操作必须得到大多数(>N/2
)节点的同意才能提交
Leader
,避免多个 Leader
并行工作,确保了 Leader
的唯一性 #高频面试当我们向 Raft
集群发起一系列读写操作时,集群内部究竟发生了什么呢?我们先来简单了解一下
Raft
集群必须存在一个主节点(Leader
),客户端向集群发起的所有操作都必须由主节点处理。所以 Raft
核心算法中的第一部分就是选主(Leader election
)。没有主节点集群就无法工作,先选出一个主节点,再考虑其他的事情
主节点会负责接受客户端发过来的操作请求,将操作包装为日志同步给其他节点,在保证大部分节点都同步了本次操作后,就可以安全地给客户端回应响应了。这部分工作在 Raft
核心算法中叫日志复制(Log Replication
)
因为主节点的责任非常大,所以只有符合条件的节点才能当选主节点。为了保证集群对外展现的一致性,主节点在处理操作日志时,也一定要谨慎,这部分在 Raft
核心算法中叫安全性(Safety
)
Raft
算法将一致性问题分解为了三个子问题:Leader
选举、日志复制和安全性
下面我们详细介绍下 Raft
的选主过程
选主(Leader election
)就是在集群中抉择出一个主节点来负责一些特定的工作。在执行了选主过程后,集群中的每个节点都会识别出一个特定的、唯一的节点作为 leader
在 Raft
算法中,每个节点都处于以下三种角色之一
Leader
(领导者):负责处理所有客户请求,并将这些请求作为日志项复制到所有 Follower
。Leader
定期向所有 Follower
发送心跳消息,以维持其领导者地位,防止 Follower
进入选举过程Follower
(跟随者):接收来自 Leader
的日志条目,并在本地应用这些条目。跟随者不直接处理客户请求Candidate
(候选者):当跟随者在一段时间内没有收到来自 Leader
的心跳消息时,它会变得不确定 Leader
是否仍然可用。在这种情况下,跟随者会转变角色为 Candidate
,并开始尝试通过投票过程,成为新的 Leader
在正常情况下,集群中只有一个 Leader
,剩下的节点都是 Follower
,下图展示了这些状态和他们之间的转换关系
Follower
状态,在一段时间内如果没有收到来自 Leader
的心跳,从 Follower
切换到 Candidate
,发起选举。majority
)的投票(含自己的一票),则切换到 Leader
的状态Leader
一般会一直工作,直到发生异常为止Raft
将时间划分成任意长度的任期(term
)。每一段任期从一次选举开始,在这个时候会有一个或者多个 Candidate
尝试成为 Leader
。
Leader election
之后,一个 Leader
就会一直管理集群,直到任期结束Leader
,这个时候这个任期会以没有 Leader
而结束(如下图 t3
)Term
更像是一个逻辑时钟(logic clock
)的作用,就可以发现哪些节点的状态已经过期。每一个节点都保存一个 current term
,在通信时带上这个 term
的值
每一个节点都存储着一个当前任期号(current term number
)。该任期号会xua随着时间单调递增。节点之间通信的时候会交换当前任期号
Candidate
或者 Leader
发现自己的任期号过期了,它就会立刻回到 Follower
状态RequestVote RPCs
:请求投票,由 Candidate
在选举过程中发出AppendEntries RPCs
:追加条目,由 Leader
发出,用来做日志复制和提供心跳机制Raft
采用一种心跳机制来触发 Leader
选举,当服务器启动的时候,都是 Follower
状态。如果 Follower
在 election timeout
内没有收到来自Leader
的心跳(可能没有选出 Leader
,也可能 Leader
挂了,或者 Leader
与 Follower
之间网络故障),则会主动发起选举
步骤如下:
Candidate
状态,并投自己一票RequestVote RPCs
给集群中的其他服务节点(企图得到他们的投票)在这个过程中,可能出现三种结果:
Leader
(包括自己的一票)Follower
majority
(多数派)投票,保持 Candidate
状态,重新发出选举投票要求:
Candidate
接下来对这三种情况进行说明:
第一种情况:赢得选举之后,新的 Leader
会立即给所有节点发送消息,广而告之,避免其他节点触发新的选举
第二种情况:比如有三个节点 A B C
,A B
同时发起选举,而 A
的选举消息先到达 C
,C
给 A
投了一票,当 B
的消息到达 C
时,已经不能满足上面提到的第一个约束,即 C
不会投票给 B
,这时候 A
就胜出了
A
胜出之后,会给 B
,C
发心跳消息,节点 B
发现节点 A
的 term
不低于自己的 term
,就知道已经有 Leader
了,于是把自己转换为 Follower
term
会自增 1
竞选主节点的节点在发送出消息之后,可能消息还没到远端的节点,投票就已经超过半数,选举成功了
第三种情况:没有任何节点获得 majority
投票。比如所有的 Follower
同事变成 Candidate
,然后他们都将票投给自己,那这样就没有 Candidate
能得到超过半数的投票了。
当这种情况发生的时候,每个 Candidate
都会进行一次超时响应,然后通过自增任期号来开启新一轮的选举,并启动另一轮的 RequestVote RPCs
。如果没有额外的措施,这种无结果的投票可能会无限重复下去
为了解决上述问题,Raft
采用随机选举超时时间(randomized election timeouts
)来确保很少产生无结果的投票,并且就算发生了也能很快地解决。
150-300ms
)中随机选择Raft
动画演示在线地址: https://raft.github.io/每个仲裁队列都有多个副本,它包含一个主和多个从副本。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<>();
parmm.put("x-queue-type", "quorum");
channel.queueDeclare("quorum_queue", true, false, false, param);
Type
为 Quorum
,指定主副本+2
字样,代表这个队列有 2 个镜像节点点进去,可以看到队列详情
仲裁队列发送和接收消息和普通队列一样
面对大量业务访问、高并发请求,可以使用高性能的服务器来提升 RabbitMQ
服务的负载能力。当单机容量达到极限时,可以采取集群的策略来对负载能力做进一步提升,但这里还存在一些问题
试想一下,如果一个集群中有 3 个节点,我们在写代码时,访问哪个节点呢?这时就存在两个问题
node1
,但是 node1
挂了,我们的程序也会出现问题,所以最好是有一个统一的入口,一个节点故障时,流量可以及时转移到其他节点node1
建议连接,那么 node1
的网络负载必然会大大增加,而其他节点又由于没有那么多的负载而造成硬件资源的浪费这时候负载均衡显得尤为重要
引入负载均衡之后,各个客户端的连接可以通过负载均衡分摊到集群的各个节点之中,从而避免前面的问题
这里主要讨论的是如何有效的对 RabbitMQ
集群使用软件负载均衡技术,目前主流的方式有在客户端内部实现负载均衡,或者使用 HAProxy
、LVS
等负载均衡软件来实现。
接下来我们来安装 HAProxy
,实现负载均衡
HAProxy
(High Availability Proxy
)是一个开源的负载均衡器和 TCP/HTTP
应用程序的代理服务器,它被设计用来提供高可用性,负载均衡和代理模式。HAProxy
主要用于分发网络流量到多个后端服务器,以提高网络的可靠性和性能
引入 HAProxy
之后,RabbitMQ
的集群使用和单机使用一样,只不过需要把 RabbitMQ
的 IP
和 port
改为 HAPorxy
的 IP
和 port
spring:
rabbitmq:
addresses: amqp://study:study@127.0.0.1:5670/coding
test_cluster
public static final String CLUSTER_QUEUE = "cluster_queue";
@Configuration
public class ClusterConfig {
@Bean("clusterQueue")
public Queue clusterQueue() {
return QueueBuilder.durable(Constant.CLUSTER_QUEUE).quorum().build();
}
}
@RequestMapping("/cluster")
public String cluster() {
rabbitTemplate.convertAndSend("", Constant.CLUSTER_QUEUE, "quorum test ...");
return "发送成功!";
}
cluster_queue
可以看到 IP
和端口号改成 HAProxy
的的之后,消息依然可以发送成功