文章目录
在Broker 端进行消息过滤
在Broker 端进行消息过滤,可以减少无效消息发送到Consumer ,少占用网络带宽从而提高吞吐量。Broker 端有三种方式进行消息过滤
消息的Tag 和Key
- 消息的Tag
- 一个应用来说,尽可能只用一个Topic,不同的消息子类型用Tag 来标识
- 服务器端基于Tag 进行过滤,并不需要读取消息体的内容,所以效率很高。
- 一个Message 只能有一个Tag
- 消息的Key
- 对发送的消息设置好Key ,以后可以根据这个Key 来查找消息
- 这个Key 一般用消息在业务层面的唯一标识码来表示,保证后续查询消息异常,消息丢失等都很方便
- Broker 会创建专门的索引文件,来存储Key 到消息的映射,由于是哈希索引,应尽量使Key 唯一,避免潜在的晗希冲突
- Tag 和Key 的主要差别是使用场景不同
- Tag 用在Consumer 的代码中,用来进行服务端消息过滤
- Key 主要用于通过命令行查询消息
通过Tag 进行过滤
- Broker 端可以在ConsumeQueue 中做这种过滤,只从CommitLog 里读取过滤后被命中的消息。
- ConsumeQueue 存储格式
- ConsumeQueue 的第三部分存储的是Tag 对应的hashcode ,是一个定长的字符串,通过Tag 过滤的过程就是对比定长的hashcode 。经过hashcode 对比,符合要求的消息被从CommitLog 读取出来,不用担心Hash 冲突问题,消息在被消费前,会对比完整的Message Tag 字符串,消除Hash 冲突造成的误读
用SQL 表达式的方式进行过滤
- Tag 的方式,效率高,但是方式单一,逻辑简单
- 使用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"));
- 类似SQL 的过滤表达式, 支持如下语法
- 数字对比, 比如
>、>=、<、<= 、BETWEEN 、=
- 字符串对比,比如
= 、<>、IN
IS NULL or IS NOT NULL
- 逻辑符号
AND 、OR 、NOT
- 支持的数据类型
- 数字型,比如
123 、3.1415
- 字符型,比如
'abe'
、注意必须用单引号 - 特殊字符,
NULL
- 布尔型,
TRUE or FALSE
Filter Server 方式过滤
概述
- Filter Server 是一种比SQL 表达式更灵活的过滤方式,允许用户自定义Java 函数,根据Java 函数的逻辑对消息进行过滤
- Filter Server 类似一个RocketMQ 的Consumer 进程,它从本机Broker 获取消息,然后根据用户上传过来的Java 函数进行过滤,过滤后的消息再传给远端的Consumer
- 这种方式会占用很多Broker 机器的CPU 资源,要根据实际情况谨慎使用。上传的java代码也要经过检查,不能有申请大内存、创建线程等这样的操作,否则容易造成Broker 服务器宕机
实现
- Broker 配置,启动3 个FilterServer 进程
filterServerNums = 3
- 过滤逻辑代码,实现
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;
}
}
- 使用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 的处理能力
提高消费并行度
-
增加 Consumer 实例。
在同一个ConsumerGroup
下( Clustering方式),可以通过增加Consumer
实例的数量来提高并行度。通过加机器,或者在已有机器中启动多个Consumer
进程都可以增加Consumer
实例数。注意总的Consumer 数量不要超过Topic 下ReadQueue
数量,超过的Consumer
实例接收不到消息。 -
提高单个Consumer实例的线程秉性数。
通过提高单个Consumer 实例中的并行处理的线程数,修改consumeThreadMin
和consumeThreadMax
以批量方式进行消费
-
某些业务场景下,多条消息同时处理的时间会大大小于逐个处理的时间总和,比如消费消息中涉及update 某个数据库, 一次update 10 条的时间会大大小于10次update 1 条数据的时间。
-
通过批量方式消费来提高消费的吞吐量。
- 实现方法是设置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 的负载均衡
- 在一个 ConsumerGroup 中启动多个 Consumer 并发处理,这个时候就涉及如何在多个Consumer 之间负载均衡的问题
- 在RocketMQ 中,负载均衡或者消息分配是在 Consumer 端代码中完成的, Consumer 从Broker 处获得全局信息(如 一个ConsumerGroup里到底有多少个Consumer),然后自己做负载均衡,只处理分给自己的那部分消息
DefaultMQPushConsumer 的负载均衡
-
DefaultMQPushConsumer
的负载均衡过程客户端会自动处理,每个DefaultMQPushConsumer
启动后,会马上会触发一个doRebalance
动作;而且在同一个ConsumerGroup
里加入新的DefaultMQPushConsumer
时,各个Consumer 都会被触发doRebalance
动作 -
负载均衡算法有6种
AllocateMachineRoomNearby
机房近端优先级的分配策略代理AllocateMessageQueueAveragely
AllocateMessageQueueAveragelyByCircle
AllocateMessageQueueByConfig
AllocateMessageQueueByMachineRoom
AllocateMessageQueueConsistentHash
- 影响负载均衡的因素
Topic
的MessageQueue
数量ConsumerGroup
里的Consumer
的数量
- 负载均衡的分配粒度只到
MessageQueue
,把Topic 下的所有MessageQueue
分配到不同的Consumer 中,所以MessageQueue 和Consumer 的数量关系,或者整除关系影响负载均衡结果。
DefaultMQPullConsumer 的负载均衡
-
PullConsumer
可以看到所有的MessageQueue
, 而且从哪个MessageQueue
读取消息,读消息时的Offset
都由使用者控制,可以实现任何特殊方式的负载均衡。 -
DefaultMQPullConsumer
有两个辅助方法可以帮助实现负载均衡
registerMessageQueueListener()
方法, 在有新的Consumer
加人或退出时被触发MQPullConsumerScheduleService
类,负载实现在其内部类MessageQueueListenerimpl
中,可以通过更改MessageQueueListenerimpl
的实现来自定义的负载均衡策略
- 例子
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 的发送速度
- 发送一条消息出去的步骤:
- 客户端发送请求到服务器
- 服务器处理该请求
- 服务器向客户端返回应答
- 提高发送的方式
Oneway
方式,速度快,可靠性不高。只发送请求不等待应答,即将数据写人客户端的Socket 缓冲区就返回,不等待对方返回结果,用这种方式发送消息的耗时可以缩短到微秒级- 增加 Producer 的并发量。RocketMQ 引人了一个并发窗口,在窗口内消息可以并发地写人
DirectMem
中,
然后异步地将连续一段无空洞的数据刷人文件系统当中。顺序写CommitLog
可让RocketMQ 无论在HDD 还是SSD 磁盘情况下都能保持较高的写人性能。
- 在Linux 操作系统层级进行调优,
- 推荐使用
EXT4
文件系统。RocketMQ 的 CommitLog 会有频繁的创建、删除动作 - IO 调度算法使用
deadline
算法。
- deadline 算法大致思想如下:
- 实现四个队列,其中两个处理正常的read 和write 操作,另外两个处理超时的 read 和write 操作。
- 正常的read 和write 队列中,元素按扇区号排序,进行正常的IO 合并处理以提高吞吐量。因为IO 请求可能会集中在某些磁盘位置,这样会导致新来的请求一直被合并,可能会有其他磁盘位置的IO 请求被饿死。
- 超时的read 和write 的队列中,元素按请求创建时间排序,如果有超时的请求出现,就放进这两个队列,调度算法保证超时(达到最终期限时间)的队列中的IO 请求会优先被处理
系统性能调优的一般流程
- 步骤:最终保持系统在峰值状态下运行,然后查看硬件
- 搭建环境,模拟压测
- 逐步增大请求量,检测系统的TPS
- 维持系统的QPS 峰值状态运行
TOP
命令: 查看CPU 和内存的利用率sar
命令:查看网卡使用情况
iperf3
命令,验证网卡是否达到了极限netstat -t
查看网卡的连接情况
-
iostat
: 查看磁盘的使用情况 -
Java 代码程序判断问题