RabbitMQ总结
特点
首先, 消息队列的三个好处: 异步, 解耦,削峰填谷, 除此以外,还有其他应用场景包括最终一致性, 广播等 RabbitMQ是一个开源的AMQP实现, 服务端用Erlang编写, 支持多种客户端,在易用性, 扩展性,高可用性等方面表现不俗, 具体特点:
- 可靠性(Reliability)
- 路由灵活(Flexible Routing)
- 消息集群(Clustering)
- 高可用(Highly Available Queues)
- 多种协议(Multi-protocol)
- 多语言客户端(Many Clients)
- 管理界面(Management UI)
- 跟踪机制(Tracing)
- 插件机制(Plugin System)
概念模型
基本概念
-
Message
消息, 消息头和消息体构成, 消息体不透明, 消息头由一系列可选属性组成, 包括routing-key, priority,delivery-mode等 -
Publisher
生产者, 发布消息的客户端应用程序 -
Exchange
交换器, 用来接收生产者发送消息并将这些消息路由给服务器的队列 -
Binding
绑定,用于消息队列和交换器之间的关联 -
Queue
队列,保存消息知道发送给消费者, 消息容器 -
Connection
网络连接 -
Channel
信道, 多路复用连接中的一条独立的双向数据流管道 -
Consumer
消息的消费者 -
Virtual Host 虚拟主机, 共享相同的身份认证和加密环境的独立服务器域, 是AMQP概念的基础, 连接时必须指定,默认是/
-
Broker 消息队列服务器实体
Exchange类型
direct, fanout, topic, headers。fanout和headers不处理路由键, headers性能差很多
- direct 消息中的路由键和Binding中的bindingKey一致,单播模式
- fanout
消息分到所有绑定的队列上, 子网广播 - topic
模式匹配分配消息, 路由键和模式进行匹配,识别'#'和'' '#'匹配0个或多个单词, ''匹配一个单词 - headers 在消息头上设置属性, 匹配属性信息
Virtual host
vhost 可以限制最大连接数和最大队列数, 达到限制后将报错, 并且可以设置vhost下的用户资源权限和Topic权限。用户资源权限指RabbitMQ用户在客户端执行AMQP操作命令时, 拥有的对资源操作和使用权限,分为三部分: configure, write, read.具体参考www.rabbitmq.com/access-cont… Topic权限参考www.rabbitmq.com/access-cont…, 一般情况下不会使用,只有使用了MQTT协议才可能用到
数据存储
消息两种类型: 持久化消息和非持久化消息, 两种消息都会被写入磁盘
- 持久化消息在到达队列时写入磁盘,在内存中保存一份备份, 内存吃紧, 消息从内存中清除,会提高一部分性能
- 非持久化消息只存在内存中, 内存压力大时,数据刷盘,节省内存空间
存储层分为两部分: 队列索引(index file)和消息存储(store file)
-
队列索引: rabbit_queue_index 索引维护队列的落盘消息的信息,如存储地点, 是否已被消费者接收, 是否被ack等。索引使用顺序的段文件存储, 后缀是.idx, 文件名从0开始累加, 每个段文件中包含固定的segment_entry_count条记录,默认值16384, 每个index从磁盘读取消息, 至少在内存中维护一个段文件,设置queue_index_embed_msgs_below要格外谨慎, 默认4k, 当消息小于4k时,消息存入了索引文件, 这样会被读到内存中,引起内存爆炸.
-
消息存储: rabbit_msg_store 消息以键值对的形式存储到文件, 一个虚拟主机的所有队列使用同一块储存,每个节点只有一个, 存储分为持久化(msg_store_persistent) 和短暂存储(msg_store_transient) 。store使用文件存储, 后缀. rdq, 以追加的方式写入文件, 大小超过指定限制(file_size_limit)后,会关闭文件,创建新文件, 文件名从0开始, 进行存储时, RabbitMQ会在ETC(Erlang Term Storage) 表中记录消息在文件中的位置映射和文件的相关信息
读取消息时,现根据消息ID找到对应存储的文件, 文件存在且未被锁住, 直接打开文件, 从指定位置读取消息, 文件不存在或者被锁住了,发送store进行处理
删除消息,从ETS表删除指定消息的相关信息, 同时更新消息对应的存储文件和相关信息, 在执行删除时,并不立即对文件中的消息进行删除, 仅仅标记垃圾数据, 当一个文件中都是垃圾数据时可以将文件删除, 当检测到两个文件中的有效数据可以合并成一个文件, 并且所有的垃圾数据的大小和所有文件的数据大小的比值超过阈值garbage_fration(默认值0.5),触发垃圾回收, 将文件合并, 执行合并的两个文件一定是逻辑上相邻的两个文件,合并逻辑
- 锁定两个文件
- 先整理前面文件的有效数据,再整理后面文件的有效数据
- 将后面文件的有效数据写入前面的文件
- 更新消息在ETS表中的记录
- 删除后面文件
队列结构
rabbit_amqqueue_process和backing_queue两部分组成. rabbit_amqqueue_process负责协议相关的消息处理, backing_queue是消息存储的具体形式和引擎,,并向rabbit_amqqueue_process提供相关的接口以供调用
其中rabbit_amqqueue_process 定义了队列四种状态1. alpha: 索引和内容都存内存 2. beta: 索引存内存 内容存磁盘 3.gama: 索引 内存 和磁盘都有,消息内容存磁盘 4. Delta消息索引和内容 都存磁盘
高级应用
rabbitMQ 作为RPC
集群连接恢复
参考www.rabbitmq.com/api-guide.h… , 通过设置factory.setAutomaticRecoveryEnabled(true)可以设置自动恢复的开关,默认已开启。通过factory.setNetworkRecoveryInterval(10000) 可以设置多长时间尝试恢复一次, 默认5s: com.rabbitmq.client.ConnectionFactory.DEFAULT_NETWORK_RECOVERY_INTERVAL
- 什么时候会触发自动恢复
- 连接的IO循环抛出异常
- 读取Socket超时
- 检测不到服务器心跳
- 在连接的IO循环中引发任何其他异常
- 如果客户端第一次连接失败,不会自动恢复连接,需要我们自己负责重试连接,记录失败的尝试,实现重试次数的限制
- 程序中调用了Connection.close, 不会自动恢复连接
- Channel-level的异常,不会自动恢复连接, 因为这些异常是应用程序语音问题,例如从不存在的队列中消费
- 在Connection和Channel上可以设置重新连接的监听器, 开始重连和重连成功后,会触发监听器,添加和移除监听,需要将Connection或Channel强制转换成Recoverable接口
((Recoverable)connection).addRecoveryListener()
((Recoverable)connection).removeRecoveryListener()
消息可靠性
- 客户端代码异常捕获
捕获异常后, 可以重试, spring通过
spring.rabbitmq.template.retry.enable=true进行重试 - RabbitMQ事务机制
channel.select();
chanenl.txCommit();
channel.txRollback();
- 发送端确认机制
spring:
rabbitmq:
publisher-confirm-type:correlated
channel.confirmSelect();
channel.waitForConfirmsOrDie(5000);
channel.addConfirmListener();
实现ConfirmCallback方法, 消息只要被rabbitmq broker接收到就会触发ConfirmCallback回调, ack为true,表示消息发送成功, ack为false表示消息发送失败
return返回模式
spring:
rabbitmq:
publisher-returns: true
rabbitTemplate.setMandatory(true)
设置投递失败模式,如果没有路由到Queue, 丢弃消息(默认), 配置mandatory返回给消息发送方ReturnCallBack(开启后), 实现ReturnCallback接口, 启动消息失败返回, 消息路由不到队列时会触发回调接口。另外一种就是设置备份交换器(Alternate Exchange)
- 消息持久化机制
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).build();
除了消息, 交换器和队列也可以进行持久化
- Broker高可用集群
- 消费者确认机制
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
channel.basicAck(deliveryTag, false);
channel.basicNack(deliveryTag, false, true);
channel.rejectAck(deliveryTag, false);
有三种模式, NONE, AUTO, MANUAL
spring.rabbitmq.listener.simple.acknowledge-mode=manual
-
消费端限流
-
内存告警和磁盘告警
默认情况下set_vm_memory_high_watermark值为0.4, 当RabbitMQ使用的内存超过总可用内存40%,会产生内存告警并阻塞所有生产者的连接, 一旦告警被解除, 会恢复正常. (总可用内存表示操作系统给每个进程分配的大小, 如32位系统分配2GB, 阈值820MB) 在出现内存告警后,所有的客户端连接都会被阻塞,阻塞分为blocking和blocked
- blocking: 没有发送消息的连接
- blocked: 试图发送消息的连接 处理方案: 如果出现了内存告警,并且机器还有可用内存, 通过命令调整内存阈值, 解除告警
rabbitmqctl set_vm_memory_high_watermark 1 rabbitmqctl set_vm_memory_high_watermark absolute 1GB #这个设置RabbitMQ服务重启后会还原,永久调整修改配置文件并重启 vim /etc/rabbitmq/rabbitmq.conf vm_memory_high_watermark.relative=0.4 vm_memory_high_watermark.absolute=1GB当磁盘剩余空间低于磁盘阈值时, RabbiMQ同样会阻塞生产者, 可以避免因非持久化消息持续换页而耗尽磁盘空间导致服务崩溃。默认情况下磁盘阈值50MB, 设置可以减少,但不能完全消除因为磁盘耗尽而导致崩溃的可能性, 比如在两次磁盘空间检测期间,磁盘空间从大于50M被耗尽到0M。 通过命令可以调整磁盘阈值, 临时生效, 重启恢复
## disk_limit为固定大小, 单位为MB GB rabbitctl set_disk_free_limit <disk_limit> ## 相对比值, 取值1.02.0之间 rabbitctl set_disk_free_limit mem_relative <fraction>在Broker节点的使用内存达到内存阈值前,会尝试将队列的消息存储到磁盘,以释放内存空间,这个动作叫做内存换页。持久化和非持久化消息都会被存储到磁盘中, 其中持久化的消息本身在磁盘中有份备份, 此时,会将持久化消息从内存中清除掉, 默认情况下,内存达到内存阈值的50%就会换页,也就是在默认内存阈值0,4的情况下, 内存超过0.2就会换页,通过修改配置文件,调整内存换页分页阈值
vm_memory_high_watermark_paing_ratio=0.75 -
RabbitMQ提供了基于credit flow的流控机制, 面向每个连接进行流控, 当单个队列达到最大流速,或多个队列达到总流速,会触发流控。
-
Qos机制, 可以限制Channel上接收到并未被Ack的消息数量, 超过这个数量限制RabbitMQ不会再往消费端推送信息, 可以防止大量消息瞬时从Broker送达消费端,这种模式仅对于推模式有效, 拉模式无效,不支持NONE Ack机制, channel.basicConsume方法之前,通过channel.basicQos之前可以设置数量。消息发送异步, 消息确认异步, 在消费者消费慢的时候,设置prefetchCount, 表示broker向消费者发送消息的时候,一旦发送了prefechCount个消息,没有一个消息确认就停止发送
-
-
消息幂等性
- 数据库唯一索引
- 检查机制,使用乐观锁,悲观锁
- 消息设置唯一ID
可靠性保证
可靠性分为At most once, At least once, Exactly once RabbitMQ支持最多一次和最少一次
最少一次考虑
- 消息生产者开启事务或者confirm 机制
- mandatory参数或者备份交换器
- 消息和队列都需要进行持久化处理
- 将autoAck设置false, 通过手动确认去确认已经正确消费的消息
最多一次 随意发送
恰好一次 RabbitMQ目前无法保证
- 消费者在消费完一条消息后向RabbitMQ发送ack命令,此时网络断开或其他原因, RabbitMQ没有收到确认命令, 就不会进行标记删除, 重新连接后,消费者会重复消费
- 生产者使用confirm机制, 发送后网络断开, 为了确保可靠性重发, 就会重复消费
追踪插件
Firehose功能实现消息追踪,记录每一次发送或消费记录, 进行调试,排错。原理是将消息按指定格式发送到默认交换机(amq.rabbitmq.trace),这是个topic类型交换机,路由键是publish.{exchangename}和deliver.{queuename}
## 开启firehose
rabbitmqctl trace_on [-p vhost]
## 关闭firehose
rabbitmqctl trace_off [-p vhost]
firehose默认关闭, 状态是非持久化的,重启后会还原默认的状态 rabbitmq_tracing 插件, 会将消息日志存入相应的trace文件中.
rabbitmq-plugins enable rabbitmq_tracing
TTL
过期时间设置分为队列过期时间和消息过期时间, 如果设置了队列过期时间,也设置了消息过期时间, 以时间短的为准。队列过期时,会将队列所有消息全部移除, 消息过期时,只有消息在队列顶端,才会判断是否过期(移除掉)
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 30000);
arguments.put("x-expires", 10000);
channel.queueDeclare("queue1", true, false,false, arguments);
rabbitmqctl set_policy q.ttl ".*" '{"message-ttl":20000, "expires": 10}' --apply-to queues
死信队列
消息称为死信的三种情况
- 队列消息长度达到限制
- 消费者拒接消费信息, basicNack/basicReject,并且不把消息放回原目标队列, requeue=false
- 原队列存在消息过期设置, 消息到达超时时间未被消费 队列绑定死信交换机: 给队列设置参数: x-dead-letter-exchange和x-dead-letter-routing-key
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 30000);
arguments.put("x-expires", 10000);
arguments.put("x-dead-letter-exchange", "exchange1");
arguments.put("x-dead-letter-routing-key", "key1");
延迟队列
使用rabbitmq_delayed_message_exchange, 和ttl不一样的是 ttl在死信队列, 插件在延时交换机里 x-delayed-message-exchange
监控
主要有三种: Management UI, rabbitmqctl和Rest API, prometheus+grafana
rabbitmqctl 常用命令
# 启动服务
rabbitmq-server
#停止
rabbitmqctl stop
# vhost 增删查
rabbitmqctl add_vhost
rabbitmqctl delete_vhost
rabbitmqctl list_vhosts
#查询交换机
rabbitmqctl list_exchanges
# 查询队列
rabbitmqctl list_queues
# 查询消费者信息
rabbitmqctl list_consumers
#user增删查
rabbitmqctl add_user
rabbitmqctl delete_user
rabbitmqctl list_users
prometheus参考www.rabbitmq.com/prometheus.… http://ip:15672/api/index.html http://ip:15692/metrics
用例
可以参考gitee.com/ruichunjie/… 不过用例场景不全,后续补充