架构决策之消息中间件MQ系列二-ActiveMQ

一、前言

     ActiveMQ是"老牌"的MQ,在开源MQ匮乏的年代,是很多公司的必选利器。ActiveMQ是基于JMS1.1标准实现,它设计的目标旨在提供标准的、面向消息的、多语言的应用集成消息通信中间件,实现高效、可扩展、稳定、安全的企业级消息通信。也许是太"古老"了,5.x系列后迭代了下一代产品-Artemis,希望能换发青春。

二、架构

ActiveMq是JSM标准实现,其架构也与其保持一致。生产者创建和发送消息,MQ进行消息持久化和保存,消费者连接MQ,获取消息并处理。

客户端,负责消息的发送和接受,包括生产者和消费者。

生产者,Producer,创建和发送消息的客户端应用。

中间件Broker,ActiveMQ的实例,负责消息的接受,持久化和转发。

消费者,Consumer,连接MQ,获取消息并处理。

消息模式,包括点对点,发布/订阅两种。点对点模式消息,只能被一个消费者接受并处理;发布/订阅模式,消息被所有订阅的客户端接收和处理。

三、生产消息

1、消息组成

消息包括消息头,属性,载荷三部分。

消息头可设置消息的元数据,主要有:

JMSDestination,对应的Destination。

JMSDeliveryMode,设置消息的持久化模式,包括持久化和非持久化两种模式。

JMSExpiration,设置消息的超时时间。

JMSMessageID,消息ID,唯一标识,以"ID:"开头,用于消息传输和存储。

JMSPriority,优先级,0表示最低,9表示最高。优先级越高,会优先被处理。

JMSTimestamp,消息发送的时间戳。

JMSCorrelationID,用来关联当前消息和之前消息。

JMSReplyTo,通常用于request/reply,后面介绍。

JMSType,用于语义上标识消息类型。

JMSRedelivered,消息已发送,并未确认。

属性是附加给客户端消息的可选字段,我们可以自定义的。

负载是消息的主体,包括TextMessage、StreamMessage、ObjectMessage、MapMessage、BytesMessage、BlobMessage几种类型。

2、消息发送

我们看下在生产端消息发送的流程。

(1)创建一个连接工厂并与broker建立连接,ActiveMQ支持以下几种连接协议。

协议描述
TCP默认的通信协议
NIO基于TCP协议,进行了扩展和优化,具有更好的扩展性
UDP性能比TCP高,但是不具有可靠性
SSL安全链接
HTTP基于HTTP
VM当客户端和broker位于同一个Java虚拟机时,通过该方式直接通信

(2)创建session,需要指定两个参数,第一个参数是是否支持事务,第二个参数是设置签收模式。事务模式,我们在可靠性章节介绍,签收模式对于生产者区别不是很大,主要是针对消费者的,后面章节我们重点介绍。

(3) 创建生产者。

(4)创建目的地,即创建队列(点对点模式)或者主题(发布/订阅模式)。

(5)生产者发布消息到目的地。

3、发送模式

ActiveMQ包含三种发送模式,分别是同步模式,异步模式,单向模式。

同步模式,指消息发送方发出数据后,会在收到接收方发回响应之后才发下一个数据包的通讯方式。这种模式的可靠性高,但是会影响吞吐量,是ActiveMQ的默认模式。

异步模式,指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式,结果会通过回调接口异步返回。这种模式可靠性比同步模式较差,但是吞吐量会大大提升。

       在异步模式下,可以设置ProducerWindowSize,控制待确认的消息的大小,即producer每发送一个消息,都会统计一下发送的字节数,当字节数达到ProducerWindowSize值时,需要等待broker的确认,确认当前剩余的空间是否做够接收新消息(大小),然后才能继续发送。

单向模式,只管发送,不关心返回结果,当然也就不关注是否发送成功了。这种模式的延迟小,吞吐量大,但是可靠性差。一般用于用于允许丢失部分数据的场景,比如日志,用户行为等。

四、消息存储

      消息转发到broker后,根据消息类型(前面所介绍的JMSDeliveryMode),分为持久化,和非持久化消息两种。

持久化消息,无论消费者是否在线,确保消息会被消费,当确认被消费后,才会被删除。

非持久化消息,消息及时转发给消费者,并不保存,所以可靠性并不是必须条件。

1、消息模式

      前面介绍消息发送的模式包含点对点模式(消息仅能被一个消息者消费),发布/订阅模式(消息广播给所有订阅的消费者进行消费)。持久化消息对于这两种模式的消息分别是以队列和主题的方式存储。

(1)队列

       以先进先出(FIFO)的队栈模型存储,接受到消息后,不断的加载到Queue中。一个消息仅能分发到一个消费者,当消费者接受处理并ACK消息后,该消息将被删除。

(2)主题

     当接受到消息,保存到对应主题中,发布消息到所有的持久化订阅者。由于每个订阅者消息的进度是不一样的,所以订阅者会维护保存下次消息的指针。当所有的消费者都消费完该条消息后,即可被删除。

2、存储方式

队列和主题是消息的逻辑存储模式,具体的物理存储模式主要有以下4种方式:

(1)AMQ

      AMQ是早期版本的默认消息存储方式,它被设计成一种基于文件存储,高性能的消息存储系统。下面是其内部结构图

  • Data logs:消息日志包含了消息日志。
  • Cache:用于消息的快速检索。
  • Reference store indexes:用于引用 datalogs 中的消息,通过 message ID 关联。

      采用日志文件保存消息,并且是以追加的方式,所以写入的速度非常高(顺序写入磁盘的速度要大于随机写入内存的速度),当文件的大小达到阀值(设置maxFileLength),会重新建立一个文件,当所有的消息消费完成,会删除或者归档这个文件。为了提升读取性能,创建消息主键索引,并且提供缓存机制。

   对于队列以及持久化订阅模式,会每一个 Destination 创建一个索引,如果使用了大量的 Queue,索引文件的大小会占用很多磁盘空间,而且由于索引巨大,一旦 Broker 崩溃,重建索引的速度会非常慢。

    保存的文件结构目录:

  • Lock:保证同一时间只有一个 borker 访问文件目录(后面主从架构会介绍)。
  • temp-storag:用于存储非持久化消息(当不在被存储在内存中),如等待慢消费者处理消息。
  • Kr-store:用于存储引用消息日志数据。
  • journal directory:包含了消息文件、消息日志和消息控制信息。
  • Archive:归档的数据日志。

(2)KahaDB

     在5.4版本后,KahaDB是ActiveMQ默认存储方式,其结构与AMQ非常类似。上面讲到在AMQ中,会为每一个Destination(队列或者主题)创建索引文件,这样限制了Destination量不能太多。在KahaDB中,优化了索引的存储,所有的索引保存在一个文件中。

      可以看到,其索引文件是以B树的结构保存在内存中,并定期刷新到Redo log文件中,当Broker的Destination的大小超过500,我们建议使用KahaDB方式存储。

保存的文件目录:

  • Db log files:用于存储消息(默认大小32M),当 log 日志满了,会创建一个新的,当 log 日志中的消息都被删除,该日志文件会被删除或者归档。
  • Archive directory:当 datalog 不在被 kahadb 需要,会被归档(通过 archiveDataLogs 属性控制)。
  • Db.data:存放 Btree indexs。
  • Db.redo:存放 redo file,用于恢复 Btree indexs。

(3)JDBC

      ActiveMQ支持JDBC将消息存储到关系型数据库中,包括Apache Derby、MySQL、PostgreSQL、Oracle、SQLServer、Sybase、Informix、MaxDB等。

主要包含三张表:

ACTIVEMQ_MSGS,用于存储消息,Queue 和 Topic 都存储在这个表中。

ACTIVEMQ_ACKS,用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存。

ACTIVEMQ_LOCK,消息锁,保证同一时间只能有一个broker访问这些表结构(与AMQ类似,主从架构下会用到)。

JDBC模式由于性能的问题,用的不多,一般用在吞吐量不大,一致性要求高的场景。

        一般情况下,JDBC会与ActiveMq Jounal联合使用,消息数据先写入Jounal的缓存中,然后延迟刷新到数据库中,需要注意的是,在主从模式的数据库中,不建议使用,会导致丢数据。

(4)内存

     将持久化消息保存到内存中,严格意义上,这不是持久化,因为一旦进程关闭,或者服务器宕机,这些内存消息都会被清空。受限于内存大小,保存消息量也不能太多。

      但是这种方式,写入和读取速度都很快,适合于性能要求高,但是可靠性一般的应用场景。

3、消息删除

当消息被消费后(对于发布订阅模式,需要所有消费者都消费),消息会被删除或者归档。

消息被设置过期时间(JMSExpiration),一旦达到过期时间,无论消费是否被消息,也会被删除。

五、消息消费

1、消费者创建

消费者创建的过程与生产者创建过程流程是类似的。

需要注意的是以下两点:

(1)创建Session,需要指定两个参数,一个是是否启动事务,一个签收的模式。其中签收模式,对于消费者来说非常重要,当消费者处理完消息后,需要向broker进行确认,告诉broker我已收到,那么broker就可以发送下一个消息。

签收的模式有三种类型:

Session.AUTO_ACKNOWLEDGE,消费者成功receive消息并返回时,会自动确认。

Session.CLIENT_ACKNOWLEDGE,由消费者显性的调用确认,一旦确认某条消息后,该消息前面所有的消息都会被确认。适用于批量确认的场景。

Session.DUPS_ACKNOWLEDGE, 延迟批量确认消息的提交。

(2)创建Consumer,对于发布/订阅模式,可以创建非持久订阅以及持久订阅。

非持久订阅,当消费者下线或者宕机后,Broker不会为其保存消息,上线后,从最新的消息开始消费。

持久化订阅,当消费者下线或者宕机后,Broker为其保存消息,上线后,重新发送所有缺失的消息。

2、消息接受

      创建消费者后,通过同步(recevie)或者异步设置监听(setMessageListener())方式,从broker获取消息。broker根据消息模式采用不同的策略分发。

(1)点对点模式

如下图所示:

点对点模式,Brokek端采用轮询的模式,将消息发送给消费者,保证消息的负载,并且确保消息仅被一个消费者消费。

(2)发布订阅模式

如下图所示:

发布订阅模式,将消息发送给所有的订阅者。对于持久化订阅,当订阅者下线或者宕机后重新,需要补发所有未消费的消息。

六、可靠性

我们从生产,存储,消费三个阶段探讨下ActiveMQ的可靠性。

1、生产阶段

       在生产消息章节,我们提到了三种发送模式,对于可靠性来说,同步模式>异步模式>单向模式。对于同步和异步模式,会返回发送结果,根据结果的状态,决定是否需要重发,确保在发送阶段消息不丢失。除此之外,我们看下哪些可靠性措施。

(1)事务消息

       ActiveMQ在生产阶段和消费阶段都是支持事务的。采用二阶段提交的模式,一般会先预提交消息,然后本地处理逻辑,依据处理的结果,决定提交或者回滚消息。比如在淘宝购物的场景中,下单的同时,发送短信或邮件消息,当下单成功后,提交确认消息,如果失败,则回滚消息。

       那事务消息如何创建呢,在前面我们介绍session创建时,第一个参数为是否创建事务性的session,如果true,表示该session创建的为事务消息。

      在支持事务的session中,producer发送message时,在message中带有transactionID。broker收到message后判断是否有transactionID,如果有就把message保存在transaction store中,等待commit或者rollback消息。无论最后是否commit,broker都会收到message。当事务commit后,broker将消息发送到正常的queue或者topic中,消费者进行消息;如果rollback,就会从transaction store中删除。

2、存储阶段

当消息发送到broker进行保存和转发,在broker宕机的情况下,如何保证容错性呢?

(1)共享存储主从模式(M-S)

      类似于mysql,ActiveMQ提供共享存储的主从模式。存储的模式不同,又可以分为共享文件系统(Shared File System Master Slave),数据库(JDBC Master Slave)。这两种的原理类似,我们以database为例,架构如下:

     Broker为一主多从(对于从的数量,没有限制),主从共享存储。broker启动时,谁启动的快,谁就能拿到存储层的独占锁,成为Master(比如Broker1),那么其他的就是Slave,客户端连接Master节点进行读写,Slave不连接客户端。

      一旦Master节点宕机或者进程挂死,独占锁就会释放,剩下的Slave节点就会开始强占,谁抢到谁就成为新的Master(如Broker2)。由于共享存储,所以无需数据复制。

当老的Master节点恢复后,因为已经有了新主,所以就变成了从。

这里有个疑问,在主从切换时,client端如何感知?有以下几种实现方法

1、配置failover,客户端支持failover配置,将主从的broker地址都写上,当主节点宕机后,会自动故障转移。

如:failover:(tcp://192.168.1.100:61616,tcp://192.168.1.101:61616,tcp://192.168.1.102:61616)

2、使用负载均衡软件,比如Keepalived,LVS等,客户端的连接地址是唯一的,故障转移可以使用这些软件来实现。

3、应用程序自行实现,维护一个Broker的服务列表,当Broker不可用时,尝试连接其他的Broker服务。这种方式需要有缓存机制,不要每次都去尝试。

数据库不适合高并发,高吞吐的场景,所以ActiveMQ还提供了了基于文件共享的主从模式,支持KahaDB,以及AMQ作为文件存储。

(2)非共享型主从模式

主从都有自己的的消息存储,如下图所示:

    客户端通过连接Master进行读写消息,Master与Slave间建立连接,实现数据的拷贝(包括消息,确认状态,消费组,事务等)。

    Master节点需要先启动,在接受生产者发送的持久化消息,需要同步给Slave节点,Slave节点保存并返回后,Master节点向生产者确认该条消息。所以此模式下,Slave节点个数是有限制的,否则影响消息生产的吞吐量。

     如果Master宕机,Slave无法自动提升为主,需要手动切换。Client端通过配置failover,实现自动探测和切换。

      可以看到,这种模式下,主从切换需要人工干预,缺少仲裁者,消息可靠性是无法得到有效保障,除非容忍failover期间SLA水平降级。

(3)Replicated LevelDB Store方式

        这种集群方式是ActiveMQ5.9版本以后新增的特性,LevelDB 是 Google开发的一套用于持久化数据的高性能KV数据库,ActiveMQ利用该数据库进行数据的存储。该模式使用ZooKeeper从一组Broker中协调选择一个Broker作为Master主,其他broker作为Slave从的模式,所有Slave从节点通过复制Master主节点的消息来实现消息同步。

与上面两种模式一样,只有Master 接受客户端连接,提供读写,Slave不接受客户端连接,Master的所有存储操作都将被复制到slaves。

Zookeeper作为协调者,当Master节点宕机,Zookeeper选取符合条件的其中最新状态的Slave作为Master,当老的Master节点恢复后,作为Slave接入。

Master节点在同步数据时,要求半数以上的节点确认,才返回success。所以副本数(主从节点数)需要为奇数,一般推荐3副本。

3、消费阶段

     前面介绍了生产阶段的事务机制,在消费者阶段也是可以配置事务的,其原理与生产阶段是一致的,当commit后,确认消费成功;如处理失败,则rollback回滚消息。

   除了事务模式,消费阶段有三种确认模式,前面介绍了下三种模式的基本使用,本节重点分析下确认机制的设置对可靠性的影响。

  (1)确认机制

    消息的确认机制,就是consumer告诉broker,消息我已经收到了,不要给我重发了;如果没有确认,broker会重复发送,直到确认,达到累计发送阀值(默认为6)时候,就停止发送。

   AUTO_ACKNOWLEDGE,自动确认。consumer同步或者异步接受消息并返回时,自动发送确认。自动确认机制下,可能会导致消息的丢失。比如consumer接受到消息后,同步返回并自动确认,并启用异步线程,处理消息并入库。在异步处理阶段发生某种异常,导致消息没有正确入库。由于已经自动确认,broker认为消息已正确消费,不会重发,那么就会导致该条消息的丢失。

CLIENT_ACKNOWLEDGE,手动确认consumer同步或者异步接受消息,可以根据处理结果,"择机"手动确认,这样就避免了自动确认的导致的问题。需要注意的,手动确认可以批量一次性确认多条消息,比如确认消息A后,那么消息A之前的所有消息都将被确认。虽然手动确认模式,能防止数据丢失的问题,但是也可能导致消息的重复发送。比如消息被确认处理后,还没来得及确认,消费端宕机,那么该条消息就会被重发,此时会产生重复消费,要求消费端做好幂等性保护。

DUPS_OK_ACKNOWLEDGE,批量自动确认。与AUTO_ACKNOWLEDGE机制类似,只不过是批量进行确认,具有"延迟性",但性能会更高。这种模式也会存在重复消费的场景,问题的原因与CLIENT_ACKNOWLEDGE类似。

综上所述,CLIENT_ACKNOWLEDGE的模式可靠性是最高的,但是没有哪种模式能满足"仅此一次"的场景。

(2)消息重发和死信队列(Dead Letter Queue)

以下情况下,消息会重发到客户端

  • 使用事务的时候,调用了session的rollback的方法
  • 使用事务的时候,在会话关闭后commit
  • 使用CLIENT_ACKNOWLEDGE消息确认机制的时候,调用session的recover方法。
  • 客户端连接或确认超时。

当重发次数达到一定的阀值(maximumRedeliveries设置,默认为6),将不再发送,相关消息移除到死信队列。

死信队列其性质与普通队列是一样的,只不过名称为ActiveMQ.DLQ。通过对死信队列的监控,可以及时处理这些消息。

七、高吞吐

     "削峰"作为MQ的重要的应用场景,决定了其高并发,高吞吐的特性。我们也从生产,存储,消费三个阶段分析ActiveMq的架构设计。

1、生产阶段

      高吞吐和可靠性在大部分情况下是一对矛盾体,鱼和熊掌不可兼得。同步和事务模式,可靠性高,但是对性能的影响也比较大,对于高吞吐,且可靠性要求不高的场景,比如日志,用户行为收集等,使用异步或者单向模式。

    Producer与Broker的协议配置为NIO协议,也能提升性能。

2、存储阶段

       消息发送时,可以设置消息的发送模式,包括持久消息和非持久化消息。对于非持久化消息,broker会立即转发给消费者,不需要存储,吞吐性能会更高。

      无论单机模式还是主从模式,消息的读写都集中在一台broker上,由于单机的性能限制,影响吞吐。有两种方式提升性能,一种是纵向扩容,即增加机器的规格,这没啥好说的;一种是横向扩容,即集群模式,我们重点看下。

(1)集群模式

    集群模式是通过网络桥接实现。我们先来看下什么是网络桥接(network bridge)。

      网络桥接可以实现Broker间消息的互转,有双向桥接(设置duplex为true)和单向桥接。比如上图中的Broker1与Broker2为双向桥接,Broker1接受到的生产消息转发给Broker3,同样Broker3接受到的生产消息也会转发给Broker1,Producer发送给各自broker消息,都可以转发到对方的Broker上消息进行消费。Broker1与Broker2之间是单向桥接,Broker1接受到的消息会转发给Broker2,但是Broker2的消息不会转发Broker1,也就是说,Producer发送给Broker1的消息即可以在Broker上消费,也可以在Broker2上消费,反过来则不行。

网络桥接可以设置为静态和动态两种。静态需要指定桥接的Broker地址,动态则默认自动发现所有的Broker。

<networkConnectors>
<!-- 动态 -->
<networkConnector name="default-nc" uri="multicast://default"/>
<!--静态 -->
<!--
<networkConnector name="host1 and host2"
uri="static://(tcp://host1:61616,tcp://host2:61616)"/>
-->
</networkConnectors>

       可以看到,利用网络桥接实现的集群模式,通过Broker之间的互联和消息转发,将之前仅在单台broker完成的读写,可以分布到多台broker上完成,实现性能的提升。

3、消费阶段

在消费阶段,提高吞吐量最直接的方法就是增加消费端,但是也不能无限制增加,需要考虑到成本。我们来看下还有哪些方式。

(1)批量获取和确认

       这点其实很好理解,消息端一次性获取多条消息,一定会比一次获取一条的速度进行处理的效率要高很多。对于100条消息,批量传输消耗1个RTT(三层网络传输延迟),但是单个获取,需要消耗100个RTT,特别是对于类似跨机房,RTT比较大的场景下,这个性能提升还是很可观的。

      批量获取要对应批量确认才有意义,批量确认需要开启optimizeAcknowledge,与prefetchSize 进行配合。为了减少消费端的"等待延迟",达到0.65 * prefetchSize 个消息后进行确认,broker又可以批量发送。当然也可以使用DUPS_OK_ACKNOWLEDGE模式,它会在0.5*prefetchSize 个消息后进行确认。

     是不是prefethSize越大越好呢,当时也不是,如果消息端的处理能力跟不上,会导致消息的大量积压,最终导致整体性能下降。

持久化和非持久化队列,prefetchSize默认值为1000。

持久化Topic,prefetchSize默认值为100。

非持久化Topic,prefetchSize默认值为32766。

八、总结

本文首先从生产,存储,消费三个阶段介绍了ActiveMQ的基本架构。

在生产阶段,消息由消息头,载荷,以及消息体三部分组成;客户端通过连接创建session会话,实现与broker的消息发送,支持6种传输协议。发送的模式包括同步,异步和单向三种模式。

在存储阶段,Broker通过session与客户端连接,并监听其发送的消息;消息的模式包括点对点(queue),订阅主题(Topic)两种模式;传输模式又分为持久化和非持久化两种;对于持久化消息,需要进行持久化保存,存储方式包括AMQ,KahaDB,JDBC,以及内存四种方式。

在消费阶段,与生产者类似,也是通过创建session与broker进行会话,可以同步或者异步消费;消费完成后,需要ACK broker进行确认,其中确认包括三种模式。

接下来,我们又分析了ActiveMq的可靠性设计,包括事务消息,主从模式,以及消息的确认机制,死信队列等。

最后,我们分析了高并发,高吞吐,提升性能的措施,包括集群模式,批量获取和确认消息。

ActiveMQ作为老牌的MQ,其功能非常丰富,性能调优也设计到方方面面,本文仅对核心功能和架构做了分析,希望能抛砖引玉。

附录:

架构决策之消息中间件MQ系列一-开篇

架构决策之消息中间件MQ系列二-ActiveMQ

架构决策之消息中间件MQ系列三-RabbitMQ

架构决策之消息中间件MQ系列四-Kafka

架构决策之消息中间件MQ系列五-RocketMQ

架构决策之消息中间件MQ系列六-Pulsar

架构决策之消息中间件MQ系列七-总结

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值