仲裁队列
概述
1)RabbitMQ 普通队列在一个节点宕机之后,其他节点无法读写宕机节点的队列,为了解决这个问题,引入了仲裁队列.
2)仲裁队列通过 Raft 协议,实现了不同节点间队列消息数据的复制,使得在 创建这个队列的节点 宕机时,其他节点仍然可以使用该队列进行服务.
Ps:仲裁队列时 RabbitMQ3.8 版本的重要改动,是 镜像队列 的替代方案(设计上有缺陷). 镜像队列已被弃用,并计划在将来的版本中移除.
Raft 协议
概述
在分布式系统中,为了解决单点问题,通常会使用副本(从节点)来进行容错,但这又引入了另外一个问题——“如何保证副本之间的一致性?”.
共识算法
就是来解决这个问题的,它可以使得一些节点发生故障、网络分区或其他问题情况下,也能保证系统的一致性和数据可靠性,而 Raft
就是 共识算法
中的一种.
Raft 算法将一致性问题分解为三个子问题:Leader选举、日志复制、安全性
Ps:除了 Raft 还有很多其他的共识算法,例如 Paxos、Zab、Gossip
基本概念
1)节点角色
在 Raft 算法中,每个节点都有以下 3 个角色之一:
Leader(领导者)
:负责处理所有客户端的请求,并将这些请求作为日志,发送给所有的 Follower. Leader 会定期向所有 Follower 发送心跳消息,告诉他们 “我是领导,我还活着”,防止 Follower 进入选举流程.Follower(跟随者)
:跟随者 不直接处理客户端请求,而是接受 Leader 发送的日志,并使用这些信息.Candidate(候选者)
:当 跟随者 在一段时间内没有收到 Leader 的心跳消息,就会变成 Candidate,并开始进入选举流程.’
大致流程:
所有节点在刚启动时都是 follow 状态,在一段时间内如果没有收到来自 leader 的心跳,就会从 follower 切换到 candidate,发起选举. 如果收到多个投票(> n/2,包括自己的一票),则切换到 leader 状态,然后一直工作到它发生异常为止.
2)任期(term)
从一次选举开始,到一个 leander 最后宕机,就是一次任期. 如果一次选举没有选出 leader,这个任期会以没有 leader 而结束.
每个节点会存储当前任期号(current term number),并且会随着每次选举成功后
单调递增.
节点之间通信的时候会交换任期号:
- 如果一个 跟随者 节点的 任期号 小于其他节点,他就会将自己的任期号更新为较大的那个值.
- 如果一个 候选者 或 领导者 节点的 任期号过期了(请求当leader票 或 发送心跳包 时顺带判断),他就会立即回到 follower 状态.
- 如果一个节点收到了带着过期的任期号的请求,那么他会拒绝这次请求.
例如新的主节点的任期号为2,而 旧的主节点恢复了连接(任期号为1),给其他节点发送信号,其他节点一看,比我还小,就不听他的,并且告诉旧主机点,现在已经是 任期号为2 的年代了,此时旧的主节点就会把修改自己的任期号为2,并且将自己修改为 Follower 的角色.
Raft 算法中服务器节点之间采用 RPC 进行通信,主要有两类 RPC 请求:
- RequestVote RPCs:请求投票,由 候选者 在选举过程中发出.
- AppendEntries RPCs:追加条目,由 leader 发出,用来做日志复制和提供心跳机制.
选举流程(重点)
选主(Leader election)就是在集群中选择出一个主节点来负责特定工作(类似于公司的领导).
Note:正常情况下,集群中只有一个 Leader,剩下的节点都是 follower.
1)服务器刚启动的时候,所有节点都是 follow 状态,如果 follower 在 选举超时时间(election timeout)
内没有收到来自 leader 的心跳(可能:没有选出 leader、leader 挂了、leader 与 follower之间网络故障),则会主从发起选举. 步骤如下:
- 第一个超时的节点,自增当前任期号,然后切换为 候选人 状态,并先给自己投一票.
- 以并行的方式给集群中其他节点发送一个 RequestVote RPCs. (想要得到其他人的投票)
在这个过程中,可能会出现三种结果(后面会细致说明这三种情况):
- 赢得选举,称为 Leader(包括自己的一票).
- 其他节点赢得选举,它自行切换到 follower.
- 在 选举超时时间 内没有凑够 > n/2 的票数,就会保持 候选人状态,重新发出选举.
其他节点收到票后,判断如果符合任期要求
,就会会按照先来先服务原则(first-come-first-served)
值投给一个 候选人.
投票之后,会将自己的 任期 和 候选人 一致.
2)第一种情况:赢得选举后,新的 leader 会立即给所有节点发消息,避免其余节点触发新的选举.
3)第二种情况:其他节点赢得选举成为 Leader,并且收到了 Leader 的心跳包,判定任期符合要求,然后就会将自己转为 follower.
4)第三种情况:没有任何一个节点获得 半数投票. 例如所有 follower 同时变成 candidate,然后他们都给先给自己投一票,这样就没有 candidate 能超过半数投票了.
那岂不是形成了一个闭环(永远没有节点能超过半数投票)?
为了解决这个问题,Raft 采用随机选举超时时间. 选举时间会从一个 固定的区间(比如 150-300ms)种随机选择. 这样就可以确保大部分情况下不会发生所有人同时再次选举,发起拉票
的情况.
Ps:Raft 在线演示地址 https://raft.github.io/
消息复制
每个仲裁队列都有多个副本,包含 一个主 和 多个从副本,每个 副本都在不同的 RabbitMQ 节点上.
客户端(生产者 和 消费者)只会和 主节点 进行交互,主节点 再将这些命令复制到 从节点. 当 主节点 下线,其中一个 从节点 就会被选举成为 主节点,继续提供服务.
当之前 挂掉的主节点 重新上线后,发现自己的任期号 小于 当前 Leader 的任期号,就会将自己变为 Follower.
仲裁队列的使用
MQ 管理平台
Spring AMQP
1)配置
spring:
application:
name: rabbitmq
rabbitmq:
host: env-base
port: 5672
username: root
password: 1111
2)Bean
import com.cyk.rabbitmq.constants.MQConst
import org.springframework.amqp.core.Queue
import org.springframework.amqp.core.QueueBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class QuorumConfig {
@Bean
fun quorumQueue(): Queue = QueueBuilder
.durable(MQConst.QUORUM_QUEUE)
.quorum()
.build()
}
3)生产者接口
@RestController
@RequestMapping("/mq")
class MQApi(
private val rabbitTemplate: RabbitTemplate
) {
@GetMapping
fun quorum(): String {
rabbitTemplate.convertAndSend("", MQConst.QUORUM_QUEUE, "quorum msg 1")
return "ok"
}
}
4)触发端点,效果如下:
可以看到 仲裁队列 Node 这里 rabbit@mq1 +2
有一个 +2
的字样,代表整个队列有 2 个镜像节点.
进去可以看到 Leader 和 其他从节点是谁.
此时,将创建 仲裁队列 的节点下线(docker stop mq1),观察其他节点仲裁队列的信息,发现依旧可以提供服务.