RabbitMQ

RabbitMQ作用及其出现背景

作用: 削峰,解耦,异步调用

RabbitMQ特性和好处:

  • 开源
  • 轻量级
  • 面向大多数现代语言的客户端开发库
  • 灵活控制消息通信的平衡性
  • 高延迟性环境插件—-因为不是所有的网络拓扑和架构都是一样的,RabbitMQ既支持在低延迟环境下的消息通信机制,也提供了针对互联网的高延迟环境下的插件
  • 第三方插件系统—–作为应用集成的一个关键要素,RabbitMQ提供了灵活的插件系统。当需要

RabbitMQ架构

producer, consumer, broker

  • AMQP协议

    • exchange
    • queue
    • route-key/bind-key

    一个AMQP连接可以有多个信道,允许客户端和服务器之间进行多次会话,从技术上讲,这被称为多路复用。

    在创建客户端庄用程序时,不要使用过多的信道使事情变得复杂。在编组帧的线路上,信道不过是分配给服务器和客户端之间所传递消息的一个整数值;而在 RabbitMQ 服务器和客户端中,它们代表更多的含义 因为会为每个信道设置内存结构和对象,连接中的信道越多, RabbitMQ 用于管理该连接的消息流所需的内存也就越多 如果你能合理地使用它们,你将会有一个更健康的 RabbitMQ服务器和一个更简洁的客户端应用程序

    AMQP帧由五个不同组件组成: 帧类型,信道编号,帧大小,帧有效载荷,结束字节标记。

    AMQP帧: 协议头帧,方法帧,内容头帧,消息体帧以及心跳帧,使用方法帧,内容头帧,消息体帧向broker发布消息

    • 协议头帧用于连接RabbitMQ,仅使用一次
    • 方法帧携带发送给RabbitMQ或从RabbitMQ接收的RPC请求或响应
    • 内容头帧包含一条消息的大小和属性
    • 消息体帧包含消息的内容

    mandatory标志(属于方法帧)告知RabbitMQ必须完成消息路由,否则它应该发送一个Basic.Ruturn帧用于指明消息无法路由。

消息属性

  • content-type

  • expiration

  • reply-to

  • content-encoding

  • delivery-mode

    1表示非持久化消息,2表示持久化消息

  • message-id

  • priority

  • correlation-id

消息发布

金发姑娘原则:

  • 消费者发布时保证消息进入队列的重要性有多高?
  • 如果消息无法路由,是否应将消息返回给发布者?
  • 如果消息无法路由,是否应该将其发送到其他地方稍后进行重新路由?
  • 如果RabbitMQ服务器崩溃,可以接收信息丢失吗?
  • RabbitMQ在处理新消息时是否应该确认它已经为发布者执行了所有请求的路由和持久化任务?
  • 消息发布者是否可以批量投递消息,然后从RabbitMQ收到一个确认用于表明所有请求的路由和持久化任务已经批量应用到所有的消息中?
  • 如果你要批量发布消息,而这些消息需要确认路由和持久化,那么对每一条消息是否需要对目标队列实现真正意义上的原子提交?
  • 在可靠投递方面是否有可接受的平衡性,你的发布者可以使用它来实现更高的性能和消息吞吐量吗?
  • 消息发布还有哪些方面会影响消息吞吐量和性能?

RabbitMQ与原子事务: 原子性确保事务中所有操作的完成都将作为事务提交的 部分。在 AMQP 中,这意味着直到事务中的所有操作都完成为止,你的客户端将不会收到 TX.CornmitOk响应帧。不幸的是,对于那些寻求真正原子性的人来说, RabbitMQ 只在每个发出的命令作用于单个队列时才执行原子事务。如果不止一个队列受到事务中任何命令的影响,则提交就不具备原子性。尽管当事务中的所有命令仅影响同一个队列时 RabbitMQ 会执行原子事务,但发布者通常不能很好地控制消息是否被投递到多个队列。使用 RabbitMQ 高级路由方法,很容易想象一个应用程序在发布消息到单个队列时启动原子提交,然后有人可能使用同一个路由键绑定一个新的队列。这样任何使用该路由键的发布事务将不再具备原子性。还值得指出的是,当将delivery-mode值设置为2从而对消息进行持久化时,真正的原子事务可能会导致发布者的性能问题。如果在发送TX.CommitOK之前,RabbitMQ正在等待服务器I/O密集型写入操作的完成,那么客户端可能比命令没有被包装在事务中的场景需要等待更长的时间。

HA队列

Channel.Flow和Backpressure机制(停止接受TCP套接字上的数据)

消费消息

为什么你应该避免拉取消息,而应该倾向于消费消息?Basic.Get会导致每条消息都会产生与RabbitMQ同步通信的开销;由于Basic.Get的临时性,RabbitMQ不能以任何方式优化投递过程

如何平衡消息的投递的可靠性与性能?

增加Linux操作系统接收套接字缓冲区的数量

Qos:预拉取数量

dead-letter exchange。死信交换器与备用交换器不同,过期或被拒绝的消息通过死信交换器进行投递,而备用交换器则路由那些无法由RabbitMQ路由的消息

队列

队列的行为有:自动删除自己,只允许一个消费者进行消费,自动过期的消息,保持有限数量的消息,将旧消息推出堆栈

集群

RabbitMQ提高了队列高可。节点类型和消息持久化,

  • erlang分布式

    erlang的分布式特性在语言层面支持,可以使用语言内置的API函数。、

    erlang的分布式是以erlang的两个基本特性为基础:

    • 复制式进程通信

      Erlang的进程间通信采用的是严格的异步消息传递(发送消息后无须等待网络上的确认),接受方收到数 据时实际上获取了数据的一份独立的副本;此后接收方将无法感知发送方对数据所做的任何操作,反之亦 然。后续的任何通信都必须借助额外的消息才能进行。无论是运行在同一台机器上的进程,还是运行在不 同机器上并通过网络互联的进程,这种模型都非常奏效.

    • 位置透明性

      erlang会确定进程标识符在多机网络上的唯一性,erlang的pid是一个结构化的对象,包含node id, process id, serial三个元素

实战指南

消息是指在应用间传送的数据,消息可以非常简单.

消息队列中间件是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。

一般有两种传递模式:p2p, pub/sub

消息中间件的作用:

  • 解耦
  • 冗余
  • 削峰
  • 异步

vhost

Erlang在运行时使用线程池来异步执行I/O操作。线程池的大小可以通过环境变量来调节

大部分操作系统都限制了同一时间可以打开的文件句柄数。在优化并发连接数的时候,需确保系统有足够的文件句柄数来支撑客户端和Broker的交互。增大TCP缓冲区大小可以提高吞吐量,如果减少TCP缓冲区就可以减少每个连接上的内存使用量(并发更重要可修改此值)

  • 存储机制

    持久层实际包含两个部分.rabbit_queue_index和rabbit_msg_store。rabbit_queue_index负责维护队列中落盘消息,包括消息的存储地点,是否已被交付给消费者、是否已被消费者ack等。每个队列都有与之对应的一个rabbit_queue_index. rabbitmq_msg_store以键值对的形式存储消息,它被所有队列共享,在每个节点有且只有一个. rabbit_msg_store分为msg_store_persistent和msg_store_transient。

    消息可以直接存储在rabbit_queue_index中,也可以保存在rabbit_msg_store中。消息大小的界定queue_index_embed_msgs_below(默认4096B)

    rabbit queue index 中以顺序(文件名从 开始累加〉的段文件来进行存储,后缀为’ . idx ”,每个段文件中包含固定的 SEGMENT ENTRY COUNT 条记录,SEGMENT_ENTRY_COUNT 默认值为 1638 。每个 rabbit_queue_index 从磁盘中读取消息的时候至少要在内存中维护一个段文件,所以设置 queue index embed msgs below 值的时候要格外谨慎,一点点增大也可能会引起内存爆炸式的增长

    经过 rabbit msg store 处理的所有消息都会以追加的方式写入到文件中,当 个文件的大小超过指定的限制( file size limit )后,关闭这个文件再创建一个新的文件以供新的消息写入。文件名(文件后缀是“ .rdq ”)从 开始进行累加,因此文件名最小的文件也是最老的文件。在进行消息的存储时, bb itM 会在 ET (Erlang Term storage )表中记录消息在文件中的位置映射( Index )和文件的相关信息( File Summary )。

    在读取消息的时候,先根据消息的 ID Cmsg id )找到对应存储的文件,如果文件存在并且未被锁住,则直接打开文件,从指定位置读取消息的内容。如果文件不存在或者被锁住了,则发送请求由 rabbit_msg_store 进行处理。

    消息的删除只是从 ETS 表删除指定消息的相关信息 同时更新消息对应的存储文件的相关信息。执行消息删除操作时,井不立即对在文件中的消息进行删除,也就是说消息依然在文件中。仅仅是标记为垃圾数据而已。当一个文件中都是垃圾数据时可以将这个文件删除。当检测到前后两个文件中的有效数据可以合并在一个文件中。并且所有的垃圾数据的大小和所有文件的数据大小比值超过gc阈值进行文件合并

    执行合并的两个文件一定是逻辑上相邻的两个文件。执行合并时首先锁定这两个文件,并先对前面文件中的有效数据进行整理,再将后面的文件的有效数据写入到前面的文件,同时更新消息在ETS表的记录,最后删除后面的文件。

  • 队列的结构

    通常队列由rabbit_amqqueue_process和backing_queue这两部分组成,rabbit_amqqueue_process负责协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息,处理消息的确认等。backing_queue是消息存储的具体形式和引擎,并向rabbitmq_amqqueue_process提供相关的接口以供调用.

    如果消息投递的目的队列是空的,并且有消费者订阅了这个队列,那么消息会直接发送给消费者,不会经过队列这一步.而当消息无法直接投给消费者时,需要暂时将消息存入队列,以便重新投递。消息存入队列后,不是固定不变的,它会随着系统的负载在队列中不断地流动,消息的状态会不断发生变化。

    • alpha: 消息内容和消息索引都存储在内容中
    • beta: 消息内容保存在磁盘中,消息索引保存在内存中
    • gamma: 消息内容保存在磁盘中, 消息索引在磁盘和内存中都有
    • delta: 消息内容和索引都在磁盘中

    对于持久化的消息,消息内容和消息索引都必须先保存在磁盘上,才会处于上述状态中的一种,而gamma状态的消息是只有持久化的消息才会有的状态.

    RabbitMQ 在运行时会根据统计的消息传送速度定期计算一个当前内存中能够保存的最大消息数量 (target_ram_count),如果 alpha 状态的消息数量大于此值时,就会引起消息的状态转换,多余的消息可能会转换到 beta 状态、 gamma 状态或者 delta 状态。区分这状态的主要作用是满足不同的内存和 CPU 需求。 alpha 状态最耗内存,但很少消耗 CPU。 delta状态基本不消耗内存,但是需要消耗更多的 CPU 和磁盘 I/O 操作。 delta 状态需要执行两次I/O 操作才能读取到消息, 一次是读消息索引(从 rabbit queue index 中), 一次是读消息内容(从 rabbit_msg_store 中); beta gamma 状态都只需要一次 I/O 操作就可以读取到消息(从 rabbit msg store 中)。

  • 内存告警和磁盘告警

    当内存使用超过配置的阈值或者磁盘剩余空间低于配置的阈值时,RabbitMQ都会暂时阻塞客户端的连接并停止接收从客户端发来的消息,避免服务崩溃。与此同时,客户端与服务端的心跳检测也会失效。被阻塞的connection状态是blocking,要么是blocked。前者对应于并不是试图发送消息的connection。比如消费者关联的connection,这种状态的connection可以继续运行。如果一个broker节点的内存或者磁盘受限,都会引起整个集群中所有的connection被阻塞.

    理想情况是当发生阻塞时可以在阻止生产者的同时而又不影响消费者的运行。

    默认情况下RabbitMQ所使用的内存阈值设置为40%.在最坏情况下,Erlang的垃圾回收机制会导致两倍的内存消耗,也就是80%的使用占比

    流控: 一个连接触发流控会处于"flow"的状态,也就意味着connection的状态每秒在blocked和unblocked之间来回切换数次,这样可以将消息发送的速率控制在服务器能支撑的范围之内。流控机制不只是作用于connection,同样作用于channel和队列。

  • 镜像队列

    slave会准确地按照master执行命令的顺序进行动作,故slave与master上维护的状态应该是相同的。如果master由于某种原因失效,那么"资历最老"的slave会被提升为新的master。根据slave加入的时间排序,时间最长的slave即为"资历最老"。发送到镜像队列的所有消息被同时发往master和所有的slave上,如果此时master挂掉了,消息还会在slave上,这样slave提升为master的时候消息也不会丢失。除发送消息(Basic.Publish)外所有动作都只会向master发送,然后再由master将命令执行的结果广播给各个slave.

    master和slave是针对队列而言,而队列可以均匀地散落在集群地各个Broker节点中达到负载均衡地目的,因为真正地负载还是对实际物理机器而言地,而不是内存中驻留地队列进程.只要确保队列的master节点均匀散落在集群中的各个Broker节点即可确保很大程度的负载均衡(每个队列的流量会有不同,因此均匀散落各个队列的master也无法确保绝对的负载均衡)。读写分离得不到更好的收益,即读写分离并不能进一步优化负载,却会增加编码实现的复杂度。

    GM模块实现的是一种可靠的组播通信协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到。它的实现大致为:将所有节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点,当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上;当有节点失效时,相邻的节点会接管以保证本次广播的消息会复制到所有的节点。gm_group的信息会记录在mnesia中,不同的镜像队列形成不同的组。操作命令从 master 对应的 GM 发出后,顺着链表传送到所有的节点。由于所有节点组成了一个循环链表, master 对应的 GM 最终会收到自己发送的操作命令,这个时候 master 就知道该操作命令同步到了所有的 slave

  • 网络分区

Resource