ActiveMQ性能优化思路-02

ActiveMQ处理规则和优化

在ActiveMQ单个服务节点的优化中,除了对ActiveMQ单个服务节点的网络IO模型进行优化外,生产者发送消息的策略和消费者处理消息的策略也关乎整个消息队列系统是否能够高效工作。

  1. Producer既是消息生产者,作为一个发送消息的客户端它既可以使用同步消息发送模式,也可以使用异步的消息发送模式。另外,消息生产者在ActiveMQ服务节点产生消息堆积的情况下,也不能一味的追求发送效率。还好,这种情况下消息生产者端有完整的保证机制——Slow Producer。另外,JMS提供事务功能,所以生产者是否开启事务发送消息,将会影响消息发送性能。

  2. 在整个消息处理规则中,ActiveMQ服务节点最极端的情况就是产生消息堆积(消息的消费速度持续低于消息生成速度,就会出现消息堆积)。那么ActiveMQ服务节点处理网络IO模型的优化外,最大的优化点就是:如何应对消息堆积。基本原则是:NON_PERSISTENT Message在内存堆积后,转储到Temp Store区域(当然也可以设置为不转储);PERSISTENT Meaage无论怎样都会先使用持久化方案存储到永久存储区域,再视情况决定是否进行发送;在这些区域也产生堆积后,通知消息生产者使用Slow Producer机制

  3. ActiveMQ服务节点在成功完成PERSISTENT Meaage的持久存储操作后,就会向消息生产者发送一个确认信息,表示该条消息已处理完成。如果ActiveMQ服务节点接受的是NON_PERSISTENT Message,那么生产者默认情况下不会等待服务节点的回执。我们后续会介绍PERSISTENT Meaage的发送也可以设置为不等待回执,这样可以显著提高生产端的消息发送效率。

  4. ActiveMQ服务节点会以一种设置好的策略将消息发送给消费者,这个策略的原则是ActiveMQ服务节点主动推送消息给某一个消费者(不是消费者主动拉取),这样的方式很好便于ActiveMQ服务节点占据整个策略的领导地位。在这个策略中,最重要的属性是prefetchSize:单次获得的消息数量。除此以外,消费者端也是可以调整IO网络模型。

  5. 消费者在获得到消息后,就可以根据这条消息进行业务处理了。最后,消费者需要按照处理结果向ActiveMQ服务节点告知这条(或这些)消息是否处理成功——ACK应答。ActiveMQ中有多种ACK应答方式,它们对性能的影响也不相同。通过这些描述可以看出,消费者的处理性能更能直接影响整个ActiveMQ系统的性能。因为消费者不仅要接受消息、处理消息还要返回消息应答。所以如果您的业务有一定的复杂性,造成每一条消息的处理都会消耗相当的处理时间,那么最直接的性能改善方式就是采用多个消费者节点

6-1、生产者策略:Send

消息生产者发送的消息主要分为两种类型:发送PERSISTENT Meaage和发送NON_PERSISTENT Message。

6-1-1、发送NON_PERSISTENT Message

默认情况下,ActiveMQ服务端认为生产者端发送的是PERSISTENT Message。所以如果要发送NON_PERSISTENT Message,那么生产者端就要明确指定。以下语句明确指定消息发送者将要发送NON_PERSISTENT Message:

......
// 设置发送者发送的是NON_PERSISTENT 
MessageMessageProducer sender = session.createProducer(sendQueue);
sender.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
......

发送NON_PERSISTENT Message时,消息发送方默认使用异步方式:即是说消息发送后发送方不会等待NON_PERSISTENT Message在服务端的任何回执。那么问题来了:如果这时服务端已经出现了消息堆积,并且堆积程度已经达到“无法再接收新消息”的极限情况了,那么消息发送方如果知晓并采取相应的策略呢?

实际上所谓的异步发送也并非绝对的异步,消息发送者会在发送一定大小的消息后等待服务端进行回执(这个配置只是针对使用异步方式进行发送消息的情况):

......
// 以下语句设置消息发送者在累计发送102400byte大小的消息后(可能是一条消息也可能是多条消息)
// 等待服务端进行回执,以便确定之前发送的消息是否被正确处理
// 确定服务器端是否产生了过量的消息堆积,需要减慢消息生产端的生产速度
connectionFactory.setProducerWindowSize(102400);
......

如果您使用的是异步发送方式,那么必须通过这个以上代码指明回执点。例如发送NON_PERSISTENT Message时,这时消息发送者默认使用异步方式。那么如果您想在发送NON_PERSISTENT Message时,每次都等待消息回执,又该如何设置呢?您可以使用connectionFactory提供的“alwaysSyncSend”设置来指定每次都等待服务端的回执:

......
// 设置成:无论怎样每次都等待服务器端的回执
// 但关键是您确定自己的业务需求真的需要这样使用吗?
connectionFactory.setAlwaysSyncSend(true);
......

6-1-2、发送PERSISTENT Message

如果您不特意指定消息的发送类型,那么消息生产者默认发送PERSISTENT Meaage。这样的消息发送到ActiveMQ服务端后将被进行持久化存储(持久化存储方案将在后文进行详细介绍),并且消息发送者默认等待ActiveMQ服务端对这条消息处理情况的回执。

以上这个过程非常耗时,ActiveMQ服务端不但要接受消息,在内存中完成存储,并且按照ActiveMQ服务端设置的持久化存储方案对消息进行存储(主要的处理时间耗费在这里)。为了提高ActiveMQ在接受PERSISTENT Meaage时的性能,ActiveMQ允许开发人员遵从JMS API中的设置方式,为消息发送端在发送PERSISTENT Meaage时提供异步方式:

......
// 使用异步传输
// 上文已经说过,如果发送的是NON_PERSISTENT Message
// 那么默认就是异步方式
connectionFactory.setUseAsyncSend(true);.....

一旦您进行了这样的设置,就需要设置回执窗口:

......
// 同样设置消息发送者在累计发送102400byte大小的消息后
// 等待服务端进行回执,以便确定之前发送的消息是否被正确处理
// 确定服务器端是否产生了过量的消息堆积,需要减慢消息生产端的生产速度
connectionFactory.setProducerWindowSize(102400);
......

6-2、生产者策略:事务

JMS规范中支持带事务的消息,也就是说您可以启动一个事务(并由消息发送者的连接会话设置一个事务号Transaction ID),然后在事务中发送多条消息。这个事务提交前这些消息都不会进入队列(无论是Queue还是Topic)。

不进入队列,并不代表JMS不会在事务提交前将消息发送给ActiveMQ服务端。 实际上这些消息都会发送给服务端,服务端发现这是一条带有Transaction ID的消息,就会将先把这条消息放置在“transaction store”区域中(并且带有redo日志,这样保证在收到rollback指令后能进行取消操作),等待这个Transaction ID被rollback或者commit。

一旦这个Transaction ID被commit,ActiveMQ才会依据自身设置的PERSISTENT Meaage处理规则或者NON_PERSISTENT Meaage处理规则,将Transaction ID对应的message进行入队操作(无论是Queue还是Topic)。以下代码示例了如何在生产者端使用事务发送消息:

......
//进行连接
connection = connectionFactory.createQueueConnection();
connection.start();

//建立会话(设置一个带有事务特性的会话)
session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
//建立queue(当然如果有了就不会重复建立)
Queue sendQueue = session.createQueue("/test");
//建立消息发送者对象
MessageProducer sender = session.createProducer(sendQueue);

//发送(JMS是支持事务的)
for(int index = 0 ; index < 10 ; index++) {
    TextMessage outMessage = session.createTextMessage();
    outMessage.setText("这是发送的消息内容-------------------" + index);
    // 无论是NON_PERSISTENT message还是PERSISTENT message
    // 都要在commit后才能真正的入队
    if(index % 2 == 0) {
        sender.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
    } else {
        sender.setDeliveryMode(DeliveryMode.PERSISTENT);
    }

    // 没有commit的消息,也是要先发送给服务端的
    sender.send(outMessage);
}

session.commit();
......

以上代码中,在“connection.createSession”这个方法中一共有两个参数(这句代码在上文中已经出现过多次)。第一个布尔型参数很好理解,就是标示这个连接会话是否启动事务;第二个整型参数标示了消息消费者的“应答模型”,我们会在下文中进行详细介绍。

6-3、生产者策略:ProducerFlowControl

生产流控制,是ActiveMQ消息生产者端最为重要的性能策略,它主要设定了在ActiveMQ服务节点在产生消息堆积,并超过限制大小的情况下,如何进行消息生产者端的限流。

具体来说,如果以上情况在ActiveMQ出现,那么当生产者端再次接受ActiveMQ的消息回执时,ActiveMQ就会让消息生产者进入等待状态或者在发送者端直接抛出JMSException。当然您也可以配置ActiveMQ不进行ProducerFlowControl,如果您对自己ActiveMQ服务端的底层性能和消费者端的性能足够自信的话。

在ActiveMQ的主配置文件activemq.xml中,关于ProducerFlowControl策略的控制标签是“destinationPolicy”和它的子标签。请看如下配置示例:

......

 
   
     
   

 


......

以上示例配合所有的Topic模式的队列不进行producerFlowControl策略控制。当然还可以为队列配置启用producerFlowControl策略:

......



......

以上配置项表示为ActiveMQ中的所有Queue模式的队列启用producerFlowControl策略,并且限制每个Queue信息的最大内存存储限制(memoryLimit)为200MB;这是指的最多使用200MB的内存区域,而不是说Queue中消息的总大小为200MB。例如,在ActiveMQ 5.X+ 版本中NON_PERSISTENT Message会被转出到 temp store区域,所以有可能您观察到的现象是,无论怎样堆积NON_PERSISTENT Message消息,Queue的使用内存始终无法达到200MB。

......

     
         
     


......

以上配置表示只使用内存存储Queue中的所有消息,特别是NON_PERSISTENT Message只存储在内存中,不使用temp store区域进行转储。在官方文档中,有关于policyEntry标签的所有配置选项都有完整说明:http://activemq.apache.org/per-destination-policies.html。

6-4、消费者策略:Dispatch Async

讨论完了消息生产者的关键性能点,我们再将目光转向消息消费者(接收者端);就像本小节开始时描述的那样,比起消息生产者来说消息消费者的性能更能影响ActiveMQ系统的整体性能,因为要成功完成一条消息的处理,它的工作要远远多于消息生产者。

首先,在默认情况下ActiveMQ服务端采用异步方式向客户端推送消息。也就是说ActiveMQ服务端在向某个消费者会话推送消息后,不会等待消费者的响应信息,直到消费者处理完消息后,主动向服务端返回处理结果。如果您对自己的消费者性能足够满意,也可以将这个过程设置为“同步”:

......
// 设置为同步
connectionFactory.setDispatchAsync(false);
......

6-5、消费者策略:Prefetch

消费者关键策略中,需要重点讨论的是消费者“预取数量”——prefetchSize。可以想象,如果消费者端的工作策略是按照某个周期(例如1秒),主动到服务器端一条一条请求新的消息,那么消费者的工作效率一定是极低的;所以ActiveMQ系统中,默认的策略是ActiveMQ服务端一旦有消息,就主动按照设置的规则推送给当前活动的消费者。其中每次推送都有一定的数量限制,这个限制值就是prefetchSize。

针对Queue工作模型的队列和Topic工作模型的队列,ActiveMQ有不同的默认“预取数量”;针对NON_PERSISTENT Message和PERSISTENT Message,ActiveMQ也有不同的默认“预取数量”:

  • PERSISTENT Message—Queue:prefetchSize=1000
  • NON_PERSISTENT Message—Queue:prefetchSize=1000
  • PERSISTENT Message—Topic:prefetchSize=100
  • NON_PERSISTENT Message—Topic:prefetchSize=32766

ActiveMQ中设置的各种默认预取数量一般情况下不需要进行改变。如果您使用默认的异步方式从服务器端推送消息到消费者端,且您对消费者端的性能有足够的信心,可以加大预取数量的限制。但是非必要情况下,请不要设置prefetchSize=1,因为这样就是一条一条的取数据;也不要设置为prefetchSize=0,因为这将导致关闭服务器端的推送机制,改为客户端主动请求

  • 可以通过ActiveMQPrefetchPolicy策略对象更改预取数量

......
// 预取策略对象
ActiveMQPrefetchPolicy prefetchPolicy = connectionFactory.getPrefetchPolicy();
// 设置Queue的预取数量为50
prefetchPolicy.setQueuePrefetch(50);
connectionFactory.setPrefetchPolicy(prefetchPolicy);
//进行连接
connection = connectionFactory.createQueueConnection();
connection.start();
......

  • 也可以通过Properties属性更改(当然还可以加入其他属性)预取数量:

......
Properties props = new Properties();
props.setProperty("prefetchPolicy.queuePrefetch", "1000");
props.setProperty("prefetchPolicy.topicPrefetch", "1000");
//设置属性
connectionFactory.setProperties(props);
//进行连接
connection = connectionFactory.createQueueConnection();
connection.start();
......

6-5、消费者策略:事务和死信

6-5-1、消费者端事务

JMS规范除了为消息生产者端提供事务支持以外,还为消费服务端准备了事务的支持。您可以通过在消费者端操作事务的commit和rollback方法,向服务器告知一组消息是否处理完成。采用事务的意义在于,一组消息要么被全部处理并确认成功,要么全部被回滚并重新处理

......
//建立会话(采用commit方式确认一批消息处理完毕)
session = connection.createSession(true, Session.SESSION_TRANSACTED);
//建立Queue(当然如果有了就不会重复建立)
sendQueue = session.createQueue("/test");
//建立消息发送者对象
MessageConsumer consumer = session.createConsumer(sendQueue);
consumer.setMessageListener(new MyMessageListener(session));

......

class MyMessageListener implements MessageListener {
    private int number = 0;

    /**
     * 会话
     */
    private Session session;

    public MyMessageListener(Session session) {
        this.session = session;
    }

    @Override
    public void onMessage(Message message) {
        // 打印这条消息
        System.out.println("Message = " + message);
        // 如果条件成立,就向服务器确认这批消息处理成功
        // 服务器将从队列中删除这些消息
        if(number++ % 3 == 0) {
            try {
                this.session.commit();
            } catch (JMSException e) {
                e.printStackTrace(System.out);
            }
        }
    }
}

以上代码演示的是消费者通过事务commit的方式,向服务器确认一批消息正常处理完成的方式。请注意代码示例中的“session = connection.createSession(true, Session.SESSION_TRANSACTED);”语句。第一个参数表示连接会话启用事务支持;第二个参数表示使用commit或者rollback的方式进行向服务器应答。

这是调用commit的情况,那么如果调用rollback方法又会发生什么情况呢?调用rollback方法时,在rollback之前已处理过的消息(注意,并不是所有预取的消息)将重新发送一次到消费者端(发送给同一个连接会话)。并且消息中redeliveryCounter(重发计数器)属性将会加1。请看如下所示的代码片段和运行结果

@Override
public void onMessage(Message message) {
    // 打印这条消息
    System.out.println("Message = " + message);
    // rollback这条消息
    this.session.rollback();
}

以上代码片段中,我们不停的回滚正在处理的这条消息,通过打印出来的信息可以看到,这条消息被不停的重发:

Message = ActiveMQTextMessage {...... redeliveryCounter = 0, text = 这是发送的消息内容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 1, text = 这是发送的消息内容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 2, text = 这是发送的消息内容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 3, text = 这是发送的消息内容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 4, text = 这是发送的消息内容-------------------20}
 

可以看到同一条记录被重复的处理,并且其中的redeliveryCounter属性不断累加。

6-5-2、重发和死信队列

但是消息处理失败后,不断的重发消息肯定不是一个最好的处理办法:如果一条消息被不断的处理失败,那么最可能的情况就是这条消息承载的业务内容本身就有问题。那么无论重发多少次,这条消息还是会处理失败。

为了解决这个问题,ActiveMQ中引入了“死信队列”(Dead Letter Queue)的概念。即一条消息再被重发了多次后(默认为重发6次redeliveryCounter==6),将会被ActiveMQ移入“死信队列”。开发人员可以在这个Queue中查看处理出错的消息,进行人工干预。

ActiveMQ性能优化思路-02_第1张图片

默认情况下“死信队列”只接受PERSISTENT Message,如果NON_PERSISTENT Message超过了重发上限,将直接被删除。以下配置信息可以让NON_PERSISTENT Message在超过重发上限后,也移入“死信队列”:

 
     
         
   
 

另外,上文提到的默认重发次数redeliveryCounter的上限也是可以进行设置的,为了保证消息异常情况下尽可能小的影响消费者端的处理效率,实际工作中建议将这个上限值设置为3。原因上文已经说过,如果消息本身的业务内容就存在问题,那么重发多少次也没有用。

RedeliveryPolicy redeliveryPolicy = connectionFactory.getRedeliveryPolicy();
// 设置最大重发次数
redeliveryPolicy.setMaximumRedeliveries(3);

实际上ActiveMQ的重发机制还有包括以上提到的rollback方式在内的多种方式:

  • 在支持事务的消费者连接会话中调用rollback方法。

  • 在支持事务的消费者连接会话中,使用commit方法明确告知服务器端消息已处理成功前,会话连接就终止了(最可能是异常终止)

  • 在需要使用ACK模式的会话中,使用消息的acknowledge方式明确告知服务器端消息已处理成功前,会话连接就终止了(最可能是异常终止)

但是以上几种重发机制有一些小小的差异,主要体现在redeliveryCounter属性的作用区域。简而言之,第一种方法redeliveryCounter属性的作用区域是本次连接会话,而后两种redeliveryCounter属性的作用区域是在整个ActiveMQ系统范围。

6-6、消费者策略:ACK

消费者端,除了可以使用事务方式来告知ActiveMQ服务端一批消息已经成功处理外,还可以通过JMS规范中定义的acknowledge模式来实现同样功能。事实上acknowledge模式更为常用

6-6-1、基本使用

如果选择使用acknowledge模式,那么你至少有4种方式使用它,且这四种方式的性能区别很大:

  • AUTO_ACKNOWLEDGE方式:这种方式下,当消费者端通过receive方法或者MessageListener监听方式从服务端得到消息后(无论是pul方式还是push方式),消费者连接会话会自动认为消费者端对消息的处理是成功的。但请注意,这种方式下消费者端不一定是向服务端一条一条ACK消息

  • CLIENT_ACKNOWLEDGE方式:这种方式下,当消费者端通过receive方法或者MessageListener监听方式从服务端得到消息后(无论是pul方式还是push方式),必须显示调用消息中的acknowledge方法。如果不这样做,ActiveMQ服务器端将不会认为这条消息处理成功:

public void onMessage(Message message) {
    //====================
    //这里进行您的业务处理
    //====================
    try {
        // 显示调用ack方法
        message.acknowledge();
    } catch (JMSException e) {
        e.printStackTrace();
    }
}

  • DUPS_OK_ACKNOWLEDGE方式:批量确认方式。消费者端会按照一定的策略向服务器端间隔发送一个ack标示,表示某一批消息已经处理完成。DUPS_OK_ACKNOWLEDGE方式 和 AUTO_ACKNOWLEDGE方式在某些情况下是一致的,这个在后文会讲到。

  • INDIVIDUAL_ACKNOWLEDGE方式:单条确认方式。这种方式是ActiveMQ单独提供的一种方式,其常量定义的位置都不在javax.jms.Session规范接口中,而是在org.apache.activemq.ActiveMQSession这个类中。这种方式消费者端将会逐条向ActiveMQ服务端发送ACK信息。所以这种ACK方式的性能很差,除非您有特别的业务要求,否则不建议使用

6-6-2、工作方式和性能

笔者建议首先考虑使用AUTO_ACKNOWLEDGE方式确认消息,如果您这样做,那么一定请使用optimizeACK优化选项,并且重新设置prefetchSize数量为一个较小值(因为1000条的默认值在这样的情况下就显得比较大了):

......
//ack优化选项(实际上默认情况下是开启的)
connectionFactory.setOptimizeAcknowledge(true);
//ack信息最大发送周期(毫秒)
connectionFactory.setOptimizeAcknowledgeTimeOut(5000);
connection = connectionFactory.createQueueConnection();
connection.start();
......

AUTO_ACKNOWLEDGE方式的根本意义是“延迟确认”,消费者端在处理消息后暂时不会发送ACK标示,而是把它缓存在连接会话的一个pending 区域,等到这些消息的条数达到一定的值(或者等待时间超过设置的值),再通过一个ACK指令告知服务端这一批消息已经处理完成;而optimizeACK选项(指明AUTO_ACKNOWLEDGE采用“延迟确认”方式)只有当消费者端使用AUTO_ACKNOWLEDGE方式时才会起效

“延迟确认”的数量阀值:prefetch * 0.65 
“延迟确认”的时间阀值:> optimizeAcknowledgeTimeOut

DUPS_OK_ACKNOWLEDGE方式也是一种“延迟确认”策略,如果目标队列是Queue模式,那么它的工作策略与AUTO_ACKNOWLEDGE方式是一样的。也就是说,如果这时prefetchSize =1 或者没有开启optimizeACK,也会逐条消息发送ACK标示;如果目标队列是Topic模式,那么无论optimizeACK是否开启,都会在消费的消息个数>=prefetch * 0.5时,批量确认这些消息。

6-7、消费者和生产者性能总结

本小节我们介绍了基于ActiveMQ构建的消息队列系统中,生产者和消费者需要关注的重要性能点。但是整个ActiveMQ中的性能还需要各位读者在实际工作中,一点一点的去挖掘。这里我们根据已经介绍过的性能关注点进行总结:

  • 发送NON_PERSISTENT Message和发送PERSISTENT Message是有性能差异的。引起这种差异的原因是前者不需要进行持久化存储;但是这样的性能差异在某些情况下会缩小,例如发送NON_PERSISTENT Message时,由于消费者性能不够导致消息堆积,这时NON_PERSISTENT Message会被转储到物理磁盘上的“temp store”区域。

  • 发送带有事务的消息和发送不带有事务的消息,在服务器端的处理性能也是有显著区别的。引起这种差异的原因是带有事务的消息会首先记录在服务器端的“transaction store”区域,并且服务器端会带有redo日志,这样保证发送者端在发送commit指令或者rollback指令时,服务器会完整相应的处理。

  • ActiveMQ中,为消息生产者所设定的ProducerFlowControl策略非常重要,它确定消息在ActiveMQ服务端产生大量堆积的情况下,ActiveMQ将减缓接收消息,保证了ActiveMQ能够稳定的工作。您可以通过配置文件设置ProducerFlowControl策略的生效阀值,甚至可以关闭ProducerFlowControl策略(当然不建议这样做)。

  • 消息生产者端和消息消费者端都可以通过“异步”方式和服务器进行通讯(但是意义不一样)。在生产者端发送异步消息,一定要和ProducerWindowSize(回执窗口期)的设置共同使用;在消费者异步接受消息时,要记住有Prefetch这个关键的预取数值,并且PrefetchSize在非必要情况下不要设置为1。很显然适合的PrefetchSize将改善服务端和消费者端的性能。

  • JMS规范中,消息消费者端也是支持事务的。所谓消费者端的事务是指:一组消息要么全部被commit(这时消费者会向服务端发送ACK表示),要么全部被rollback(这时同一个消费者端会向自己重发这些消息,并且这些消息的redeliveryCounter属性+1);进行消息的重发是非常消耗消费者端性能的一件事情,这是因为在这个连接会话中,被Prefetch但是还没有被处理的消息将一直等待重发的消息最终被确认。

  • 为了避免带有错误业务信息的消息被无止境的重发,从而影响整个消息系统的性能。在ActiveMQ中为超过MaximumRedeliveries阀值(默认值为6,但是很明显默认值太高了,建议设置为3)的消息准备了“死信队列”。

  • 只有服务器收到了一条或者一组消息的ACK标示,才会认为这条或者这组消息被成功过的处理了。在消费者端有4种ACK工作模式,建议优先选择AUTO_ACKNOWLEDGE。如果您这样做了,那么请一定重新改小预取数量、设置OptimizeAcknowledge为true、重设OptimizeAcknowledgeTimeOut时间。这样才能保证AUTO_ACKNOWLEDGE方式工作在“延迟确认”模式下,以便优化ACK性能。

你可能感兴趣的:(JAVA)