消息积压问题优化思路探讨

JAVA前线

共 17298字,需浏览 35分钟

 · 2021-04-25


JAVA前线 


欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习


0 文章概述

在使用消息中间件时消息积压是我们必须面对的问题,无论这种问题是生产消息过快还是消费者消费能力不足导致的。本文我们以RocketMQ为例分析消息积压问题通用处理思路。



1 不处理

消息积压一定要处理吗?我认为在不影响业务情况下,消息积压可以不处理,等待积压消息逐渐被消化即可,因为消息积压本质上是对消费者的保护。我们不妨回顾一下消息中间件三个作用:解耦、异步、削峰。


1.1 解耦

假设用户在一个电商系统购物,支付成功后系统应该怎么把这个消息告诉物流系统?

第一种方式是支付系统直接调用物流系统,但这样会有一个问题:支付系统和物流系统产生了强依赖,当物流系统出现问题,直接影响用户交易流程,导致支付失败。

第二种方式是支付系统把支付成功消息推送给消息中间件,此时交易流程结束。物流系统订阅这个消息进行后续处理,即使物流系统出现问题,也不影响交易系统。


1.2 异步

假设物流系统处理业务需要100毫秒。如果支付系统直接调用物流系统,整个链路响应时长就增加了100毫秒。

如果支付系统把支付成功消息推送给消息中间件,支付系统就可以直接返回了,那么整个链路时长就不需要增加这100毫秒了,这就是异步化带来性能提升。


1.3 削峰

假设双十一商家正在做秒杀活动,瞬时产生了大量支付单据。如果支付系统直接调用物流系统,支付系统压力就会同时传递给物流系统,这是完全没有必要的。

如果支付系统把支付成功消息推送给消息中间件,物流系统可以根据系统能力,匀速拉取数据处理,削减了流量洪峰。

消息堆积在中间件本质上是对物流系统的一种保护,流量压力被匀速释放给物流系统,所以这种情况我们无须对消息积压处理。


2 要处理

如果业务对消费者处理实时性有要求,必须在一定时间内处理完所有消息,因为这种场景消息积压已经影响了业务,这时我们必须有所行动。首先我们看看官网上RocketMQ网络部署图:




Producer是消息生产者,Consumer是消息消费者,Broker是消息中转者,负责消息存储和转发。NameServer作为注册中心维护着生产者、Broker、消费者集群服务状态。对于消息积压问题我们可以从生产者、Broker、消费者三个维度进行思考。


2.1 生产者

生产者可以减少消息发送量从而减少消息积压。消息发送量又可以从两个维度进行思考:第一是减少消息发送数量,对于下游明显不需要的消息可以不发送,或者是对于一些频繁变化的业务消息,可以选择等待业务消息稳定后再发送。

第二是减少消息内容大小,例如消费者只需要5个字段,那么生产者就无需发送全部10个字段,尤其是一些体积很大的上下文字段可以不必发送。


2.2 Broker

对于消费者不关心的消息完全可以在Broker端进行过滤,从而减少传输到消费者的消息量从而提高吞吐量。我们以RocketMQ为例分析其提供的Tag、SQL表达式、Filter Server三种过滤方式。


(1) Tag

生产者为一个消息设置一个Tag,消费者在订阅时设置需要关注的Tag,这样Broker可以在ConsumeQueue进行过滤,只会从CommitLog读取命中标签的消息:

public class TagFilterProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
        producer.start();
        String[] tags = new String[] { "TagA""TagB""TagC" };
        for (int i = 0; i < 10; i++) {
            // 每个消息设置一个Tag
            Message msg = new Message("MyTopic", tags[i % tags.length], "Hello".getBytes(RemotingHelper.DEFAULT_CHARSET));
            SendResult sendResult = producer.send(msg);
            System.out.println("sendResult=", + sendResult);
        }
        producer.shutdown();
    }
}

public class TagFilterConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException, IOException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup");
        // 只订阅消息Tag等于TagA或者TagC
        consumer.subscribe("MyTopic""TagA || TagC");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println("ThreadName=" + Thread.currentThread().getName() + ",messages=" + msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

(2) SQL表达式

Tag只能支持比较简单的过滤逻辑,如果需要进行复杂过滤就需要使用SQL表达式,官网对SQL表达式介绍如下:

支持语法
Numeric comparison, like >, >=, <, <=, BETWEEN, =
Character comparison, like =, <>, IN
IS NULL or IS NOT NULL
Logical AND, OR, NOT

支持类型
Numeric, like 123, 3.1415
Character, like 'abc', must be made with single quotes
NULL, special constant
Boolean, TRUE or FALSE

生产者通过putUserProperty自定义属性,消费者通过表达式进行过滤:

public class SqlFilterProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
        producer.start();
        String[] tags = new String[] { "TagA""TagB""TagC" };
        for (int i = 0; i < 10; i++) {
            // 每个消息设置一个Tag
            Message msg = new Message("MyTopic", tags[i % tags.length], ("Hello" + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 每个消息设置自定义属性
            msg.putUserProperty("userId", String.valueOf(i));
            SendResult sendResult = producer.send(msg);
            System.out.println("sendResult=" + sendResult);
        }
        producer.shutdown();
    }
}

public class SqlFilterConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup");
        // SQL表达式
        consumer.subscribe("MyTopic", MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" + "and (userId is not null and a between 0 and 3)"));
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println("ThreadName=" + Thread.currentThread().getName() + ",messages=" + msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

(3) Filter Server

Filter Server支持用户自定义Java函数,Broker端会执行该函数对消息进行过滤。我们在编写函数时需要注意,不要包含大量消耗内存或者创建线程操作,否则可能造成Broker宕机:

public class Producer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
        producer.start();
        String[] tags = new String[] { "TagA""TagB""TagC" };
        for (int i = 0; i < 10; i++) {
            Message msg = new Message("MyTopic", tags[i % tags.length], ("Hello" + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            msg.putUserProperty("userId", String.valueOf(i));
            SendResult sendResult = producer.send(msg);
            System.out.println("sendResult=" + sendResult);
        }
        producer.shutdown();
    }
}

public class Consumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup");
        String codeScript = MixAll.file2String("/home/admin/filters/MyMessageFilterImpl.java");
        consumer.subscribe("MyTopic""com.java.front.rocketmq.test.filter.MyMessageFilterImpl", codeScript);
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for(MessageExt msg : msgs) {
                    System.out.println("ThreadName=" + Thread.currentThread().getName() + ",message=" + msg);
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }

        });
        consumer.start();
    }
}

public class MyMessageFilterImpl implements MessageFilter {

    @Override
    public boolean match(MessageExt msg) {
        String property = msg.getUserProperty("userId");
        if(StringUtils.isEmpty(property)) {
            return false;
        }
        if(Integer.parseInt(property) >= 3) {
            return true;
        }
        return false;
    }
}

2.3 消费者

消费者需要思考如何提高消费速度,尽快消费完积压消息。需要注意如果消费者还有下游依赖,例如订阅消息后写数据库或者调用下游应用,提高消费速度时也必须考虑下游依赖的能力。


(1) 优化消费逻辑

如果消息消费逻辑中存在慢SQL、慢服务等问题会降低消费速度,从而造成消息积压。我们可以使用例如Arthas等开源诊断工具,分析消费全链路每个方法响应时间,发现慢方法则进行优化。


(2) 增加消费线程

适当增加消费线程,增加一定的消费并发度也可以增加消费速度,RocketMQ提供两个方法设置线程数:

setConsumeThreadMin
消费最小线程数

setConsumeThreadMax
消费最大线程数

(3) 增加消费步长

每次消费可以多获取几条消息也可以增加消费速度,RocketMQ提供两个方法设置消费步长:

setPullBatchSize
单次从MessageQueue获取最大消息数量

setConsumeMessageBatchMaxSize
单次传给消费者执行器最大消息数量(参数List<MessageExt> msgs最大长度)

(4) 增加消费节点

由于单机处理能力有限,当消费线程和消费步长都已增加到瓶颈时,我们可以考虑扩容集群中消费节点。这个操作有两个注意点:第一是消费节点数不要超过消息分区数,第二是有序消费造成并发度低问题。


(5) 有序消费改为无序消费

RocketMQ按照顺序维度分为三类消息,普通消息并发度最好,但是不保证有序。全局有序消息有序性最好,但是并发度最差。分区有序消息既可以保证相同业务ID局部有序,又有保证一定并发度,但并发度受限于队列数。

(a) 普通消息

普通消息也被称为并发消息,生产时一条消息可能被写入任意一个队列里,消费者可以启动多个线程并行消费,这类消息无法保证顺序。虽然消息侧无法保证有序,但我们可以在业务侧使用状态机实现业务有序

(b) 分区有序消息

需要注意消息有序性需要生产者和消费者共同配合才能完成。生产者需要把相同业务ID的消息发送到同一个messageQueue,而在消费时一个messageQueue不可以并发处理,这在一定程度上影响了消费并发度

public class Producer {
    public static void main(String[] args) throws Exception {
        MQProducer producer = new DefaultMQProducer("producerGroup");
        producer.start();
        for (int i = 0; i < 10; i++) {
            int orderId = i;
            Message msg = new Message("MyTopic", ("Hello" + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Integer id = (Integer) arg;
                    int index = id % mqs.size(); // 根据业务编号计算队列下标
                    return mqs.get(index);
                }
            }, orderId);
        }
        producer.shutdown();
    }
}

public class Consumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("MyTopic""*")
        consumer.registerMessageListener(new MessageListenerOrderly() { // 有序消费
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                System.out.println("ThreadName=" + Thread.currentThread().getName() + ",messages=" + msgs);
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
    }
}

(c) 全局有序消息

全局有序是分区有序的一种特殊情况,如果一个主题只有一个消息队列时,那么就可以做到全局有序,但这种方案并发度最差


3 文章总结

本文我们分析了消息积压问题处理思路,处理方案分为不处理和要处理两大类。不处理是指在不影响业务的情况下利用消息系统削峰特性可以保护消费者,如果要处理我们从生产者、Broker、消费者三个维度进行了分析,即生产者减少生产消息量,Broker进行消息过滤,消费者增加消费速度,希望本文对大家有所帮助。




JAVA前线 


欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习


浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报