RocketMQ4.3.X笔记(9):高吞吐场景

在Broker 端进行消息过滤

在Broker 端进行消息过滤,可以减少无效消息发送到Consumer ,少占用网络带宽从而提高吞吐量。Broker 端有三种方式进行消息过滤

消息的Tag 和Key

  1. 消息的Tag
  • 一个应用来说,尽可能只用一个Topic,不同的消息子类型用Tag 来标识
  • 服务器端基于Tag 进行过滤,并不需要读取消息体的内容,所以效率很高。
  • 一个Message 只能有一个Tag
  1. 消息的Key
  • 对发送的消息设置好Key ,以后可以根据这个Key 来查找消息
  • 这个Key 一般用消息在业务层面的唯一标识码来表示,保证后续查询消息异常,消息丢失等都很方便
  • Broker 会创建专门的索引文件,来存储Key 到消息的映射,由于是哈希索引,应尽量使Key 唯一,避免潜在的晗希冲突
  1. Tag 和Key 的主要差别是使用场景不同
  • Tag 用在Consumer 的代码中,用来进行服务端消息过滤
  • Key 主要用于通过命令行查询消息

通过Tag 进行过滤

  1. Broker 端可以在ConsumeQueue 中做这种过滤,只从CommitLog 里读取过滤后被命中的消息。
  2. ConsumeQueue 存储格式

ConsumeQueue 存储格式

  1. ConsumeQueue 的第三部分存储的是Tag 对应的hashcode ,是一个定长的字符串,通过Tag 过滤的过程就是对比定长的hashcode 。经过hashcode 对比,符合要求的消息被从CommitLog 读取出来,不用担心Hash 冲突问题,消息在被消费前,会对比完整的Message Tag 字符串,消除Hash 冲突造成的误读

用SQL 表达式的方式进行过滤

  1. Tag 的方式,效率高,但是方式单一,逻辑简单
  2. 使用Message 的用户属性方式实现过滤:
  • Producer 在构造 Mesage 的时候,通过putUserProperty 函数来增加多个自定义的属性
  • Consumer (值支持 PushConsumer)用类似SQL 表达式的方式对消息进行过滤,使用 MessageSelector.bySql()
  • SQL 表达式方式的过滤需要Broker 先读出消息里的属性内容, 然后做SQL 计算,增大磁盘压力,没有Tag 方式高效
# Mesage 设置属性
Message message = new Message();
message.setTopic("property_topic");
message.putUserProperty("a", String.valueOf(i));
message.putUserProperty("b", "property_topic");

# 消费端 进行消费过滤
DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("base_consumer_group");
pushConsumer.subscribe("property_topic", MessageSelector.bySql("a between 0 and 3"));
  1. 类似SQL 的过滤表达式, 支持如下语法
  • 数字对比, 比如 >、>=、<、<= 、BETWEEN 、=
  • 字符串对比,比如= 、<>、IN
  • IS NULL or IS NOT NULL
  • 逻辑符号AND 、OR 、NOT
  1. 支持的数据类型
  • 数字型,比如123 、3.1415
  • 字符型,比如'abe' 、注意必须用单引号
  • 特殊字符, NULL
  • 布尔型, TRUE or FALSE

Filter Server 方式过滤

概述
  1. Filter Server 是一种比SQL 表达式更灵活的过滤方式,允许用户自定义Java 函数,根据Java 函数的逻辑对消息进行过滤
  2. Filter Server 类似一个RocketMQ 的Consumer 进程,它从本机Broker 获取消息,然后根据用户上传过来的Java 函数进行过滤,过滤后的消息再传给远端的Consumer
  3. 这种方式会占用很多Broker 机器的CPU 资源,要根据实际情况谨慎使用。上传的java代码也要经过检查,不能有申请大内存、创建线程等这样的操作,否则容易造成Broker 服务器宕机
实现
  1. Broker 配置,启动3 个FilterServer 进程
filterServerNums = 3
  1. 过滤逻辑代码,实现 org.apache.rocketmq.common.filter.MessageFilter 接口
# 实现了过滤逻辑,根据消息的 "SequenceId" 这个属性来过滤的
class MessageFilterImpl implements MessageFilter {

        @Override
        public boolean match(MessageExt msg, FilterContext context) {

            String property = msg.getUserProperty("SequenceId");
            if (property != null) {
                int id = Integer.parseInt(property);
                if ((id % 3) == 0 && (id > 10)) {
                    return true;
                }
            }

            return false;
        }
    }
  1. 使用FilteServer 的Consumer 实现
  • 主要是把实现过滤逻辑的类作为参数传到Broker 端,Broker 端的FilterServer 会解析这个类,然后根据m atch函数里的逻辑进行过
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("MessageFilterDemo");
consumer.setNamesrvAddr("10.0.64.106:9876;10.0.64.107:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);


# 使用Java 代码,在服务器做消息过滤
String filterCode = MixAll.file2String("/home/admin/MessageFilterImpl.java");
consumer.subscribe("TopicFilter", "com.learning.rocketmq.optimal.throughput.MessageFilterDemo.MessageFilterImpl", filterCode);

consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        System.out.println(Thread.currentThread().getName() + "Receive New Messages" + msgs);
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

提高Consumer 处理能力

当Consumer 的处理速度眼不上消息的产生速度,会造成越来越多的消息积压,这个时候首先查看消费逻辑本身有没有优化空间,除此之外还有三种方法可以提高Consumer 的处理能力

提高消费并行度

  1. 增加 Consumer 实例。
    在同一个ConsumerGroup 下( Clustering方式),可以通过增加Consumer实例的数量来提高并行度。通过加机器,或者在已有机器中启动多个Consumer进程都可以增加Consumer 实例数。注意总的Consumer 数量不要超过Topic 下ReadQueue 数量,超过的Consumer 实例接收不到消息。

  2. 提高单个Consumer实例的线程秉性数。
    通过提高单个Consumer 实例中的并行处理的线程数,修改consumeThreadMinconsumeThreadMax

以批量方式进行消费

  1. 某些业务场景下,多条消息同时处理的时间会大大小于逐个处理的时间总和,比如消费消息中涉及update 某个数据库, 一次update 10 条的时间会大大小于10次update 1 条数据的时间。

  2. 通过批量方式消费来提高消费的吞吐量。

  • 实现方法是设置Consumer 的 consumeMessageBatchMaxSize 这个参数,默认是 1
  • 如果设置为N,在消息多的时候每次收到的是个长度为N 的消息链表

检测延时情况,跳过非重要消息

Consumer 在消费的过程中, 如果发现由于某种原因发生严重的消息堆积,短时间无法消除堆积,这个时候可以选择丢弃不重要的消息,使Consumer 尽快追上Producer 的进度

pushConsumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {

        long offset = msgs.get(0).getQueueOffset();
        String maxOffset = msgs.get(0).getProperty(MessageConst.PROPERTY_MAX_OFFSET);
        long diff = Long.parseLong(maxOffset) - offset;

        // 当某个队列的消息数堆积到90000 条以上, 就直接丢弃
        if (diff > 90000) {
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }

        // 正常消费消息
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

Consumer 的负载均衡

  1. 在一个 ConsumerGroup 中启动多个 Consumer 并发处理,这个时候就涉及如何在多个Consumer 之间负载均衡的问题
  2. 在RocketMQ 中,负载均衡或者消息分配是在 Consumer 端代码中完成的, Consumer 从Broker 处获得全局信息(如 一个ConsumerGroup里到底有多少个Consumer),然后自己做负载均衡,只处理分给自己的那部分消息

DefaultMQPushConsumer 的负载均衡

  1. DefaultMQPushConsumer的负载均衡过程客户端会自动处理,每个DefaultMQPushConsumer 启动后,会马上会触发一个doRebalance 动作;而且在同一个ConsumerGroup 里加入新的DefaultMQPushConsumer时,各个Consumer 都会被触发doRebalance 动作

  2. 负载均衡算法有6种

  • AllocateMachineRoomNearby 机房近端优先级的分配策略代理
  • AllocateMessageQueueAveragely
  • AllocateMessageQueueAveragelyByCircle
  • AllocateMessageQueueByConfig
  • AllocateMessageQueueByMachineRoom
  • AllocateMessageQueueConsistentHash
  1. 影响负载均衡的因素
  • TopicMessageQueue数量
  • ConsumerGroup 里的Consumer 的数量
  1. 负载均衡的分配粒度只到MessageQueue ,把Topic 下的所有MessageQueue 分配到不同的Consumer 中,所以MessageQueue 和Consumer 的数量关系,或者整除关系影响负载均衡结果。

DefaultMQPullConsumer 的负载均衡

  1. PullConsumer 可以看到所有的MessageQueue, 而且从哪个MessageQueue 读取消息,读消息时的Offset都由使用者控制,可以实现任何特殊方式的负载均衡。

  2. DefaultMQPullConsumer 有两个辅助方法可以帮助实现负载均衡

  • registerMessageQueueListener() 方法, 在有新的Consumer 加人或退出时被触发
  • MQPullConsumerScheduleService 类,负载实现在其内部类MessageQueueListenerimpl 中,可以通过更改MessageQueueListenerimpl 的实现来自定义的负载均衡策略
  1. 例子
public class PullConsumerRebalance {


    /**
     * `registerMessageQueueListener()` 方法, 在有新的`Consumer` 加人或退出时被触发
     *
     * @throws MQClientException
     */
    public static void consumer() throws MQClientException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");

        consumer.start();

        consumer.registerMessageQueueListener("TopicTest1", new MessageQueueListener() {
            @Override
            public void messageQueueChanged(String topic, Set<MessageQueue> mqAll, Set<MessageQueue> mqDivided) {
                // 负载处理
            }
        });
    }


    /**
     * MQPullConsumerScheduleService 类的实现,负载实现在其内部类`MessageQueueListenerimpl` 中,可以通过更改`MessageQueueListenerimpl` 的实现来自定义的负载均衡策略
     *
     * @throws MQClientException
     */
    public static void consumer2() throws MQClientException {
        final MQPullConsumerScheduleService scheduleService = new MQPullConsumerScheduleService("PullConsumerServicel");
        scheduleService.getDefaultMQPullConsumer().setNamesrvAddr("");
        scheduleService.setMessageModel(MessageModel.CLUSTERING);
        scheduleService.registerPullTaskCallback("testPullConsumer", new PullTaskCallback() {
            @Override
            public void doPullTask(MessageQueue mq, PullTaskContext context) {
                MQPullConsumer consumer = context.getPullConsumer();
                try {
                    long offset = consumer.fetchConsumeOffset(mq, false);
                    if (offset < 0) {
                        offset = 0;
                    }
                    PullResult pullResult = consumer.pull(mq, "*", offset, 32);
                    // 根据不同的消息状态做不同的处理
                    switch (pullResult.getPullStatus()) {
                        case FOUND:
                            break;
                        case NO_MATCHED_MSG:
                            break;
                        case NO_NEW_MSG:
                        case OFFSET_ILLEGAL:
                            break;
                        default:
                            break;
                    }
                    consumer.updateConsumeOffset(mq, pullResult.getNextBeginOffset());
                    context.setPullNextDelayTimeMillis(1000);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        scheduleService.start();
    }
}

提高Producer 的发送速度

  1. 发送一条消息出去的步骤:
  • 客户端发送请求到服务器
  • 服务器处理该请求
  • 服务器向客户端返回应答
  1. 提高发送的方式
  • Oneway 方式,速度快,可靠性不高。只发送请求不等待应答,即将数据写人客户端的Socket 缓冲区就返回,不等待对方返回结果,用这种方式发送消息的耗时可以缩短到微秒级
  • 增加 Producer 的并发量。RocketMQ 引人了一个并发窗口,在窗口内消息可以并发地写人DirectMem 中,
    然后异步地将连续一段无空洞的数据刷人文件系统当中。顺序写CommitLog 可让RocketMQ 无论在HDD 还是SSD 磁盘情况下都能保持较高的写人性能。
  1. 在Linux 操作系统层级进行调优,
  • 推荐使用EXT4 文件系统。RocketMQ 的 CommitLog 会有频繁的创建、删除动作
  • IO 调度算法使用deadline 算法。
  1. deadline 算法大致思想如下:
  • 实现四个队列,其中两个处理正常的read 和write 操作,另外两个处理超时的 read 和write 操作。
  • 正常的read 和write 队列中,元素按扇区号排序,进行正常的IO 合并处理以提高吞吐量。因为IO 请求可能会集中在某些磁盘位置,这样会导致新来的请求一直被合并,可能会有其他磁盘位置的IO 请求被饿死。
  • 超时的read 和write 的队列中,元素按请求创建时间排序,如果有超时的请求出现,就放进这两个队列,调度算法保证超时(达到最终期限时间)的队列中的IO 请求会优先被处理

系统性能调优的一般流程

  1. 步骤:最终保持系统在峰值状态下运行,然后查看硬件
  • 搭建环境,模拟压测
  • 逐步增大请求量,检测系统的TPS
  • 维持系统的QPS 峰值状态运行
  1. TOP 命令: 查看CPU 和内存的利用率
  2. sar 命令:查看网卡使用情况
  • iperf3 命令,验证网卡是否达到了极限
  • netstat -t 查看网卡的连接情况
  1. iostat: 查看磁盘的使用情况

  2. Java 代码程序判断问题

参考

  1. Apache RocketMQ 官网
  2. Best Practice For Broker
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值