生产者(Producer)创建消息发布到RabbitMQ,消息一般包含两个部分:消息体(payload)和标签(Label)。消息的标签用来表述这条消息,比如一个交换器名称和一个路由键。生产者把消息交由RabbitMQ之后会根据标签把消息发送给感兴趣的消费者(Consumer)。
消费者(Consumer)连接到RabbitMQ服务器并订阅队列。消费消息的消息体(payload)。在消息路由过程中,消息的标签会丢弃,存入到队列中的消息只有消息体。
Broker:消息中间件的服务节点。
对于RabbitMQ来说,一个RabbitMQ Broker可以简单看作一个RabbitMQ服务节点或者RabbitMQ服务实例。
生产者将业务数据包装成消息,发送(AMQP协议中这个动作对应命令为Basic.Publish)到Broker中。消费者订阅并接收消息(AMQP协议中这个动作对应的命令为Basic.Consume 或Basic.Get),经过可能的解包处理得到原始的数据再进行业务处理逻辑。业务处理逻辑可以使用Java的BlockingQueue进一步解耦提高处理效率。
队列(Queue):RabbitMQ内部对象用于存储消息。RabbitMQ中消息都只能存储在队列中,这一点和Kafka相反,Kafka将消息存储在topic(主题)这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件中的位移标识。多个消费者可以订阅同一个队列,消息会被平均分摊(Round-Robin)。RabbitMQ不支持队列层面的广播消费。
交换器(Exchange):生产者将消息发送到Exchange,由交换器将消息路由到一个或者多个队列中,如果路由不到或许返回给生产者,或许直接丢弃。RabbitMQ的交换器有四种类型,不同类型有不同的路由策略。
路由键(RoutingKey):生产者将消息发给交换器的时候,会指定一个RoutingKey,用来指定这个消息的路由规则,这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
绑定(Binding):RabbitMQ中通过绑定讲交换器与队列关联起来,绑定时一般会指定给一个绑定键(BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列。生产者将消息发送给交换器,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应队列。
RabbitMQ常用的交换器由fanout、direct、topic、headers这四种。AMQP协议里还提到另外两种类型:System和自定义。
headers:不依赖于路由键的匹配规则来路由消息,而是根据发送消息内容中的headers属性进行匹配。
fanout:会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
direct:会把消息路由到哪些BindingKey和RoutingKey完全匹配的队列中。
topic:与direct相似,是将消息路由到BindingKey和RoutingKey相匹配的队列中,但是这里匹配规则有些不同:
生产者发送消息:
消费者接受消息:
生产者和消费者都需要和RabbitMQ Broker建立连接,这个连接就是一条TCP连接,也就是Connection。建立之后可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID,信道时建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP指令都是通过信道完成的。RabbitMQ采用了类似NIO的做法,TCP连接复用减少开销。
本身包含三层:
AMQP说到底还是一个通信协议,通信协议都会涉及报文交互,从low-level来说,AMQP是应用层协议,其填充于TCP协议层的数据部分。从high-level来说,AMQP是通过协议命令进行交互的。AMQP协议可以看作一系列结构化命令的集合,这里的命令代表一种操作。
ConnectionFactory创建Connection
Connection创建Channel,Channel声明交换器(channel.exchangeDeclare)和队列(channel.queueDeclare)
参数说明:
不带参数的queueDeclare方法默认创建一个由RabbitMQ命名的(类似amq.gen-Lhfjakjhdfawef2ij的匿名队列)、排他的、自动删除的、非持久化的队列。
参数说明:
将队列和交换器绑定的方法
参数说明:
将交换器和队列绑定的方法
channel.basicPublish(
exchangeName,
routingKey,
new AMQP.BasicProperties.Builder()
.contentType("text/plain")
.deliveryMode(2)
.priority(1)
.userId("hidden")
.header(new HashMap<String, Object>())
.expiration("60000")
.build() ,
messageBodyBytes
);
RabbitMQ的消费模式分为两种,推(Push)和拉(Pull)。推模式采用Basic.Consume,拉模式调用Basic.Get进行消费
推模式接收消息一般通过实现Consumer接口或者继承DefaultConsumer类来实现。当调用与Consumer相关API方法时,不同的订阅采用不同的消费者标签(consumerTag)来区分彼此,在同一个Channel中的消费者也需要通过唯一的消费者标签以作区分。
basicConsume方法参数如下:
通过channel.basicGet方法可以单条获取消息,其返回值时GetReponse。
GetResponse response = channel.basicGet(QUEUE_NAME, false);
System.out.println(new String(reponse.getBody()));
channel.basicAck(reponse.getEnvelop().getDeliveryTag(),false);
为了保证消息可靠到达消费者,RabbitMQ提供了消息确认机制(message acknowledge)。消费者在订阅队列时可以指定autoAck参数,表示是否需要显示应答才会删除消息。未回复确认时,RabbitMQ会一直等待,除非消费该消息的消费者连接已经断开。消费者页可以调用channel.basicReject方法拒绝单个消息。使用channel.basicNack方法批量拒绝消息。
mandatory 和 immediate 是 channel.basicPublish 方法中的两个参数,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。
mandatory参数为true,交换机无法根据自身类型和路由键找到一个符合条件的队列会返回给生产者,如果为false,则消息被丢弃。
immediate参数为true,如果交换器将消息路由到队列时发现队列上不存在消费者,则这条消息不会存入队列中。所有匹配的队列都没有消费者时,该消息返回生产者。3.0去除
备份交换器(alternate exchange),生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下会丢失,如果设置了mandatory,需要添加ReturnListener逻辑。但是可以使用备份交换器将未被路由的消息存储在RabbitMQ中,需要时再去处理。
Map<String,Object> args = new HashMap<String,Object>();
args.put("alternate-exchange","myAe");
args.put("x-message-ttl",6000); // 设置过期时间
channel.exchangeDeclare("normalExchange","direct",true,false,args);
channel.exchangeDeclare("myAe","fanout",true,false,null);
channel.queueDeclare("normalQueue",true,false,false,null);
channel.queueBind("normalQueue","normalExchange","normalKey");
channel.queueDeclare("unroutedQueue",true,false,false,null);
channel.queueBind("unroutedQueue","myAe","");
RabbitMQ可以对消息和队列设置TTL(Time To Live)过期时间。
目前有两种方法设置消息的TTL,第一种方法通过队列属性设置,队列中所有的消息都有相同的过期时间。第二种方法时对消息本身进行单独设置,每条消息的TTL可以不同。如果两种方法一起使用,则消息的TTL以两者较小的那个数值为准。消息在队列中生存时间一旦超过设置的TTL值,就会变成死信(Dead Message)。
对于设置队列TTL属性的方法,一旦消息过期,就会从队列中抹去,而在第二种方法中,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期时在即将投递到消费者之前判定的。
第一种方法中,队列中已经过期的消息肯定在队列的头部,RabbitMQ只要定期从队头开始扫描是否有过期的消息即可。第二种方法里,每条消息的过期时间不同,如果要删除所有过期消息必须要扫描整个队列,所以等到消息被消费的时候再判定是否过期在进行删除。
死信队列(DLX),全称是Dead-Letter-Exchange。
消息变成死信有下面几个原因:
channel.exchangeDeclare("dlx_exchange","direct");
Map<String,Object> args = new HashMap<String,Object>();
args.put("x-dead-letter-exchange","dlx_exchange");
// 可以为DLX指定路由键,如果没有特殊指定,则使用原队列的路由键
args.put("x-dead-letter-routing-key","dlx-routing-key");
channel.queueDeclare("myqueue",false,false,false,args);
通过设定TTL和DLX模拟出延迟队列。消费者订阅的是死信队列而不是正常队列。
可以通过设置队列的x-max-priority参数来实现优先级队列,优先级高的消息具备优先被消费的特权。
持久化可以提高RabbitMQ的可靠性,以防在异常情况(重启,关闭,宕机等)下的数据丢失。持久化分为三个部分:交换器的持久化、队列的持久化、消息的持久化。可以引入镜像队列机制。
消息生产者消息发送之后,默认时不会返回任何信息给生产者。为了解决这个问题,提供了两种解决方式:
RabbitMQ客户端中与事务机制相关的方法有三个:channel.txSelect(设置为事务模式)、channel.txCommit(提交事务)和channel.txRollback(回滚事务)。事务会很大影响RabbitMQ的消息吞吐量。
轻量级的方式:发送方确认机制。生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配队列之后,RabbitMQ就发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确的到达了目的地。如果消息和队列是可持久化的,那么确认消息就会在消息写入磁盘后发出。RabbitMQ回传给生产者的确认消息中的deliveryTag包含了待确认消息的序号,此外RabbitMQ也可以设置channel.basicAck方法中的multiple参数,表示这个序号之前的所有消息都已经得到了处理。
事务机制是同步的,会阻塞发送端。发送方确认机制时异步的,可以通过回调方法来处理该确认消息。建议使用异步confirm模式,在Channel接口中提供的addConfirmListener方法可以添加ConfirmListener这个回调接口,包含两个方法:handleAck和handleNack。我们要为每个信道维护一个“unconfirm”的消息序号集合,每发送一条,集合中元素加1,回调时如果multiple为false就一条,为true时多条。
消费端有几点要注意:
消息分发默认时轮询(round-robin),可以加入流量控制,使用channel.basicQos(int prefetchCount),允许限制信道上的消费者所能保持的最大未确认消息数量。Basic.Qos的使用对于拉模式的消费方式无效。
消息顺序性是指消费者消费到的消息顺序和生产者发布的顺序是一致的。没有高级特性的情况下,默认能够保持顺序性。
弃用QueueingConsumer,建议使用继承DefaultConsumer的方式。
消息中间件的消息传输保障分为三个层级:
RabbitMQ集群中的所有节点都会备份所有的元数据信息,包括:
在RabbitMQ集群中创建队列,集群只会在单个节点而不是在所有节点上创建队列的进程并包含完整的队列(元数据、状态、内容)。这样只有队列的宿主节点(所有者)节点知道队列的所有信息,所有其他非所有者只知道队列的元数据和指向该队列存在的那个节点的指针。节点崩溃时,该节点的队列进程和关联的绑定都会消失,附加在那些队列上的消费者也会丢失其所订阅的信息,并且任何匹配该队列绑定信息的新消息也会丢失。
不同于队列那样有自己的进程,交换器实际上只是一个名称和绑定列表。当消息发布到交换器时,实际上是由所连接的信道将消息上的路由键同交换器的绑定列表进行比较,然后再路由消息。当创建一个新的交换器时,RabbitMQ所要做的就是将绑定列表添加到集群中的所有节点上。这样每个节点上的每个信道都可以访问到新的交换器。
多机多节点是指每台物理机器都安装了RabbitMQ,应当只在局域网内使用,广域网应当使用Federation或者Shovel。
命令行主要使用 rabbitmqctl join_cluster {nodename} 加入集群节点
rabbitmqctl forget_cluester_node {nodename}
RabbitMQ要求集群中至少有一个磁盘节点,其他节点可以是内存节点。当节点加入或者离开集群的时候,它们必须将变更通知到至少一个磁盘节点。如果唯一一个磁盘节点崩溃,集群可以继续收发消息,但是不能执行创建队列、交换器、绑定关系、用户,以及更改权限、添加和删除集群节点的操作。所以集群应该保障有两个或者多个磁盘节点的存在。
/usr/lib64/erlang/erts-8.0.3/bin/beam.smp -W w -A 64 -P 1048576 -t 5000000 -stbt db -zdbbl 32000 -K true -B i – -root /usr/lib64/erlang -progname erl – -home /root – -pa /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.6/ebin -noshell -noinput -s rabbit boot -sname rabbit@localhost -boot start_sasl -kernel inet_default_connect_options [{nodelay,true}] -sasl errlog_type error -sasl sasl_error_logger false -rabbit error_logger {file,"/var/log/rabbitmq/[email protected]"} -rabbit sasl_error_logger {file,"/var/log/rabbitmq/[email protected]"} -rabbit enabled_plugins_file “/etc/rabbitmq/enabled_plugins” -rabbit plugins_dir “/usr/lib/rabbitmq/lib/rabbitmq_server-3.6.6/plugins” -rabbit plugins_expand_dir “/var/lib/rabbitmq/mnesia/rabbit@localhost-plugins-expand” -os_mon start_cpu_sup false -os_mon start_disksup false -os_mon start_memsup false -mnesia dir “/var/lib/rabbitmq/mnesia/rabbit@localhost” -kernel inet_dist_listen_min 25672 -kernel inet_dist_listen_max 25672 status
RabbitMQ的日志默认存放在 $RABBITMQ_HOME/var/log/rabbitmq文件夹内。一般会创建两个日志文件:RABBITMQ_NODENAME-sasl.log 和 RABBITMQ_NODENAME.log两个日志文件。SASL(System Application Support Libraries,系统应用程序支持库)是库的集合,RabbitMQ在记录Erlang相关信息会写入这个文件,例如可以找到Erlang的崩溃报告。后者就会有RabbitMQ的应用服务的日志。
集群故障例如IDC整体停电、网线被挖断等,这时候需要通过集群迁移重新建立一个新的集群。
RabbitMQ集群迁移包括元数据重建、数据迁移、以及与客户端连接的切换。
元数据重建是指新的集群中创建原集群的队列、交换器、绑定关系、vhost、用户、权限和Parameter等数据信息。元数据重建之后才可以将原集群中的消息以及客户端连接迁移过来。
可以在原集群管理界面上点击 “Download broker definitions” 下载集群元数据信息文件
RabbitMQ Management插件提供了管理界面,也提供了HTTP API接口来调用提供监控数据。
Federation插件可以让多个交换器或者多个队列进行联邦,一个联邦交换器(federated exchange)或者一个联邦队列(federated queue)接收上游(upstream)的消息,这里的上游时指位于其他Broker上的交换器或者队列联邦交换器能够将原本发送给上游交换器(upstream exchange)的消息路由到本地的某个队列中;联邦队列则允许一个本地消费者接收到来自上游队列(upstream queue)的消息。
与Federation具备的数据转发功能类似,Shovel能够可靠、持续的从一个Broker的队列拉取数据并转发只另一个Broker的交换器。实际上是基于AMQP协议的转发器。
Shovel可以部署在源端也可以部署在目的端。有两种方式可以部署Shovel:静态方式(static)和动态方式(dynamic)。静态方式是指在RabbitMQ.config配置文件中设置,动态方式只指通过Runtime Parameter设置。
当集群消息堆积严重时,可以通过Shovel将队列中的消息移交给另一个集群,这是一备一的情况。如果需要一备多,可以采用镜像队列或者引入Federation。
Shovel工作在Federation的更低一层。监狱Federation从一个交换器中转发消息到另一个交换器(如果有必要可以确认消息是否被转发),Shovel只是简单地从某个Broker上的队列消费消息,然后转发消息到另一个Broker上的交换器而已。Shovel也可以再一台单独的服务器上去转发消息,例如将一个队列中的数据移动到另一个队列中。
持久化的消息在到达消息队列时就被写入到磁盘,如果可以,在内存中也会保留一份备份。非持久化消息在内存中,内存紧张是会被换入磁盘。这些都是依靠持久层完成。
持久层时一个逻辑上的概念,包含两部分:队列索引(rabbit_queue_index)和消息存储(rabbit_msg_store)。前者负责维护队列中落盘消息的信息,包括消息的存储地点,是否已经被交付给消费者,是否已经被消费者ack等。每个队列都有这个信息。后者则以键值对的形式存储消息,它被所有队列共享,在每个节点中有且只有一个。技术层面上来说,rabbit_msg_store具体还分为msg_store_persistent(负责持久化消息,重启后消息不丢失)和msg_store_transient(负责非持久化消息,重启后消息丢失)。默认在 /var/lib/mnesia/rabbit@hostname 路径下包含 queues、msg_store_persistent、msg_store_transient三个文件夹下,建议较小的消息存储在rabbit_queue_index中,而较大的消息存储在rabbit_msg_store中。消息大小界定可以通过queue_index_embed_msgs_below来配置,默认大小为4096单位B。
通常队列由rabbit_amqqueue_process和backing_queue两部分组成,前者负责协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的confirm和消费端的ack)等。后者时消息存储的具体形式和引擎,并向rabbit_amqqueue_process提供相关的接口以供调用。
如果消息投递的目的队列是空的,并且有消费者订阅了这个队列,那么消息不会经过队列直接发送给消费者。如果无法直接投递暂存入队列。消息入队后会随着系统负载在队列中不断流动,状态也不断变化,可能处于4种状态:
普通的没有设置优先级和镜像的队列,backing_queue的默认实现时rabbit_variable_queue,其内部通过5个子队列Q1、Q2、Delta、Q3、Q4来体现消息的各个状态。Q1,Q4只包含alpha状态的消息,Q2,Q3包含beta和gamma状态的消息,Delta只包含delta状态的消息。一般情况下,消息按照Q1→Q2→Delta→Q3→Q4这样顺序步骤进行流动。
消费者消息首先会对Q4获取消息,如果Q4为空尝试从Q3获取,如果Q3为空说明队列为空。
RabbitMQ从3.6.0开始引入惰性队列(Lazy Queue)。惰性队列尽可能地将消息存入磁盘中,在消费者消费相应消息会被加载到内存,这是为了支持更长的队列。default为默认内存模式,lazy为惰性队列模式:
args.put("x-queue-mode","lazy");
RabbitMQ可以对内存和磁盘使用量设置阈值,到达阈值时生产者会被阻塞直到对应项恢复正常。2.8.0还引入了流控(Flow Control)机制确保稳定性,为了避免消息发送速率过快而导致服务器难以支撑的情形。
一个连接(Connection)触发流控时会处于“flow”的状态,也就意味着这个Connection的状态每秒在blocked和unblocked之间来回切换数次,将消息发送的速率控制在服务器能够支持的范围之内。
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他Broker节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像的另一个节点上以保证服务的可用性。每一个配置镜像的队列都包含一个主节点(master)和若干个从节点(slave),如果master失效,slave加入时间最长的会提升为master。发送到镜像队列的所有消息会被同时发往master和其他所有的slave。除了发送消息(Basic.Publish)外所有动作都只会想master发送,然后由master将命令执行的结果广播给各个slave。
消费者与slave建立连接消费时实质上都是从master上获取消息,只不过看似从slave上消费而已。例如消费者与slave建立了TCP连接后执行Basic.Get操作,由slave转发给master,再由master准备好数据返回给slave,投递给消费者。这里的master和slave针对队列而言,队列可以均匀地散落在集群的各个Broker节点中以达到负载均衡地目的,真正的负载还是针对实际的物理机器而言,而不是内存中驻留的队列进程。
网络分区的恢复
首先选一个最信任的partition,Mnesia使用该partition中的状态,其他partitions中发生的变化都将丢失。
停止其他partitions中的所有nodes,之后重启这些nodes。当这些nodes重新加入cluster后将从信任的partition恢复状态。
最后还需重启信任的partition中的所有nodes以清除network partition的警告信息
Rabbitmq自动处理网络分区的3种模式
RabbitMQ提供了3种自动处理network partitions的方式:默认为ignore模式,也即需要手工处理
pause-minority mode:暂停少数模式;
pause-if-all-down mode:暂停-如果全部停止模式
autoheal mode:自动愈合模式
pause-minority mode:暂停少数模式
在pause-minority模式下,察觉其他nodes down掉后,RabbitMQ将自动暂停认为自己是少数派的 nodes(例如小于或等于总nodes数的一半),network partition一旦发生,“少数派”的nodes将立刻暂停,直至partition结束后重新恢复。这可以保证在network partition发生时,至多只有一个partition中的nodes继续运行。(牺牲可用性保证一致性)
若所有分区的nodes个数都小于总nodes个数一半,则意味着所有分区的nodes都会认为自己是少数派,即所有nodes都将暂停
pause-if-all-down mode:暂停-如果全部停止模式
http://www.rabbitmq.com/partitions.html
autoheal模式
在autoheal模式下一旦发生了partition,RabbitMQ将自动确定一个优胜partition,然后重启所有不在优胜partition中的nodes。
获胜的partition为拥有最多客户端连接的partition(若连接相同则为节点最多的partition)。
关于自动处理partitions的设置在配置文件的cluster_partition_handling参数中进行。
各自的适用场景
network partitions自动处理并不能保证cluster不出任何问题。
一般来说可作如下选择:
ignore:若网络非常可靠。所有nodes在同一机架,通过交换机连接,该交换机也是通往外部网络的出口。在cluster的某一部分故障时不希望其余部分受影响。或者cluster只有两个node。
pause_minority:网络较不可靠。cluster处于EC2的3个AZ中,假定每次至多只有其中一个AZ故障,想要剩余的AZ继续提供服务而故障的AZ中的nodes在AZ恢复后重新自动加入到cluster。
autoheal:网络很不可靠。与数据完整性相比更关注服务的持续性。cluster只有两个node。
RabbitMQ可以使用Firehose来实现消息追踪,Firehose的原理是将生产者投递给RabbitMQ的消息或者RabbitMQ投递给消费者的消息按照指定的格式发送到默认的交换器上,这个默认的交换器名称为:amp.rabbitmq.trace,它是一个topic类型的交换器。发送到这个交换器上的消息的路由键为publish.{exchangename}和deliver.{queuename},其中exchange和queuename为交换器和队列的名称,分别对应生产者投递到交换器的消息和消费者从队列中获取的消息。
使用rabbitmqctl trace_on [-p vhost]开启Firehose命令
rabbitmq_tracing插件相当于Firehose的GUI版本,会对流入流出的消息进行封装,然后将封装后的消息日志存入相应的trace文件中。
使用rabbitmq-plugins enable rabbitmq_tracing命令来启动插件。
除了应用内部实现负载均衡外,还可以使用HAProxy、Keepalived、LVS。
HAProxy提供高可用、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机。HAProxy实现来一种事件驱动、单一进程模型,支持非常大的并发连接数。
Keepalived通过自身健康检查、资源接管功能做高可用(双机热备),实现故障转移。
Keepalived采用VRRP(Virtual Router Redundancy Protocol,虚拟路由冗余协议),以软件形式实现服务的热备功能。通常情况下是将两台Linux服务器组成一个热备组(Master和Backup),同一时间内热备组只有一台主服务器Master提供服务,同时Master会虚拟出一个公用的虚拟IP地址,简称VIP。这个VIP只存在于Master上并对外提供服务。如果Keepalived检测到Master宕机或者服务故障,备份服务器Backup会自动接管VIP并成为Master,Keepalived将原Master从热备组中移除。当原Master恢复后,会自动加入到热备组,默认再抢占成为Master启到故障转移的功能。
LVS是Linux Virtual Server的简称,是4层负载均衡,建立在OSI模型的传输层上。支持TCP/UDP的负载均衡非常高效。
LVS主要由3部分组成