RabbitMQ实战:可用性分析和实现

本系列是「RabbitMQ实战:高效部署分布式消息队列」书籍的总结笔记。

上一篇介绍了各种场景下的最佳实践,大部分场景可以使用「发后即忘」的模式,不需要响应,如果需要响应,可以使用RabbitMQ的RPC模型。

RabbitMQ以异步的方式解耦系统间的关系,调用者将业务请求发送到Rabbit服务器,就可以返回了,Rabbit会确保请求被正确处理,即使遇到网络异常、Rabbit服务器崩溃、整个机房断电等特殊场景,针对这些场景,Rabbit提供了各种机制确保其可用性。

本篇通过总结可能出现的特殊场景,对Rabbit提供的可用性保证进行分析,学习它的实现方式,你会了解到:

  • 总结异常场景
  • 集群并处理失败
  • 连接丢失和故障转移
  • 主/备方式
  • 跨机房复制

推广下我的个人公众号「情情说」,第一时间分享我的工作、学习和生活,如果对你有帮助,希望可以关注下。

异常场景

在实际工作中,有很大一部分时间用在解决各种异常情况,比如针对用户输入的验证,JDK中提供的各种异常类,网络异常等,这些相对来说比较好解决。

Rabbit服务作为调用者和处理者的桥梁,至关重要,如果因为网络异常、单台服务器崩溃、机房瘫痪等原因导致Rabbit服务不可用,会影响所有依赖的业务系统。

网络异常

处理者和服务端是通过长连接交互的,这样可以将消息实时推送,网络异常可能会导致长连接断开,如果客户端无法感知,处理者将接收不到任何消息,这种情况称为「连接丢失」。

通过捕获连接异常,进行重连,可以解决这种问题,另外,Rabbit客户端进行了封装,很容易处理这种问题。

服务器崩溃

如果只有一台服务器服务,服务器崩溃将导致服务不可用,一般会使用集群将多个服务器看成一个整体对外提供服务,这样,单台服务器崩溃不会影响整体的服务。

使用集群后,就要考虑一些问题:

  • 客户端连接到哪台服务器是随机的,而一个队列只会在某个服务器中,所以,每台服务器都要保存队列元数据(类似索引),并且可从其他服务器获取实际的队列数据;
  • 服务器崩溃,会导致非持久化的队列、交换器丢失,客户端端重连后,要再次进行创建,但未消费的消息将无法恢复;
  • 如果队列、交换器、消息等是持久化的,如何进行恢复呢,Rabbit提供了几种方式进行处理,后面会详细介绍;
  • 订阅者也需要重新建立连接,进行监听;
机房瘫痪

如果考虑机房瘫痪,就要建多个数据中心,RabbitMQ提供了一种机制,可以方便地在不同数据中心的Rabbit间复制消息。

集群并处理失败

RabbitMQ最优秀的功能之一就是其内建集群,主要用于完成2个目标:

  • 允许消费者和生产者在Rabbit节点崩溃的情况下继续运行;
  • 通过添加更多的节点线性扩展消息通信的吞吐量;
集群架构

RabbitMQ会始终记录四种类型的内部元数据(类似索引):

  • 队列元数据:队列名称和它的属性;
  • 交换器元数据:交换器名称、类型和属性;
  • 绑定元数据:一张简单的表格展示了如何将消息路由到队列;
  • vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性;

当引入集群时,就需要追踪新的元数据类型:集群节点位置,以及节点与已记录的其他类型元数据的关系。

不是每个节点都有所有队列的完全拷贝,如果在集群中创建队列,只会在单个节点上创建完整的队列信息(元数据、状态、内容),所有其他节点只知道队列的元数据和指向该队列的节点指针。

如果节点崩溃了,附加在队列上的消费者也就无法接收新的消息了。可以让消费者重连到集群并重新创建队列,这种做法仅当队列没设置持久化时才可行,这是为了确保当失败的节点恢复后加入集群,节点上的队列消息不会丢失。

为什么不将队列内容和状态复制到所有节点:第一,存储空间,如果每个集群节点都拥有所有队列的完全拷贝,添加新节点不会带来更多存储空间;第二,性能,消息的发布者需要将消息复制到每一个集群节点,对于持久化消息,网络和磁盘复制都会增加。

而交换器只是一张查询表,而非实际的消息路由器,因此将交换器在整个集群中进行复制会更加简单

可以把每个队列想象成节点上运行的进程,每个进程拥有自己的进程ID,交换器只是路由模式列表和匹配消息应发往的队列进程ID列表。

集群架构中的交换器和队列

每个Rabbit节点,要么是内存节点,要么是磁盘节点,单节点系统只运行磁盘类型的节点,在集群中,可以选择配置部分节点为内存节点。

在集群中声明队列、交换器或绑定的时候,这些操作直到所有集群节点都成功提交元数据变更后才返回。

RabbitMQ只要求集群中至少有一个磁盘节点,如果只有一个磁盘节点,刚好又崩溃了,集群可以继续路由消息,但不能创建队列、交换器、绑定、添加用户、更改权限等操作。所以,建议设置两个磁盘节点,当内存节点重启后,会连接到预先配置的磁盘节点,下载当前集群元数据拷贝,所以要将所有磁盘节点告诉内存节点。

镜像队列

前面提到,队列只会在集群中的一个节点,节点崩溃后,队列消息就会丢失,RabbitMQ2.6版本之后,提供了镜像队列,一旦主队列不可用,从队列将被选举为新的主队列。

对于镜像队列,除了将消息按照路由绑定规则投递到合适的队列,也会将消息投递到镜像队列的从拷贝。

对于发送方确认消息,Rabbit会在所有队列和队列的从拷贝安全地接收到消息时,才会通知发送方。

另外,使用镜像队列时,有一个问题:如果主拷贝节点发送故障,从队列会选举Wie主队列,所有该队列的消费者需要重新附加并监听新的队列主拷贝。对于通过故障节点进行连接的消费者,可以通过丢失到节点的TCP连接检测到,但对于那些通过节点附加到镜像队列且正常运行的消费者将无法检测到。

Rabbit通过给消费者发送一个消费者取消通知,告知不再附加在队列主拷贝了,需要重新连接。

连接丢失和故障转移

这一小节主要讨论消费者如何检测连接丢失,并进行重连操作。

处理到集群的重连有多重策略,比较好的一种方式是使用负载均衡,不仅可以减少应用程序处理节点故障代码的复杂性,又能确保在集群中连接的平均分配。

关于负载均衡,网上介绍的比较多了,这里就不再过多介绍了,主要看看如何感知故障,并进行重连操作。

感知故障比较简单,当长连接断开时,会抛出异常,捕获对应的异常即可。

当集群节点出现故障时,应用程序需要考虑:下一个该连向哪里?这个工作已经交由负载均衡器决定。

关于重连处理,要考虑:

  • 如果重连到新的服务器,信道以及其上的所有消费循环都会失效,需要对他们进行重建;
  • 当进行重连时,所有的队列、绑定有可能都不存在了,需要重新构造队列和绑定。

主/备方式

当对可用性要求特别高时,不允许消息丢失,需要将队列、交换器、消息设置成持久化,如果一个节点崩溃了,在恢复之前,将无法转发消息,因为默认的群集架构不允许在集群其他节点创建队列,防止故障节点恢复后,历史消息丢失。

可以通过构建主/备机的独立RabbitMQ,也就是warren模式,解决这个问题。一个warren是指一对主/备独立服务器,并前置一套负载均衡器来处理故障转移。

主服务器和备用服务器之间没有协作,只有当主服务器崩溃时,备用服务器才会处理消息。可以保证,主节点故障后,通过备用节点重新创建队列、交换器继续服务,故障节点恢复后,可以继续消费主节点未消费的消息。

跨机房复制

在只有一个数据中心的时候,RabbitMQ集群对于提升消息通信性能来说是很棒的方案,但需要把消息从一个程序路由到另一个城市的时候,就比较麻烦了,可以通过Shovel解决。

Shovel是RabbitMQ的一个插件,可以使你能够定义RabbitMQ上的队列和另一个RabbitMQ上的交换器之间的复制关系。说白了就是生产者和消费者离得比较远。

通过在机房1创建一个新的队列,用于接收网站发布的消息,然后让shovel消费这些消息并重新将消息通过WAN连接发布到机房2上的交换器。

这样对于用户来说,只要发布到机房1的队列即可返回,减少了响应时间。机房1可以持续将消息发布到机房2上。

Shovel处理过程

通过上面的介绍可以看到,保证高可用需要做很多工作,可以根据业务对可用性的要求,选择不同的架构方式。

下一篇重点介绍RabbitMQ管理界面和监控。

欢迎扫描下方二维码,关注我的个人微信公众号 ~


情情说