消费者重要参数
-
ConsumeFromWhere
代表消费者首次消费从哪个位置开始,当消费者再次消费就会根据上传到nameserver上到消费进度开始消费。分成三种:CONSUME_FROM_LAST_OFFSET(从队尾开始消费)、CONSUME_FROM_FIRST_OFFSET(从队首开始消费)、CONSUME_FROM_TIMESTAMP(从特定时间开始消费)。
-
AllocateMessageQueueStrategy
代表消息队列分配到消费者到策略,有如下几种:平均分配策略(默认)(AllocateMessageQueueAveragely)、环形分配策略(AllocateMessageQueueAveragelyByCircle)、手动配置分配策略(AllocateMessageQueueByConfig)、机房分配策略(AllocateMessageQueueByMachineRoom)、一致性哈希分配策略(AllocateMessageQueueConsistentHash)。具体参照:Rocketmq之消息队列分配策略算法实现的源码分析
-
offsetStore
有两种模式:RemoteBrokerOffsetStore和LocalfileOffsetStore
Rocketmq集群有两种消费模式:
1、默认是 CLUSTERING 模 式,也就是同一个 Consumer group 里的多个消费者每人消费一部分,各自收到 的消息内容不一样 。 这种情况下,由 Broker 端存储和控制 Offset 的值,使用 RemoteBrokerOffsetStore 结构 。
2、BROADCASTING模式下,每个 Consumer 都收到这个 Topic 的全部消息,各个 Consumer 间相互没有干扰, RocketMQ 使 用 LocalfileOffsetStore,把 Offset存到本地 。 -
consumeThreadMin/consumeThreadMax
调整消费者线程池的线程数量,辅助consume消费。
-
consumeConcurrentlyMaxSpan/pullThresholdForQueue
这两个参数用于限流,第一个表示单个队列并行消费最大的跨度,第二个表示单个队列最大的消费个数。
-
consumeMessageBatchMaxSize
消费者每次消费消息的最大条数,默认是1。每次只能拉取一条消息进行消费。
Offset
Offset是消息消费进度的核心。Offset指某个topic下的一条消息在某个MessageQueue里的位置。通过Offset可以进行定位到这条消息。push模式Offset是由远程Broker维护,pull模式Offset是由本地维护。
MQ中Pull和Push的两种消费方式
对于任何一款消息中间件而言,消费者客户端一般有两种方式从消息中间件获取消息并消费。严格意义上来讲,RocketMQ并没有实现PUSH模式,而是对拉模式进行一层包装,名字虽然是 Push 开头,实际在实现时,使用 Pull 方式实现。通过 Pull 不断不断不断轮询 Broker 获取消息。当不存在新消息时,Broker 会挂起请求,直到有新消息产生,取消挂起,返回新消息。这样,基本和 Broker 主动 Push 做到接近的实时性(当然,还是有相应的实时性损失)。原理类似 长轮询( Long-Polling )
(1)Pull方式
由消费者客户端主动向消息中间件(MQ消息服务器代理)拉取消息;采用Pull方式,如何设置Pull消息的频率需要重点去考虑,举个例子来说,可能1分钟内连续来了1000条消息,然后2小时内没有新消息产生(概括起来说就是“消息延迟与忙等待”)。如果每次Pull的时间间隔比较久,会增加消息的延迟,即消息到达消费者的时间加长,MQ中消息的堆积量变大;若每次Pull的时间间隔较短,但是在一段时间内MQ中并没有任何消息可以消费,那么会产生很多无效的Pull请求的RPC开销,影响MQ整体的网络性能;
(2)Push方式
由消息中间件(MQ消息服务器代理)主动地将消息推送给消费者;采用Push方式,可以尽可能实时地将消息发送给消费者进行消费。但是,在消费者的处理消息的能力较弱的时候(比如,消费者端的业务系统处理一条消息的流程比较复杂,其中的调用链路比较多导致消费时间比较久。概括起来地说就是“慢消费问题”),而MQ不断地向消费者Push消息,消费者端的缓冲区可能会溢出,导致异常;
RocketMQ中两种消费方式的demo代码
(1)Pull模式的Consumer端代码如下
在示例代码中,可以看到业务工程在Consumer启动后,Consumer主动获取MessageQueue的Set集合,遍历该集合中的每一个队列,发送Pull的请求(参数中带有队列中的消息偏移量),同时需要Consumer端自己保存消息消费的offset偏移量至本地变量中。在Pull模式下,需要业务应用代码自身去完成比较多的事情,因此在实际应用中用的较少。
public class ConsumerPullTest {
private String namesrvAddr = "192.168.152.129:9876;192.168.152.130:9876";
private String TOPIC_TEST = "TOPIC_TEST";
private String TAG_TEST = "TAG_TEST";
//pull消费模式
private DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("ConsumerTest");
private static final Map<MessageQueue, Long> offseTable = new HashMap<MessageQueue, Long>();
@PostConstruct
public void start() {
try {
System.out.println("MQ:ConsumerPullTest");
//设置nameserver地址
consumer.setNamesrvAddr(namesrvAddr);
//消费模式 集群消费
consumer.setMessageModel(MessageModel.CLUSTERING);
//启动消费
consumer.start();
consumeMessage();
System.out.println("\n\t MQ:start ConsumerTest is success ! \n\t"
+ " topic is " + TOPIC_TEST + " \n\t"
+ " tag is " + TAG_TEST + " \n\t");
} catch (MQClientException e) {
System.out.println("MQ:start ConsumerTest is fail" + e.getResponseCode() + e.getErrorMessage());
throw new RuntimeException(e.getMessage(), e);
}
}
public void consumeMessage() throws MQClientException {
//根据topic查询queue
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues(TOPIC_TEST);
for(MessageQueue mq : mqs) {
System.out.printf("Consume from the queue: %s%n", mq);
SINGLE_MQ:while (true) {
try {
PullResult pullResult =
consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
case FOUND:
//如果找到
for(Message message : pullResult.getMsgFoundList()) {
System.out.println(new String(message.getBody()));
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break SINGLE_MQ;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
offseTable.put(mq, offset);
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = offseTable.get(mq);
if (offset != null) {
return offset;
}
return 0;
}
@PreDestroy
public void stop() {
if (consumer != null) {
consumer.shutdown();
System.out.println("MQ:stop ConsumerTest success! ");
}
}
public static void main(String [] args) {
ConsumerPullTest consumerTest = new ConsumerPullTest();
consumerTest.start();
}
}
上面是官网例子里的代码,但是我们通常使用另一种方式实现:
public class ConsumerPullDemo {
public static void main(String[] args) {
String groupName = "schedule_consumer";
String TOPIC_TEST = "TOPIC_TEST";
final MQPullConsumerScheduleService scheduleService = new MQPullConsumerScheduleService(groupName);
scheduleService.getDefaultMQPullConsumer().setNamesrvAddr("192.168.152.129:9876;192.168.152.130:9876");
scheduleService.setMessageModel(MessageModel.CLUSTERING);
scheduleService.registerPullTaskCallback(TOPIC_TEST, new PullTaskCallback() {
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:
List<MessageExt> list = pullResult.getMsgFoundList();
for (MessageExt msg : list) {
System.out.println(new String(msg.getBody()));
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
case OFFSET_ILLEGAL:
break;
default:
break;
}
//存储offset,客户端每隔5s会定时刷新到broker
consumer.updateConsumeOffset(mq, pullResult.getNextBeginOffset());
//重新拉取 建议超过5s这样就不会重复获取
context.setPullNextDelayTimeMillis(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
(2)Push模式的Consumer端代码如下
在示例代码中,业务工程的应用程序使用Push方式进行消费时,Consumer端注册了一个监听器,Consumer在收到消息后主动调用这个监听器完成消费并进行对应的业务逻辑处理。由此可见,业务应用代码只需要完成消息消费即可,无需参与MQ本身的一些任务处理(ps:业务代码显得更为简洁一些)。
public class ConsumerTest implements MessageListenerConcurrently {
private String namesrvAddr = "192.168.152.131:9876;192.168.152.133:9876";
private String TOPIC_TEST = "TOPIC_TEST";
private String TAG_TEST = "TAG_TEST";
private DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerTest");
@PostConstruct
public void start() {
try {
System.out.println("MQ:启动ConsumerTest消费者");
//设置nameserver地址
consumer.setNamesrvAddr(namesrvAddr);
//从队列最后开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
//消费模式 集群消费
consumer.setMessageModel(MessageModel.CLUSTERING);
//订阅的topic
consumer.subscribe(TOPIC_TEST, TAG_TEST);
//push消费设置监听
consumer.registerMessageListener(this);
//启动消费
consumer.start();
System.out.println("\n\t MQ:start ConsumerTest is success ! \n\t"
+ " topic is " + TOPIC_TEST + " \n\t"
+ " tag is " + TAG_TEST + " \n\t");
} catch (MQClientException e) {
System.out.println("MQ:start ConsumerTest is fail" + e.getResponseCode() + e.getErrorMessage());
throw new RuntimeException(e.getMessage(), e);
}
}
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
//push方法 消费者先启动的话 每次读取到一条数据就会返回
MessageExt msg = msgs.get(0);
String messageBody = "";
try {
messageBody = new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET);
messageBody = new String(messageBody.getBytes());
System.out.println("MQ:ConsumerTest consume msg is "+
msg.getMsgId()+ "topic:" + msg.getTopic() + "tag:" + msg.getTags() + "key:" + msg.getKeys() + "message:" + messageBody);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
System.out.println("errorQueue send fail e:" + e);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;// 重试
}
}
@PreDestroy
public void stop() {
if (consumer != null) {
consumer.shutdown();
System.out.println("MQ:stop ConsumerTest success! ");
}
}
public static void main(String [] args) {
ConsumerTest consumerTest = new ConsumerTest();
consumerTest.start();
}
}
实现原理
与消息的生成者启动类似,消费者也需要先创建group,再调用start方法启动,根据pull和push的消费模式进行对消息的处理。让我们看看消费者是如何实现的。
核心类
无论是pull还是push消费模式都要一些核心类。
(1)RebalanceImpl
字面上的意思(重新平衡)也就是消费端消费者与消息队列的重新分布,与消息应该分配给哪个消费者消费息息相关。负责分配当前 Consumer 可消费的消息队列( MessageQueue )。当有新的 Consumer 的加入或移除,都会重新分配消息队列。启动MQClientInstance实例时候,会完成负载均衡服务线程—RebalanceService的启动(每隔20s执行一次)。
-
Rebalance的平衡粒度
Rebalance是针对Topic+ConsumerGroup进行Rebalance的,在我们创建的comsumer过程中会订阅topic(包括%retry%consumerGroup),Rebalance就是要这些Topic下的所有messageQueue按照一定的规则分发给consumerGroup下的consumer进行消费。
doRebalance是主要的处理方法。public void doRebalance(boolean isOrder) { //subTable的key是consumerGroup,value是topic的订阅信息 Map<String, SubscriptionData> subTable = this.getSubscriptionInner(); if (subTable != null) { Iterator i$ = subTable.entrySet().iterator(); while(i$.hasNext()) { Entry<String, SubscriptionData> entry = (Entry)i$.next(); String topic = (String)entry.getKey(); try { //根据topic进行rebalance this.rebalanceByTopic(topic, isOrder); } catch (Throwable var7) { if (!topic.startsWith("%RETRY%")) { log.warn("rebalanceByTopic Exception", var7); } } } } this.truncateMessageQueueNotMyTopic(); }
-
Rebalance的平衡过程
Rebalance的过程分为三个步骤:
1、获取messageQueue队列和consumer列表
2、进行Rebalance操作
3、如果分配的结果改变了,需要做更新private void rebalanceByTopic(String topic, boolean isOrder) { Set mqSet; switch(this.messageModel) { case BROADCASTING: mqSet = (Set)this.topicSubscribeInfoTable.get(topic); if (mqSet != null) { boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder); if (changed) { this.messageQueueChanged(topic, mqSet, mqSet); log.info("messageQueueChanged {} {} {} {}", new Object[]{ this.consumerGroup, topic, mqSet, mqSet}); } } else { log.warn("doRebalance, {}, but the topic[{}] not exist.", this.consumerGroup, topic); } break; //只分析集群模式 case CLUSTERING: //根据topic获取messageQueue mqSet = (Set)this.topicSubscribeInfoTable.get(topic); //获取consumerGroup中所有的consumer List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, this.consumerGroup); if (null == mqSet && !topic.startsWith("%RETRY%")) { log.warn("doRebalance, {}, but the topic[{}] not exist.", this.consumerGroup, topic); } if (null == cidAll) { log.warn("doRebalance, {} {}, get consumer id list failed", this.consumerGroup, topic); } if (mqSet != null && cidAll != null) { List<MessageQueue> mqAll = new ArrayList(); mqAll.addAll(mqSet); Collections.sort(mqAll); Collections.sort(cidAll); //获取处理rebalance的逻辑类 AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy; List allocateResult = null; try { //进行Rebalance,返回当前消费者需要处理的messageQueue allocateResult = strategy.allocate(this.consumerGroup, this.mQClientFactory.getClientId(), mqAll, cidAll); } catch (Throwable var10) { log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(), var10); return; } Set<MessageQueue> allocateResultSet = new HashSet(); if (allocateResult != null) { allocateResultSet.addAll(allocateResult); } //更新消费者应该消费的队列,返回是否消费列表改变 boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder); if (changed) { log.info("rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}", new Object[]{ strategy.getName(), this.consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(), allocateResultSet.size(), allocateResultSet}); //判断状态,真正处理逻辑 this.messageQueueChanged(topic, mqSet, allocateResultSet); } } } }
-
Rebalance策略
这个策略有多个类,举其中一种策略说明,这个策略是考虑当前consumerId的位置,consumer的数量,MessageQueue的数量,根据consumerId所处的位置决定分配多少消费队列。例如:8个队列,2个consumer,第一个consumer处理队列中下标为0-3,第二个consumer处理队列下标为4-7。
类似于分页的算法,将所有MessageQueue排好序类似于记录,将所有消费端Consumer排好序类似页数,并求出每一页需要包含的平均size和每个页面记录的范围range,最后遍历整个range而计算出当前Consumer端应该分配到的记录(这里即为:MessageQueue)。public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll, List<String> cidAll) { if (currentCID != null && currentCID.length() >= 1) { if (mqAll != null && !mqAll.isEmpty()) { if (cidAll != null && !cidAll.isEmpty()) { List<MessageQueue> result = new ArrayList(); if (!cidAll.contains(currentCID)) { this.log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}", new Object[]{ consumerGroup, currentCID, cidAll}); return result; } else { int index = cidAll.indexOf(currentCID); int mod = mqAll.size() % cidAll.size(); int averageSize = mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size()); int startIndex = mod > 0 && index < mod ? index * averageSize : index * averageSize + mod; int range = Math.min(averageSize, mqAll.size() - startIndex); for(int i = 0; i < range; ++i) { result.add(mqAll.get((startIndex + i) % mqAll.size())); } return result; } } else { throw new IllegalArgumentException("cidAll is null or cidAll empty"); } } else { throw new IllegalArgumentException("mqAll is null or mqAll empty"); } } else { throw new IllegalArgumentException("currentCID is empty"); } }
-
Rebalance更新Queue
调用updateProcessQueueTableInRebalance()方法,具体的做法是,先将分配到的消息队列集合(mqSet)与processQueueTable做一个过滤比对,具体的过滤比对方式如下图:
private boolean updateProcessQueueTableInRebalance(String topic, Set<MessageQueue> mqSet, boolean isOrder) { boolean changed = false; //因为是在定时任务里不断做更新,所以先取出已经存在的mq队列集合 Iterator it = this.processQueueTable.entrySet().iterator(); while(it.hasNext()) { //将已经存在的queue和刚刚分配的queue做比对 Entry<MessageQueue, ProcessQueue> next = (Entry)it.next(); MessageQueue mq = (MessageQueue)next.getKey(); ProcessQueue pq = (ProcessQueue)next.getValue(); if (mq.getTopic().equals(topic)) { //没有匹配上,从processQueueTable中删除 if (!mqSet.contains(mq)) { pq.setDropped(true); if (this.removeUnnecessaryMessageQueue(mq, pq)) { it.remove(); changed = true; log.info("doRebalance, {}, remove unnecessary mq, {}", this