RocketMQプッシュメッセージの解析をコンシューマーに-図、ソースレベルの解析

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月更新チャレンジ」に参加して15日目です。クリックしてイベントの詳細をご覧ください


概要

RocketMQのコンシューマーが消費するメッセージを取得するには、プッシュモードとプルモードの2つの方法があります。

  1. プッシュモード:サーバーはメッセージをクライアントにプッシュします
  2. プルモード:クライアントはサーバー、おそらくメッセージを要求し続けます

実際、これら2つのモードの基本的な実装は、コンシューマーが積極的にメッセージをプルする方法です。

Pushメソッドでは、Consumerポーリングプロセスがカプセル化され、MessageListenerリスナーが登録されます。メッセージがプルされると、リスナーは消費のためにウェイクアップされるため、メッセージがサーバーによってプッシュされたように感じられます。

プルモードでは、コンシューマーがメッセージをプルするプロセスを作成する必要があります。

  1. TopicすべてのMQを取得するによると
  2. MQからメッセージをプルする
  3. 次回メッセージをプルするときのオフセットを記録して、後でこの位置でメッセージを引き続き取得できるようにします。

リアルタイムメッセージを保証するRocketMQのメカニズム?ここで、RocketMQは、同期IOの概念に少し似た長い接続方法を使用します。サーバーからのメッセージがない場合、要求はブロックされ、データまたはタイムアウトが発生するまで返されません。コンシューマーはメッセージを処理した後、サーバーに新しい要求を送信し、上記のプロセスを繰り返します...ここに画像の説明を挿入

プッシュメッセージ消費プロセス

全体的なコード実行フローは次のとおりです。 ここに画像の説明を挿入

メッセージの消費を有効にする

DefaultMQPushConsumerImpl#startメソッドのセクションに戻ります。

// 注册消费者
 boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
 // 注册失败则抛出异常
 if (!registerOK) {
     this.serviceState = ServiceState.CREATE_JUST;
     this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown());
     throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
         + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
         null);
 }
 // 开启消息消费
 mQClientFactory.start();
复制代码

MQコンシューマーの登録が完了すると、mQClientFactory.start();メッセージを消費するためにコンシューマーを起動するために呼び出されることがわかります。ソースコードは次のとおりです。

public void start() throws MQClientException {

        synchronized (this) {
            switch (this.serviceState) {
                case CREATE_JUST:
                    this.serviceState = ServiceState.START_FAILED;
                    // 如果注册中心得url未给出,可以通过Http请求从其他地方获取
                    if (null == this.clientConfig.getNamesrvAddr()) {
                        this.mQClientAPIImpl.fetchNameServerAddr();
                    }
                    // 启动响应-响应通道
                    this.mQClientAPIImpl.start();
                    // 启动多个定时任务
                    this.startScheduledTask();
                    // 启动pull取消息服务
                    this.pullMessageService.start();
                    // Start rebalance service
                    this.rebalanceService.start();
                    // 传入false表示不启动push服务
                    this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
                    log.info("the client factory [{}] start OK", this.clientId);
                    this.serviceState = ServiceState.RUNNING;
                    break;
                case START_FAILED:
                    throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
                default:
                    break;
            }
        }
    }
复制代码

ソースコードから、メッセージの消費を有効にした後、サービスのステータスに応じてメッセージを消費するためのさまざまな準備が行われることがわかります。

  1. サービスのステータスが、の場合CREATE_JUST、最初にサービスのステータスをに設定しますSTART_FAILED。すべての初期化タスクが正常に完了すると、サービスのステータスはに設定されRUNNING、実行中であることを示します。
  2. this.mQClientAPIImpl.start();オープンリクエスト/レスポンス、最下層はNettyに基づいています
  3. this.startScheduledTask();启动多个定时任务,如果此时注册中心地址为null,则会没2min获取一次
  4. this.pullMessageService.start();开始拉取消息
  5. this.rebalanceService.start();消费者开启负载均衡消费消息,每20s一次,根据负载均衡策略选择机器



接收消息

通过上面的分析,this.pullMessageService.start();开启了接收消息模式,其中PullMessageService类继承了ServiceThread类,意味着PullMessageService使用多线程消费消息。 ここに画像の説明を挿入PullMessageService对象调用start方法时,会执行多线程定义的run方法来拉取消息:

@Override
    public void run() {
        log.info(this.getServiceName() + " service started");
		// 服务没有停止则执行循环
        while (!this.isStopped()) {
            try {
            	// 使用BlockingQueue阻塞队列,获取队列中的请求并执行
                PullRequest pullRequest = this.pullRequestQueue.take();
                // 拉取消息
                this.pullMessage(pullRequest);
            } catch (InterruptedException ignored) {
            } catch (Exception e) {
                log.error("Pull Message Service Run Method exception", e);
            }
        }

        log.info(this.getServiceName() + " service end");
    }
复制代码

其中用到了BlockingQueue阻塞队列来进行消息的拉取,当提交了消息拉取请求后,如果队列里面为空则立刻执行。

可以发现DefaultMQPushConsumerImpl调用的还是拉取消息的方法,拉取消息的源码如下:

public void pullMessage(final PullRequest pullRequest) {
        /*
        ......
         */
        
        // 判断队列是否被lock住,如果被锁住了,则延迟一段时间再拉取
        // 顺序消费的逻辑
        if (processQueue.isLocked()) {
            if (!pullRequest.isPreviouslyLocked()) {
                long offset = -1L;
                try {
                    offset = this.rebalanceImpl.computePullFromWhereWithException(pullRequest.getMessageQueue());
                } catch (Exception e) {
                    this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
                    log.error("Failed to compute pull offset, pullResult: {}", pullRequest, e);
                    return;
                }
                boolean brokerBusy = offset < pullRequest.getNextOffset();
                log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
                        pullRequest, offset, brokerBusy);
                if (brokerBusy) {
                    log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
                            pullRequest, offset);
                }

                pullRequest.setPreviouslyLocked(true);
                pullRequest.setNextOffset(offset);
            }
        } else {
            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
            log.info("pull message later because not locked in broker, {}", pullRequest);
            return;
        }
		
		// 获取Topic对应的订阅信息,如果不存在,则延迟拉取消息
        final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
        if (null == subscriptionData) {
            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
            log.warn("find the consumer's subscription failed, {}", pullRequest);
            return;
        }
        
        /*
        ......
         */
    }
复制代码

拉取消息的具体处理步骤如下:

  1. 判断ProcessQueue是否被废弃,如果为true则直接返回
  2. 记录最后拉取消息的时间
  3. 判断Consumer是否正在运行,如果返回false则延迟3000ms拉取消息
  4. 判断Consumer是否被锁住,如果返回true则延迟1000ms拉取消息
  5. 判断Consumer持有的消息数量是否超过最大数量1000,如果返回true则说明消费者缓冲区已经满了,延迟50ms拉取消息
  6. 判断消息Offset是否大于2000,如果返回true则延迟50ms拉取消息
  7. 顺序消费消息,使用分布式锁锁定MQ来保证一条一条消费消息。如果MQ不是被第一次锁定,则从上一次消费到的位置开始消费,如果获取锁失败则延迟一段时间再拉取消息
  8. 如果获取到的offset小于nextOffset,说明已经越界,延迟3000ms再进行消费

默认消费方式是从MaxOffset开始往前消费的

  1. 获取Topic对应的订阅消息如果不存在则延迟拉3000ms取消息
  2. 拉取消息使用带有回调的PullCallback,当拉取消息成功时开始消费

具体的消费消息的逻辑是使用向pullCallback中传入匿名内部类的方式,一旦拉取到消息之后,就会回调到内部类中的onSuccess方法执行具体的逻辑,消费消息的具体流程源码如下所示:

PullCallback pullCallback = new PullCallback() {
    @Override
    public void onSuccess(PullResult pullResult) {
        // 拉取结果不为空
        if (pullResult != null) {
            pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
                    subscriptionData);

            switch (pullResult.getPullStatus()) {
                case FOUND:
                    // 设置下次拉取消息的offset
                    long prevRequestOffset = pullRequest.getNextOffset();
                    pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                    long pullRT = System.currentTimeMillis() - beginTimestamp;
                    DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
                            pullRequest.getMessageQueue().getTopic(), pullRT);

                    long firstMsgOffset = Long.MAX_VALUE;
                    // 如果没有拉取到消息则立刻再拉取消息一次
                    if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
                        DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                    } else {
                        firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();

                        DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
                                pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
                        // 提交拉取到的消息到processQueue中,返回上一批次的消息是否已经消费完了
                        boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
                        // 在有序模式下,只有dispatchToConsume为true才提交,并发模式不受影响
                        DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                                pullResult.getMsgFoundList(),
                                processQueue,
                                pullRequest.getMessageQueue(),
                                dispatchToConsume);
                        // 如果处理消息都需要和上次保持一定时间间隔,则稍后再执行
                        if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
                            DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
                                    DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
                        } else {
                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                        }
                    }

                    // 当前offset小于上一次的offset则报错
                    if (pullResult.getNextBeginOffset() < prevRequestOffset
                            || firstMsgOffset < prevRequestOffset) {
                        log.warn(
                                "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
                                pullResult.getNextBeginOffset(),
                                firstMsgOffset,
                                prevRequestOffset);
                    }

                    break;
                case NO_NEW_MSG:
                case NO_MATCHED_MSG:    // 没有匹配到消息的情况
                    // 设置下次拉取消息的offset
                    pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                    // 持久化消费进度
                    DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
                    // 提交消费请求
                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                    break;
                case OFFSET_ILLEGAL:    // offset非法的情况
                    log.warn("the pull request offset illegal, {} {}",
                            pullRequest.toString(), pullResult.toString());
                    // 设置下次拉取消息的offset
                    pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                    // 设置ProcessQueue废弃
                    pullRequest.getProcessQueue().setDropped(true);
                    // 提交延迟处理任务,将ProcessQueue移除
                    DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {

                        @Override
                        public void run() {
                            try {
                                // 更新消费进度,同步消费进度到Broker
                                DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
                                        pullRequest.getNextOffset(), false);

                                DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());
                                // 将ProcessQueue移除
                                DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());

                                log.warn("fix the pull request offset, {}", pullRequest);
                            } catch (Throwable e) {
                                log.error("executeTaskLater Exception", e);
                            }
                        }
                    }, 10000);
                    break;
                default:
                    break;
            }
        }
    }
};
复制代码

处理异常的逻辑:

@Override
public void onException(Throwable e) {
    if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
        log.warn("execute the pull request exception", e);
    }
    // 出现异常的话就延迟拉取消息
    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
}
复制代码

根据上面的代码,具体的消费消息的流程:

  1. 使用pullAPIWrapper获取内存中ByteBuffer中的数据,得到拉取消息的结果
  2. 根据拉取消息后结果的状态来判断是否有消息可以被消费

a. FOUND:发现消息 b. NO_NEW_MSG:没有新消息可被拉取 c. NO_MATCHED_MSG:消息不匹配 d. OFFSET_ILLEGAL:offset非法,可能是消息太大

如果是FOUND状态,表示可以进行下一步的消费,之后的步骤如下:

  1. 获取消息拉取的offset,设置下一次拉取消息的offset,同时统计消息消费响应时间
  2. 如果没有拉取到消息,马上进行下一次拉取,如果拉取到消息,则把消息提交至ProcessQueue
  3. 否则,提交拉取到的消息到processQueue中,返回上一批次的消息是否已经消费完了(在有序模式下,只有dispatchToConsume为true才提交,并发模式不受影响);如果处理消息都需要和上次保持一定时间间隔,则稍后再执行

如果是OFFSET_ILLEGAL状态,之后的步骤如下:

  1. 设置下次拉取消息的offset
  2. 设置ProcessQueue废弃
  3. 提交延迟处理任务,更新消费进度,同步消费进度到Broker,并将ProcessQueue移除

ここでは、基になるデータ構造をProcessQueue使用しTreeMapて検索効率を向上させ、メッセージが消費されたかどうかをすばやく判断します。

おすすめ

転載: juejin.im/post/7086663727134539789