RabbitMQ 集群运维实践:仲裁队列与负载均衡
RabbitMQ 单机部署适合入门和小规模场景,但在真实业务中很容易遇到两个核心问题:可用性和吞吐量。
如果只有一台 RabbitMQ 服务器,一旦机器出现内存崩溃、断电、主板故障或进程异常,生产者和消费者都会受到影响。另一方面,单台服务器的吞吐量也有上限,假设单机每秒只能处理 1000 条消息,而业务需要每秒 10 万条消息,仅靠升级单机硬件并不现实。
这时就需要引入 RabbitMQ 集群。集群可以让生产者和消费者在单个节点故障时继续连接到其他节点,也可以通过增加节点扩展整体消息处理能力。不过需要注意,RabbitMQ 集群默认并不等于消息高可用。集群中的节点会同步队列、交换机、绑定关系、虚拟主机等元数据信息,但普通队列中的消息默认只存在于队列所在节点上。如果该节点宕机,即使队列和消息都设置了持久化,也可能出现消息不可用或丢失的问题。
集群部署方式
RabbitMQ 集群对网络延迟比较敏感,因此生产环境中的多个节点应尽量放在同一个局域网内,并保持 RabbitMQ 版本一致,避免由于版本差异带来兼容性问题。
常见部署方式有两种。
多机多节点适合生产环境。每台服务器运行一个 RabbitMQ 节点,节点之间组成集群。这种方式可以真正提升服务可用性,当某台服务器故障时,其他服务器仍然可以继续提供服务。
单机多节点也叫伪集群,通常用于学习、验证和演示。它是在同一台机器上启动多个 RabbitMQ 节点,通过不同端口区分。由于所有节点都依赖同一台机器,一旦机器故障,整个集群都会不可用,因此不适合生产环境。
普通队列的宕机问题
完成普通 RabbitMQ 集群搭建后,可以通过一个简单现象理解它的局限。
在管理平台中分别创建两个普通队列,例如 testQueue 和 testQueue2,并指定不同的主节点。创建完成后,从任意集群节点都能看到这些队列,因为队列元数据已经同步到所有节点。
接着向 testQueue 发送一条消息。此时在管理平台中可以看到队列中存在消息,看起来每个节点都能访问它。但如果停止 testQueue 主节点所在的 RabbitMQ 应用:
bash
rabbitmqctl -n rabbit stop_app
其他节点上虽然还能看到集群状态变化,但 testQueue 中的消息将不可用。原因是普通队列的消息并没有复制到其他节点,其他节点只是转发访问请求到队列所在节点。类似地,如果停止 testQueue2 的主节点,testQueue2 中的数据也会受到影响。
这说明普通集群主要解决的是节点入口和元数据同步问题,并不能直接解决队列消息的高可用问题。要让队列数据在节点故障后仍然可用,需要使用仲裁队列。
仲裁队列
仲裁队列是 RabbitMQ 提供的一种持久化、复制型 FIFO 队列,底层基于 Raft 一致性算法实现。它可以在多个 RabbitMQ 节点之间复制队列数据,当某个节点故障时,队列仍然可以继续对外提供服务。
仲裁队列从 RabbitMQ 3.8 开始引入,用来替代传统镜像队列。经典镜像队列曾经是实现队列高可用的重要方式,但由于设计上存在一些问题,后续逐渐被仲裁队列取代。
使用仲裁队列后,队列会拥有多个副本,其中一个是主副本,其余是从副本。生产者和消费者主要与主副本交互,主副本再把操作复制给从副本。当主副本所在节点下线时,集群会从从副本中选出新的主副本,继续提供服务。
Raft 一致性机制
Raft 是一种分布式一致性算法,用于管理复制日志。它的目标是在多个节点之间达成一致,即使部分节点故障、网络抖动或发生分区,也能尽量保证系统状态可靠。
分布式系统通常会使用多个副本来消除单点故障,但副本越多,越需要解决副本之间如何保持一致的问题。共识算法就是为了解决这个问题,常见算法包括 Paxos、Raft、Zab 和 Gossip。
Raft 的核心思想可以拆成三个部分:选举主节点、复制日志、保证安全性。
在 Raft 集群中,节点会处于三种角色之一:
Leader:负责处理客户端请求,并把请求转换成日志同步给其他节点。Follower:接收来自 Leader 的日志和心跳,不主动处理客户端请求。Candidate:当 Follower 长时间没有收到 Leader 心跳时,会转为候选状态并发起选举。
Raft 采用任期机制管理时间。每个节点都会维护当前任期号,任期号会单调递增。节点通信时会携带任期号,如果发现自己的任期落后,就会更新为更大的任期;如果 Candidate 或 Leader 发现自己任期过期,会退回 Follower 状态。
节点之间主要通过两类 RPC 通信:
RequestVote:候选节点在选举时发起,用于请求其他节点投票。AppendEntries:Leader 发起,用于复制日志,也用于发送心跳。
选举过程通常如下。
- 某个 Follower 在选举超时时间内没有收到 Leader 心跳。
- 该节点增加自己的任期号,切换为 Candidate,并先给自己投一票。
- Candidate 向其他节点并行发送
RequestVote请求。 - 如果获得超过半数节点投票,它就成为新的 Leader。
- 如果发现其他节点已经成为 Leader,它会回到 Follower。
- 如果没有节点获得多数票,则等待下一轮选举。
为了避免多个节点同时发起选举导致票数分散,Raft 使用随机选举超时时间。这样通常只有一个节点会率先超时并发起选举,从而更快获得多数票。
仲裁队列中的消息复制
仲裁队列使用多数派机制完成复制和确认。假设一个仲裁队列的副本数为 5,那么它会包含 1 个主副本和 4 个从副本,并且这些副本分布在不同 RabbitMQ 节点上。
当生产者发送消息时,主副本接收消息后会将消息复制给从副本。只有超过半数副本成功写入磁盘后,RabbitMQ 才会向生产者确认消息已写入。这样一来,少数较慢的副本不会拖垮整体性能,同时也能在部分节点故障时保留消息可用性。
如果主副本所在节点宕机,剩余副本会重新选举出新的主副本。只要存活副本仍然满足多数派要求,队列就可以继续工作。
创建仲裁队列
在 Spring 中可以通过 QueueBuilder 创建仲裁队列:
java
@Bean("quorumQueue")
public Queue quorumQueue() {
return QueueBuilder
.durable("quorum_queue")
.quorum()
.build();
}
使用 RabbitMQ Java 客户端时,可以通过队列参数指定类型:
java
Map<String, Object> param = new HashMap<>();
param.put("x-queue-type", "quorum");
channel.queueDeclare("quorum_queue", true, false, false, param);
也可以在 RabbitMQ 管理平台创建队列时,将队列类型选择为 Quorum。
仲裁队列默认副本数为 5,也就是 1 个主副本和 4 个从副本。如果集群节点数量少于 5,例如只有 3 个节点,那么会创建 1 主 2 从。如果集群节点数量大于 5,默认也只会选择其中 5 个节点承载副本。
创建完成后,在管理平台中可以看到仲裁队列后面带有副本数量标识。进入队列详情后,也可以查看主副本和从副本分别分布在哪些节点上。多个仲裁队列同时存在时,它们的主副本和从副本会分布在集群的不同节点中,一个节点可以同时承载多个队列的主副本和从副本。
仲裁队列宕机验证
向 quorum_queue 发送消息后,停止该队列主副本所在节点:
bash
rabbitmqctl -n rabbit stop_app
此时再观察其他节点,quorum_queue 中的消息仍然存在,并且队列主副本会从宕机节点转移到其他可用节点。例如原主副本位于 rabbit@hcss-ecs-2618,节点停止后,主副本可能会转移到 rabbit2@hcss-ecs-2618。
这就是仲裁队列相对普通队列的关键价值。普通队列只存放在某一个节点上,其他节点只是代理访问;仲裁队列会把队列数据复制到多个节点,在分布式环境下对消息可靠性和可用性提供更强保障。
HAProxy 负载均衡
集群解决了 RabbitMQ 多节点部署问题,但客户端连接仍然需要统一入口。
如果代码中直接连接某个节点,例如 node1,当 node1 故障时,客户端就会连接失败。即使所有节点都正常,如果大量客户端都连接到同一个节点,也会造成该节点网络负载过高,而其他节点资源利用不足。
引入负载均衡后,客户端只需要连接统一入口,后端流量由负载均衡器分发到不同 RabbitMQ 节点。这样既能降低单节点连接压力,也能在节点故障时把流量转移到其他节点。
RabbitMQ 集群常见负载均衡方案包括客户端内部负载均衡、HAProxy、LVS 等。HAProxy 是一种常用的软件负载均衡器和 TCP/HTTP 代理服务器,可以用于分发网络流量,提高服务可靠性和性能。
需要注意,HAProxy 自身也可能成为单点。如果 HAProxy 主机宕机或网卡故障,即使 RabbitMQ 集群仍然正常,客户端连接也会中断。因此生产环境中通常会使用 Keepalived 等方案为 HAProxy 做主备高可用,在主 HAProxy 故障时自动切换到备用节点。
通过 HAProxy 访问 RabbitMQ 集群
引入 HAProxy 后,应用侧使用 RabbitMQ 的方式基本不变,只需要把 RabbitMQ 的地址和端口改成 HAProxy 暴露的地址和端口。
Spring Boot 配置示例:
yaml
spring:
rabbitmq:
addresses: amqp://study:study@124.71.229.73:5670/bite
声明一个仲裁队列:
java
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();
}
}
使用 RabbitTemplate 发送消息:
java
@RequestMapping("/cluster")
public String cluster() {
rabbitTemplate.convertAndSend("", Constant.CLUSTER_QUEUE, "quorum test...");
return "发送成功!";
}
也可以使用 RabbitMQ 原生 Java 客户端发送消息:
java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
public class ClusterProducer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("124.71.229.73");
factory.setPort(5670);
factory.setVirtualHost("bite");
factory.setUsername("study");
factory.setPassword("study");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
Map<String, Object> param = new HashMap<>();
param.put("x-queue-type", "quorum");
channel.queueDeclare("test_cluster", true, false, false, param);
String msg = "hello cluster~~";
channel.basicPublish("", "test_cluster", null, msg.getBytes());
System.out.println(msg + "消息发送成功");
channel.close();
connection.close();
}
}
测试时,只要 HAProxy 后端仍然有可用 RabbitMQ 节点,消息就可以正常发送。即使停止某一个 RabbitMQ 节点:
bash
rabbitmqctl -n rabbit stop_app
应用仍然可以通过 HAProxy 继续发送消息,队列中的消息也会继续增加。节点恢复后:
bash
rabbitmqctl -n rabbit start_app
仲裁队列会重新同步副本,恢复到更完整的集群状态。
总结
RabbitMQ 集群可以提升服务入口的可用性和整体处理能力,但普通队列并不会自动复制消息。要在节点故障时保障队列数据可用,应使用基于 Raft 的仲裁队列。
仲裁队列通过主副本、从副本、多数派确认和主副本重新选举机制,让消息在多个节点之间可靠复制。再配合 HAProxy 提供统一访问入口,可以让客户端不再感知具体 RabbitMQ 节点,从而同时获得更好的可用性、扩展性和运维便利性。