乐行学院RabbitMQ学习教程 第一章 RabbitMQ介绍(可供技术选型时使用)

RabbitMQ介绍

1.RabbitMQ技术简介

官网地址:https://www.rabbitmq.com
Git地址:https://github.com/rabbitmq
开源协议:MPL1.1
开发语言:Erlang
维护公司:Pivotal
支持协议:
1.Amqp协议:高级消息队列协议(AMQP,Advanced Message Queuing Protocol)。
协议介绍:
2.STOMP协议:面向流文本的消息传输协议(STOMP,Streaming Text Oriented Messaging Protocol)
3.MQTT协议,MQTT(Message Queue Telemerty Transport)[2]是一种二进制协议,主要用于服务器和那些低功耗的物联网设备(IoT)之间的通信。
近1年时间发布的relase版本

发布版本发布时间
RabbitMQ 3.7.14 release2019-03-29
RabbitMQ 3.7.13 release2019-03-08
RabbitMQ 3.7.12 release2019-02-15
RabbitMQ 3.7.11 release2019-01-31
RabbitMQ 3.7.10 release2019-01-07
RabbitMQ 3.7.9 release2018-11-16
RabbitMQ 3.7.8 release2018-09-20
RabbitMQ 3.7.7 release2018-07-05
RabbitMQ 3.7.6 release2018-07-13
RabbitMQ 3.7.5 release2018-05-09
RabbitMQ 3.7.4 release2018-03-08

2.RabbitMQ其他扩展插件

2.1监控工具rabbitmq-management (官方提供)

官方介绍: https://www.rabbitmq.com/management.html
Git地址:https://github.com/rabbitmq/rabbitmq-management
基于http的B/S架构管理工具

2.2学习手册 RabbitMQ Tutorials(官方提供)

官方介绍: https://www.rabbitmq.com/getstarted.html
Git地址:https://github.com/rabbitmq/rabbitmq-management
其中包含以下语言:
• C#
• C# (with Visual Studio)
• Clojure
• Common Lisp
• Dart
• Elixir
• Erlang
• Go
• Haskell
• JavaScript (with Node and amqp-node) (using callbacks)
• JavaScript (with Node and amqp-node) (using promises/futures)
• Java
• Java (with IntelliJ IDEA)
• Kotlin
• PHP (with php-amqplib)
• PHP (with php-amqp)
• PHP (with queue-interop)
• Perl
• Python (with Pika)
• Ruby (with Bunny)
• Scala
• Swift
• Spring AMQP
• SoapUI

2.3Java语言客户端 rabbitmq-java-client(官方提供)

官方介绍: https://www.rabbitmq.com/api-guide.html
Git地址:https://github.com/rabbitmq/rabbitmq-java-client

3 RabbitMQ基本概念讲解

3.1 RabbitMQ核心原理图

在这里插入图片描述
在这里插入图片描述

3.2 Producer:消息生产者

生产者创建消息,然后发布到RabbitMQ中。消息一般可以包含2个部分:消息体和标签label)。消息体也可以称之为payload,在实际应用中,消息体一般是一个带有业务逻辑结构的数据,比如一个JSON字符串。当然可以进一步对这个消息体进行序列化操作。消息的标签来表述这条消息,比如一个交换器的名称和一个路由键。生产者把消息交由RabbitMQ,RabbitMQ之后会根据标签把消息发送给感兴趣的消费者(Consumer)

3.3 Cusumer:消息消费者

消费者连接到RabbitMQ服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体(payload)。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体,也就不知道消息的生产者是谁,当然消费者也不需要知道。

3.4Broker:消息中间件的服务节点

对于RabbitMQ来说,一个RabbitMQBroker可以简单地看作一个RabbitMQ服务节点,或者RabbitMQ服务实例。大多数情况下也可以将一个RabbitMQBroker看作一台RabbitMQ服务器。

3.5 Queue:队列,是RabbitMQ的内部对象,用于存储消息

RabbitMQ消息都只能存储在队列中,这一点和Kafka这种消息中间件相反。Kafka将消息存储在topic(主题)这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件中的位移标识。RabbitMQ的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin即轮询)多个消费者进行处理,而不是每个消费者都收到所有的消息并处理
RabbitMQ不支持队列层面的广播消费,如果需要广播消费,需要在其上进行二次开发,处理逻辑会变得异常复杂,同时也不建议这么做。

3.6 Connection 连接

是一个socket连接,它封装了socket协议相关部分逻辑。可以理解为一个Connection就是一个Tcp连接。因为每次创建连接会效率比较低,可以使用RabbitMQ连接池

3.7 Channel 管道

些应用需要与 AMQP 代理建立多个连接。无论怎样,同时开启多个 TCP 连接都是不合适的,因为这样做会消耗掉过多的系统资源。AMQP 0-9-1 提供了通道(channels)来处理多连接,可以把通道理解成共享一个 TCP 连接的多个轻量化连接(通常每个thread创建单独的channel进行通讯)。
消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。

3.8 routing key 路由KEY

生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。 在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。 RabbitMQ为routing key设定的长度限制为255 bytes.

3.9 Binding 绑定

RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。
在这里插入图片描述

3.10 Binding key 绑定KEY

在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。这个将在Exchange Types章节会列举实际的例子加以说明。 在绑定多个Queue到同一个Exchange的时候,这些Binding允许使用相同的binding key。 binding key 并不是在所有情况下都生效,它依赖于Exchange Type,比如fanout类型的Exchange就会无视binding key,而是将消息路由到所有绑定到该Exchange的Queue。

3.10 Exchange:交换机

生产者将消息发送到Exchange(交换器,通常也可以用大写的"X"来表示),由交换器将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃。这里可以将RabbitMQ中的交换器看作一个简单的实体。
在这里插入图片描述
RabbitMQ常用的交换器类型有fanout、direct、topic、headers这四种。

3.10.1 Fanout: 扇型交换机

在这里插入图片描述
它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中,不用管routeKey的事。

3.10.2 Direct: 直连交换机

在这里插入图片描述
直连交换机是一种带路由功能的交换机,一个队列会和一个交换机绑定,除此之外再绑定一个routing_key,当消息被发送的时候,需要指定一个binding_key,这个消息被送达交换机的时候,就会被这个交换机送到指定的队列里面去。同样的一个binding_key也是支持应用到多个队列中的。这样当一个交换机绑定多个队列,就会被送到对应的队列去处理。

3.10.3 Topic: 主题交换机

在这里插入图片描述
主题交换机必须是生产者发布消息指定的routingKey和消费者在队列绑定时指定的routingKey完全相等时才能匹配到队列上,与direct不同,topic可以进行模糊匹配,可以使用星号*和井号#这两个通配符来进行模糊匹配,其中星号可以代替一个单词;主题类型的转发器的消息不能随意的设置选择键(routing_key),必须是由点隔开的一系列的标识符组成。标识符可以是任何东西,但是一般都与消息的某些特性相关。一些合法的选择键的例子:”quick.orange.rabbit”,你可以定义任何数量的标识符,上限为255个字节。 #井号可以替代零个或更多的单词,只要能模糊匹配上就能将消息映射到队列中。当一个队列的绑定键为#的时候,这个队列将会无视消息的路由键,接收所有的消息。

3.10.4 Topic: 头交换机

在这里插入图片描述
首部交换机和扇形交换机都不需要路由键routingKey,交换机时通过Headers头部来将消息映射到队列的,有点像HTTP的Headers,Hash结构中要求携带一个键“x-match”,这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了。相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串(string)而是Object类型。

3.11 Virtual Host 虚拟机

RabbitMQ也有类似的权限管理。在RabbitMQ中可以虚拟消息服务器VirtualHost,每个VirtualHost相当月一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通。

3.12 Message acknowledgment

在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑…

3.13 Message durability

如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。由于这里仅为RabbitMQ的简单介绍,所以这里将不讲解RabbitMQ相关的事务。

3.14 Prefetch count

在这里插入图片描述
如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。

3.15 Time To Live(TTL)延时队列

延迟队列存储的对象肯定是对应的延时消息,所谓”延时消息”是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。

3.16 Dead Letter Exchanges(DLX)死信队列

RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由。
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key:指定routing-key发送
队列出现dead letter的情况有:
消息或者队列的TTL过期
队列达到最大长度
消息被消费端拒绝(basic.reject or basic.nack)并且requeue=false
利用DLX,当消息在一个队列中变成死信后,它能被重新publish到另一个Exchange。这时候消息就可以重新被消费。

3.17 RabbitMQ集群

RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的。 下面先来看下RabbitMQ集群的整体方案:
在这里插入图片描述
上面图中采用三个节点组成了一个RabbitMQ的集群,Exchange A(交换器,对于RabbitMQ基础概念不太明白的童鞋可以看下基础概念)的元数据信息在所有节点上是一致的,而Queue(存放消息的队列)的完整数据则只会存在于它所创建的那个节点上。,其他节点只知道这个queue的metadata信息和一个指向queue的owner node的指针。

3.17.1 RabbitMQ集群

RabbitMQ集群会始终同步四种类型的内部元数据(类似索引):
a.队列元数据:队列名称和它的属性;
b.交换器元数据:交换器名称、类型和属性;
c.绑定元数据:一张简单的表格展示了如何将消息路由到队列;
d.vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性;
因此,当用户访问其中任何一个RabbitMQ节点时,通过rabbitmqctl查询到的queue/user/exchange/vhost等信息都是相同的。

3.17.2 为何RabbitMQ集群仅采用元数据同步的方式

RabbitMQ集群会始终同步四种类型的内部元数据(类似索引):
a.队列元数据:队列名称和它的属性;
b.交换器元数据:交换器名称、类型和属性;
c.绑定元数据:一张简单的表格展示了如何将消息路由到队列;
d.vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性;
因此,当用户访问其中任何一个RabbitMQ节点时,通过rabbitmqctl查询到的queue/user/exchange/vhost等信息都是相同的。

3.17.3 RabbitMQ集群发送/订阅消息的基本原理

RabbitMQ集群的工作原理图如下:
在这里插入图片描述

场景1、客户端直接连接队列所在节点

如果有一个消息生产者或者消息消费者通过amqp-client的客户端连接至节点1进行消息的发布或者订阅,那么此时的集群中的消息收发只与节点1相关,这个没有任何问题;如果客户端相连的是节点2或者节点3(队列1数据不在该节点上),那么情况又会是怎么样呢?

场景2、客户端连接的是非队列数据所在节点

如果消息生产者所连接的是节点2或者节点3,此时队列1的完整数据不在该两个节点上,那么在发送消息过程中这两个节点主要起了一个路由转发作用,根据这两个节点上的元数据(也就是上文提到的:指向queue的owner node的指针)转发至节点1上,最终发送的消息还是会存储至节点1的队列1上。
同样,如果消息消费者所连接的节点2或者节点3,那这两个节点也会作为路由节点起到转发作用,将会从节点1的队列1中拉取消息进行消费。

3.18 RabbitMQ可靠性分析

生产者丢数据

RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。transaction机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。然而缺点就是吞吐量下降了。生产上用confirm模式的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,
则会发送一个Nack消息给你,你可以进行重试操作。

消息队列丢数据

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步
①、将queue的持久化标识durable设置为true,则代表是一个持久的队列
②、发送消息的时候将deliveryMode=2
这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据。在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入mirrored-queue即镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)

消费者丢数据

启用手动确认模式可以解决这个问题
①自动确认模式,消费者挂掉,待ack的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让
消息不断重试。
②手动确认模式
③不确认模式,acknowledge=“none” 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。

3.19 RabbitMQ吞吐量分析

你在Rabbit有一个队列,然后一些消费者从这个队列中消费。如果你根本没有设置QoS(basic.qos),那么Rabbit会把所有的队列消息都按照网络和客户端允许的速度推送给客户端。消费者将会飞速增加它们的内存占用,因为它们将所有消息都缓存在自己的RAM中。如果您询问Rabbit,队列可能会显示为空,但会有大量在客户端中,正准备由客户端应用程序处理的消息未被确认。如果您添加新的消费者,则队列中不会有消息发送给新的消费者。即使有其他消费者可用于更快地处理这样的消息,它们也只是在现有的客户端缓存,并且可能在那里很长一段时间。这是相当次优的。

因此,默认的QoS预取设置为客户提供了无限的缓冲区,这可能导致不良的行为和性能。但是,怎样的QoS预取缓冲区大小才是您应该设置的?设置的目的是让消费者保持工作饱和状态,同时尽量减少客户端的缓冲区大小,以便更多的消息留在Rabbit的队列中,来可供新消费者使用,或在消费者空闲时发送给消费者。

比方说Rabbit从这个队列中拿出一条消息需要50ms,把它放到网络上,然后到达消费者。客户端处理消息需要4ms。一旦消费者处理了消息,它就会发送一个ACK给Rabbit,这个Rabbit需要进一步发送50ms的信息给Rabbit进行处理。所以我们总共有104ms的往返时间。如果我们有1个消息的QoS预取设置,那么在这个往返行程完成之后,Rabbit不会发送下一个消息。因此,客户端每104ms只有4ms,或3.8%的时间忙碌,而我们希望百分之百的时间都在忙碌中。

如果我们在每个消息的客户端上执行总的往返时间/处理时间,则得到104/4 = 26。如果我们具有26个消息的QoS预取,就解决了我们的问题:假设客户端具有26个消息缓冲,等待处理。 (这是一个明智的假设:一旦你设置了basic.qos,然后从一个队列中消耗,Rabbit将发送尽可能多的消息到你订阅到客户端的队列,直到QoS限制。消息不是很大,带宽也很高,所以Rabbit很可能比你的客户端更快地发送消息到你的客户端,所以从假设的完整性来做所有的数学是合理的(也是更简单的)客户端缓冲区)。如果每条消息需要4ms的处理来处理,那么总共需要26×4 = 104ms来处理整个缓冲区。第一个4ms是第一个消息的客户端处理。客户端然后发出一个确认,然后继续处理缓冲区中的下一条消息。这一点需要50ms才能到达代理。代理向客户端发出一条新消息,这需要50ms的时间,所以到了104ms时间,客户端已经完成缓冲区的处理,代理的下一条消息已经到达,并准备好等待客户端来处理它。因此,客户端始终处于忙碌状态:具有较大的QoS预取不会使其更快;但是我们最大限度地减少了缓冲区的大小,从而减少了客户端消息的延迟:消息被客户端缓冲了,不再需要为了保持客户端的工作。事实上,客户端能够在下一条消息到达之前完全排空缓冲区,因此缓冲区实际上保持为空。

这个解决方案绝对没问题,只要处理时间和网络行为保持不变。但考虑一下如果网络突然间速度减半会发生什么情况:预取缓冲区不够大,现在客户端会闲置,等待新消息到达,因为客户端能够处理消息的速度比Rabbit能够提供新消息。

为了解决这个问题,我们仅仅需要翻倍(或几乎是双倍)QoS预取大小。如果我们把这个大小从26升到51,而客户仍然在每4ms处理消息,那么我们现在有51 * 4 = 204ms消息缓冲区中,其中4ms将用于处理消息,剩余200ms为发送一个ACK回Rabbit和接收下一个消息。因此,我们现在可以应对网络速度的减半。

但是,如果网络正常运行,现在将QoS预取提高一倍,意味着每个消息都会驻留在客户端缓冲区中一段时间,而不是在到达客户端时立即处理。再次,从现在51条消息的完整缓冲区开始,我们知道新消息将在客户端完成处理第一条消息100ms后开始出现在客户端。但是在这100ms内,客户端将会处理50个可用的100/4 = 25个消息。这意味着当新的消息到达客户端时,它将被添加到缓冲区的末尾,当客户端从缓冲区的头部移除时。因此,缓冲区总是保持50 – 25 = 25个消息长度,因此每个消息将在缓冲区中保持25 * 4 = 100ms,Rabbit发送给客户端以及客户端开始处理它的时间从50ms增加到150ms 。

因此,我们看到,增加预取缓冲区,使客户端可以应对恶化的网络性能,但是同时也会使得客户端繁忙,大大增加了网络正常运行时的延迟。

同样,排除掉网络的性能恶化,如果客户端开始处理每个消息40毫秒,而不是4ms,会发生什么?如果Rabbit的队列以前是稳定的(即入口和出口速率相同),它现在将开始快速增长,因为出口率降到了原来的十分之一。您可能会决定尝试通过添加更多的消费者来处理这种增长的积压,但现在有消息正在被现有客户端缓冲。假设26条消息的原始缓冲区大小,客户端将花费40ms处理第一条消息,然后将确认消息发送回Rabbit并移至下一条消息。 ack仍然需要50ms才能到达Rabbit,而Rabbit发出一个新的消息还需要50ms,但是在100ms内,客户端只处理了100/40 = 2.5个消息,而不是其余的25个消息。因此缓冲区在这个点上是25 – 3 = 22个消息长。现在来自Rabbit的新消息,不是立即处理,而是会位于第23位,落后于其他22个等待处理的消息,直到22 * 40 = 880ms后才会被客户端触及。考虑到从Rabbit到客户端的网络延迟仅为50ms,现在这个额外增加的880ms延迟相当于多增加了延迟的95%(880 /(880 + 50)= 0.946)。

更糟糕的是,如果我们将缓冲区大小加倍到51条消息以应对网络性能下降,会发生什么?第一条消息处理完毕后,会在客户端缓存50条消息。 100ms后(假设网络运行正常),一条新的消息将从Rabbit到达,客户端将处理这50条消息中的第三条消息(缓冲区现在为47条消息长)的一半,因此新消息将在缓冲区中是第48位,并且不会再触及直到47 * 40 = 1880ms之后。再一次,考虑到向客户端发送消息的网络延迟仅为50ms,现在这个1880ms的延迟意味着客户端缓冲占据了超过97%的延迟(1880 /(1880 + 50)= 0.974)。这可能是不可接受的:如果数据处理得很快,而不是在客户端收到数据后2秒,数据才可能是有效的和有用的!如果其他消费客户端空闲,他们无能为力:一旦Rabbit向客户端发送消息,消息就是客户端的责任,直到他们拒绝或拒绝消息为止。一旦消息被发送到客户端,客户端不能窃取彼此的消息。你想要的是让客户端保持忙碌,但是客户端尽可能少地缓存消息,这样消息就不会被客户端缓冲区延迟,因此新消费的客户端可以快速地接收到来自Rabbit队列的消息。

因此,如果网络变慢,缓冲区太小会导致客户端空闲,但如果网络正常运行,缓冲区太大会导致大量额外的延迟;如果客户端突然开始花费更长时间来处理每个缓冲区,消息比正常。很明显,你真正想要的是一个不同的缓冲区大小。这些问题在网络设备中是常见的,并且一直是很多研究的主题。主动队列管理算法试图尝试丢弃或拒绝消息,以避免消息长时间坐在缓冲区中。当缓冲器保持空闲(每个消息只遭受网络延迟,并且根本不在缓冲器中)并且缓冲器在那里吸收尖峰时,达到最低延迟。 Jim Gettys一直从网络路由器的角度来研究这个问题:局域网和广域网性能之间的差异正在遭受同样的问题。实际上,无论何时,在生产者(在本例中为Rabbit)和消费者(客户端应用程序逻辑)之间都有一个缓冲区,双方的性能可以动态变化,您将会遇到这样的问题。最近出现了一种名为Controlled Delay的新算法,它在解决这些问题上表现得很好。

作者声称他们的CoDel(“coddle”)算法是一个“无旋钮”算法。这实际上是一个谎言:这里有两个旋钮,他们都需要适当的设置。但是每次性能改变时都不需要改变它们,这是一个巨大的好处。我已经为我们的AMQP Java客户端实现了这个算法,作为QueueingConsumer的一个变种。虽然原来的算法是针对TCP层的,那么丢弃数据包是有效的(TCP本身会处理丢失数据包的重传),但在AMQP中这不太有礼拜!因此,我的实现使用Rabbit的basic.nack扩展来显式地将消息返回给队列,以便其他人可以处理它们。

使用它几乎和普通的QueueingConsumer一样,除了你应该提供三个额外的参数给构造函数来获得最好的性能。

首先是requeue,它设置当消息被阻塞,是否应该重新排序或丢弃。如果设置为FALSE,那么它们将被丢弃,这样可能会触发死信交换机制。

第二个是targetDelay,这是消息在客户端QoS预取缓冲区中等待的可接受时间(以毫秒为单位)。

第三个是interval,是以毫秒为单位的一个消息的预期最坏情况处理时间。这不一定是精确的,但在一个数量级内肯定有帮助。

您仍然应该适当地设置QoS预取大小。如果不这样做,可能是客户端会收到很多消息,然后如果他们在缓冲区中的时间太长,算法将不得不将它们返回给Rabbit。消息返回给Rabbit时,很容易产生大量额外的网络流量。一旦性能偏离规范,CoDel算法就意味着只会开始丢弃(或拒绝)消息,因此一个可行的例子可能会有所帮助。

同样,假设每个方向的网络遍历时间为50ms,并且我们期望客户端平均花费4ms的时间处理每条消息,但是这可以达到20ms。因此我们把CoDel的interval参数设置为20。有时网络速度减半,所以每个方向的遍历时间可以是100ms。为此,我们将basic.qos预取设置为204/4 = 51.是的,这意味着在网络正常运行的大部分时间内,缓冲区将保持25个消息(见前面的工作),但是我们认为这可以接受。我们预期每个消息将在缓冲区中驻留25 * 4 = 100ms,因此将CoDel的targetDelay设置为100。

正常运行时,CoDel不会碍事,很少有消息会被nacked。但是,如果客户端开始处理消息的速度比正常情况慢,CoDel会发现消息已经被客户端缓存了太久,那就将这些消息返回给队列。如果这些消息被重新发送,则它们将可用于发送给其他客户端。

这在目前是非常具有实验性的,也有可能看到CoDel不适合处理纯IP的AMQP消息的原因。另外值得记住的是,通过nacks重新发送消息是一个相当昂贵的操作,所以最好设置CoDel的参数来确保极少数的消息会在正常操作中被nacked。后台管理插件会是一个来检查有多少消息被nacked的简单方法。一如以往,评论,反馈和改进是最受欢迎的!

3.20 异常恢复

场景1: A先停, B后停
方案1: 该场景下B是Master,只要先启动B,再启动A即可。或者先启动A,再30秒之内启动B接口恢复镜像队列


场景2: A、B同时停机
方案2:该场景可能由于机房断电等原因造成的,只需在30秒之内连续启动A和B即可恢复镜像


场景3:A先停,B后停,且A无法恢复
方案3: 该场景是1场景的加强版,因为B是Master,所以等B起来以后,在B节点调用控制台命令:rabbitmqctl forget_cluster_node A解除与A的Cluster关系,再将新的Slave节点加入B即可重新恢复镜像队列


场景4: A先停,B后停,且B无法恢复
方案4:该场景是场景3的加强版,比较难处理,原因是因为Master节点无法恢复,早在3.1x时代之前没有什么好的解决方案,但是现在已经有解决方案了,在3.4.2以后的版本。因为B是主节点,所有直接启动A是不行的,当A无法启动的时候,也就没办法在A节点上调用之前的rabbitmqctl forget_cluster_node B命令了。新版本中forget_cluster_node支持–offline参数
这就意味着允许rabbitmqctl在理想节点上执行该命令,迫使RabbitMQ在未启动Slave节点中选择一个节点作为Master。当在A节点执行 rabbitmqctl forget_cluster_node --offline B时,RabbitMQ会mock一个节点代表A,执行 forget_cluster_node命令将B剔除cluster,然后A就可以正常的启动了,最后将新的Slave节点加入A即可恢复镜像队列


场景5:A先停、B后停,且A、B均无法恢复,但是能得到A或B的磁盘文件
方案5:这种场景更加难处理,只能通过恢复数据的方式去尝试恢复,将A与B的数据文件模式在$RABBIT_HOME/var/lib/目录中,把它拷贝到新的节点对应的mulxia,再将新的节点hostname改成A或B的hostname,如果是A节点(Slave)的磁盘文件,则按照场景4处理即可,如果是B节点(Master)的磁盘文件,则按照场景3处理即可,最后新的Slave加入新节点后完成恢复。

3.21 结束

本文是对于准备入门和准备开始做技术调研的程序员、架构师做的一次资料整理和加入了一些个人的见解。
本文章图片均来自CSDN网站其他人博客,部分内容也是参考其他人整理并编排。希望能帮助到大家。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值