マイクロチャネル公共番号「バックエンドアドバンス」、バックエンド技術に焦点を当てたが、共有する:Javaの、Golang、WEBフレームワーク、分散ミドルウェア、サービス管理、など。
しばらく前に友人が私に質問をし、彼は言った、次のように具体的な質問があり、RocketMQクラスタを構築する過程で消費者のサブスクリプションに関する質問に直面していました:
それから彼は私にエラーログを作りました:
the consumer's subscription not exist
ソースの位置におけるエラーを発見I初めて。
org.apache.rocketmq.broker.processor.PullMessageProcessor#のproce *** equest:
subscriptionData = consumerGroupInfo.findSubscriptionData(requestHeader.getTopic());
if (null == subscriptionData) {
log.warn("the consumer's subscription not exist, group: {}, topic:{}", requestHeader.getConsumerGroup(), requestHeader.getTopic());
response.setCode(ResponseCode.SUBSCRIPTION_NOT_EXIST);
response.setRemark("the consumer's subscription not exist" + FAQUrl.suggestTodo(FAQUrl.SAME_GROUP_DIFFERENT_TOPIC));
return response;
}
ここではトピックのサブスクリプション情報を見つけるための源であるが、ここで見つけることができませんでしたので、間違った消費者のサブスクリプションが存在しないと報告しました。
友達にも私に彼消費クラスタを告げ、そのトピックにサブスクライブし、各消費者は、彼のコンシューマ・グループの消費者は、c1とc2、c1がTOPICAを購読し、c2はtopicBを購読しています。
その後、私はすでに理由は、私が第1グループの消費者の加入情報ブローカーの話を知っているグループ化されている次のように、データ構造は次のとおりです。
org.apache.rocketmq.broker.client.ConsumerManager:
private final ConcurrentMap<String/* Group */, ConsumerGroupInfo> consumerTable =
new ConcurrentHashMap<String, ConsumerGroupInfo>(1024);
これは、消費者契約情報の各クラスタが理由ですお互いのサブスクリプション情報うち、上書きを仲介するために登録するときにことを意味し、同じコンシューマ・グループがまったく同じサブスクリプション関係が必要な理由を、同じ消費者で友人各コンシューマ・グループのサブスクリプションの関係がカバーされ、互いのサブスクリプション情報に問題がある、異なっています。
しかし、その後、彼はすべての消費者のサブスクリプションがRocketMQがそうすることを許され、なぜああ、論理的にも、彼はその後、プロフェッショナリズム古いドライバを維持、同じ理解していないん、一見何の問題もテーマを所有していないと感じ、友人が困惑があります私たちは下RocketMQ消費者のサブスクリプション機構に得られるように、下に私は、ニュース、メッセージキューと負荷の再分配メカニズムを引っ張って、サブスクリプション登録の源の深さの観点からRocketMQ消費量を分析します。
消費者のサブスクリプション登録情報
消費者のすべてのブローカに起動時にサブスクリプション情報を登録して、ハートビートメカニズムを開始し、定期的に更新サブスクリプション情報は、各消費者がMQClientInstanceを持っていることになる消費者が開始したときに、このクラスがスタートします、スタートアップ手順は、一連の定期的なタスクを開始しますここで、
org.apache.rocketmq.client.impl.factory.MQClientInstance#startScheduledTask:
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
MQClientInstance.this.cleanOfflineBroker();
MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
} catch (Exception e) {
log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
}
}
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);
上記のクラスタハートビート定期的なタスクのサブスクリプション情報内のすべてのブローカに送信され、内部を続けソースは、クラスタ内の各ブローカは、その次のデータが含まれている各クライアントのための心拍データをあるHeartbeatData、HeartbeatDataを送信していることがわかります:
// 客户端ID
private String clientID;
// 生产者信息
private Set<ProducerData> producerDataSet = new HashSet<ProducerData>();
// 消费者信息
private Set<ConsumerData> consumerDataSet = new HashSet<ConsumerData>();
トピッククライアントサブスクリプションに関する情報が含まれている消費者情報。
私たちは、クライアントがHEART_BEATとしてHeartbeatDataを送信し、要求のタイプは、我々はブローカー直接処理ロジックHEART_BEAT要求タイプを見つけるHeartbeatDataブローカーのデータに対処する方法を見ていき:
org.apache.rocketmq.broker.processor.ClientManageProcessor#ハートビート:
public RemotingCommand heartBeat(ChannelHandlerContext ctx, RemotingCommand request) {
RemotingCommand response = RemotingCommand.createResponseCommand(null);
// 解码,获取 HeartbeatData
HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class);
ClientChannelInfo clientChannelInfo = new ClientChannelInfo(
ctx.channel(),
heartbeatData.getClientID(),
request.getLanguage(),
request.getVersion()
);
// 循环注册消费者订阅信息
for (ConsumerData data : heartbeatData.getConsumerDataSet()) {
// 按消费组获取订阅配置信息
SubscriptionGroupConfig subscriptionGroupConfig =
this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(
data.getGroupName());
boolean isNotifyConsumerIdsChangedEnable = true;
if (null != subscriptionGroupConfig) {
isNotifyConsumerIdsChangedEnable = subscriptionGroupConfig.isNotifyConsumerIdsChangedEnable();
int topicSysFlag = 0;
if (data.isUnitMode()) {
topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
}
String newTopic = MixAll.getRetryTopic(data.getGroupName());
this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
newTopic,
subscriptionGroupConfig.getRetryQueueNums(),
PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag);
}
// 注册消费者订阅信息
boolean changed = this.brokerController.getConsumerManager().registerConsumer(
data.getGroupName(),
clientChannelInfo,
data.getConsumeType(),
data.getMessageModel(),
data.getConsumeFromWhere(),
data.getSubscriptionDataSet(),
isNotifyConsumerIdsChangedEnable
);
// ...
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
ここでは、要求を受信した後、ブローカーHEART_BEATを見ることができ、要求は、データ取得HeartbeatDataたHeartbeatData消費サイクルレジスタによると、サブスクリプション情報を抽出します。
org.apache.rocketmq.broker.client.ConsumerManager#registerConsumer:
public boolean registerConsumer(final String group, final ClientChannelInfo clientChannelInfo,
ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere,
final Set<SubscriptionData> subList, boolean isNotifyConsumerIdsChangedEnable) {
// 获取消费组内的消费者信息
ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group);
// 如果消费组的消费者信息为空,则新建一个
if (null == consumerGroupInfo) {
ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere);
ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp);
consumerGroupInfo = prev != null ? prev : tmp;
}
boolean r1 =
consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel,
consumeFromWhere);
// 更新订阅信息,订阅信息是按照消费组存放的,因此这步骤就会导致同一个消费组内的各个消费者客户端的订阅信息相互被覆盖
boolean r2 = consumerGroupInfo.updateSubscription(subList);
if (r1 || r2) {
if (isNotifyConsumerIdsChangedEnable) {
this.consumerIdsChangeListener.handle(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel());
}
}
this.consumerIdsChangeListener.handle(ConsumerGroupEvent.REGISTER, group, subList);
return r1 || r2;
}
消費者情報コンシューマ群ConsumerGroupInfoが空である場合、この手順は、新しいサブスクリプション情報は、消費者グループに応じて格納されている、名前から知ることができ、ブローカ更新サブスクリプションコア消費者情報の方法で、したがって場合、更新サブスクリプションの情報、サブスクリプション情報は、コンシューマ・グループに基づいて保存され、このステップは、相互に消費が覆われているのと同じセット内の各クライアントに対する消費者のサブスクリプション情報につながります。
メッセージをプル
MQClientInstanceは、起動時にタスクを引っ張ってメッセージを処理するスレッドを開始します。
org.apache.rocketmq.client.impl.factory.MQClientInstance#が起動します。
// Start pull service
this.pullMessageService.start();
pullMessageServiceはServiceThreadを継承し、次のことを達成するために、そのrunメソッドを、Runnableインタフェースを実装ServiceThread:
org.apache.rocketmq.client.impl.consumer.PullMessageService番号の実行:
@Override
public void run() {
while (!this.isStopped()) {
try {
// 从 pullRequestQueue 中获取拉取消息请求对象
PullRequest pullRequest = this.pullRequestQueue.take();
// 执行消息拉取
this.pullMessage(pullRequest);
} catch (InterruptedException ignored) {
} catch (Exception e) {
log.error("Pull Message Service Run Method exception", e);
}
}
}
消費者側のプルメッセージを取得するためのPullRequestオブジェクト、pullRequestQueueがブロッキングキューで、pullRequestデータが空の場合、新しいpullRequest引っ張っタスクが来るまで、テイク()メソッドの実装がブロックされたままになり、ここでは、非常に重要なステップでありますあなたは何時に作成し、その後pullRequestQueueに入れて、pullRequestを考えるかもしれませんか?pullRequestそれはRebalanceImplで作成された、それは再び負荷分散メカニズムRocketMQメッセージキューです。
負荷再配分メッセージキュー
メッセージソース解析上から引っ張る、原因pullRequestQueueなしpullRequestオブジェクトpullMessageServiceが起動したときに、それがブロックされていたであろう、とMQClientInstanceスタートでは、メッセージキューと負荷の再分配タスクを処理するスレッドを開始します。
org.apache.rocketmq.client.impl.factory.MQClientInstance#が起動します。
// Start rebalance service
this.rebalanceService.start();
rebalanceServiceはまた次のように実行しています、ServiceThreadを継承しました。
@Override
public void run() {
while (!this.isStopped()) {
this.waitForRunning(waitInterval);
this.mqClientFactory.doRebalance();
}
}
内部に進みます。
org.apache.rocketmq.client.impl.consumer.RebalanceImpl#doRebalance:
public void doRebalance(final boolean isOrder) {
// 获取消费者所有订阅信息
Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
if (subTable != null) {
for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
final String topic = entry.getKey();
try {
// 消息队列负载与重新分布
this.rebalanceByTopic(topic, isOrder);
} catch (Throwable e) {
if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("rebalanceByTopic Exception", e);
}
}
}
}
this.truncateMessageQueueNotMyTopic();
}
これは、メッセージのサブスクリプションが、我々がダウンしたときに、消費者のサブスクリプション情報を格納するサブテーブルは、消費者が内部に充填されるテーマに従ってクライアント加入スレッド、メッセージキューと負荷の再分配を得るために、主に次のとおりです。
org.apache.rocketmq.client.impl.consumer.RebalanceImpl#rebalanceByTopic:
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
rebalanceByTopicアプローチは、コア消費者側の負荷分散を実装することで、我々はランダムにクラスタ内のブローカーから入手内のコンシューマ・グループに続いて、第1の情報はtopicSubscribeInfoTableから話題キューをサブスクライブ取得、再配布してMessage Queueクラスタモードをロードするためにここにいますすべてのブローカがクラスタそれからサブスクリプションクライアントの情報を取得することができますなぜここのノートのクライアントIDのリストトピックを購読ですか?前の分析では、クライアントはすべてのブローカーへのハートビートパケットの送信を開始したときに消費者がスレッドを開始すると述べました。
org.apache.rocketmq.client.impl.consumer.RebalanceImpl#rebalanceByTopic:
// 如果 主题订阅信息mqSet和主题订阅客户端不为空,就执行消息队列负载与重新分布
if (mqSet != null && cidAll != null) {
List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
mqAll.addAll(mqSet);
// 排序,确保每个消息队列只分配一个消费者
Collections.sort(mqAll);
Collections.sort(cidAll);
// 消息队列分配算法
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
// 执行算法,并得到队列重新分配后的结果对象allocateResult
List<MessageQueue> allocateResult = null;
try {
allocateResult = strategy.allocate(
this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
} catch (Throwable e) {
log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
e);
return;
}
// ...
}
これらは、コアロジックをバランスメッセージ負荷され、RocketMQ自体が割り当てアルゴリズムは、次の5つの負荷アルゴリズム、デフォルトAllocateMessageQueueAveragely平均分布を用いたアルゴリズム、機能を提供します。
消費者グループG1、消費者C1およびC2が存在すると仮定し、C1は、TOPICAを加入C2は、8つのTOPICAメッセージキュー、broker_a(Q0 / Q1 / Q2 / Q3)クラスタとbroker_bがあると仮定し、そこbroker1とbroker2、topicBを購読しました以下のように(Q0 / Q1 / Q2 / Q3)、以前の私たちは、findConsumerIdList方法は、消費者のクライアントIDですべての消費者のセットを取得します知っている、アルゴリズムによって均等に分散TOPICA後の消費量は次のとおりです。
C1:broker_a(Q0 / Q1 / Q2 / Q3)
C2:broker_b(Q0 / Q1 / Q2 / Q3)
問題は、ここで発生していないサブスクリプションTOPICA C2、この場合につながるれ、割り当てられたする必要が割り当てアルゴリズム、プラスC2によると、メッセージの半分はC2を消費するために割り当てられ、c2がメッセージキューに割り当てられています10秒またはそれより長い遅延はtopicBの共感、消費されます。
私はフィギュアと消費のリバランス後TOPICAとtopicBを表現してみましょう:
なぜ消費者のサブスクリプションが存在しないために報告されますように、我々は、ラインを下に継続して:
org.apache.rocketmq.client.impl.consumer.RebalanceImpl#rebalanceByTopic:
if (mqSet != null && cidAll != null) {
// ...
Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
if (allocateResult != null) {
allocateResultSet.addAll(allocateResult);
}
// 用户重新分配后的结果allocateResult来更新当前消费者负载的消息队列缓存表processQueueTable,并生成 pullRequestList 放入 pullRequestQueue 阻塞队列中
boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
if (changed) {
log.info(
"rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
allocateResultSet.size(), allocateResultSet);
this.messageQueueChanged(topic, mqSet, allocateResultSet);
}
}
主MQSETコード論理を取り、メッセージキューcidAll負荷の再分配のために、allocateResult結果は、メッセージキューのリストであり、得られたコンシューマ負荷allocateResult更新キャッシュメッセージキューテーブルprocessQueueTable続いて、ブロッキングキューにpullRequestList pullRequestQueueを生成する上記:
org.apache.rocketmq.client.impl.consumer.RebalanceImpl#updateProcessQueueTableInRebalance:
List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
// 循环执行,将mqSet订阅数据封装成PullRequest对象,并添加到pullRequestList中
for (MessageQueue mq : mqSet) {
// 如果缓存列表不存在该订阅信息,说明这次消息队列重新分配后新增加的消息队列
if (!this.processQueueTable.containsKey(mq)) {
if (isOrder && !this.lock(mq)) {
log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
continue;
}
this.removeDirtyOffset(mq);
ProcessQueue pq = new ProcessQueue();
long nextOffset = this.computePullFromWhere(mq);
if (nextOffset >= 0) {
ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
if (pre != null) {
log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
} else {
log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
PullRequest pullRequest = new PullRequest();
pullRequest.setConsumerGroup(consumerGroup);
pullRequest.setNextOffset(nextOffset);
pullRequest.setMessageQueue(mq);
pullRequest.setProcessQueue(pq);
pullRequestList.add(pullRequest);
changed = true;
}
} else {
log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
}
}
}
// 将pullRequestList添加到PullMessageService中的pullRequestQueue阻塞队列中,以唤醒PullMessageService线程执行消息拉取
this.dispatchPullRequest(pullRequestList);
以前の私たちは、メッセージがpullRequestが実行キュープルを遮断取るpullRequestQueueから引かれるの話、上記の方法は、場所pullRequestを作成することです。
新聞、消費者のサブスクリプションは、この間違いを存在しない理由はここにソースの分析では、あなたが把握することができます:
消費者グループG1があると仮定し、G1消費者は、消費者のC1とC2の下で、c1はTOPICAを購読、c2が、この時点でtopicB、C2最初のスタートを購読、G1サブスクリプション情報はtopicBに更新されてきた、そして、C1を開始、のG1サブスクリプション情報についてTOPICAカバレッジは、負荷のリバランスのc1はむしろちょうどこの時間C2のハートビートパケットのG1サブスクリプション情報を回しpullRequestQueueにpullRequestのTOPICAを追加topicBに更新され、その後、時間PullMessageServiceスレッドc1はpullRequestのTOPICAにpullRequestQueueを取得しますメッセージプルが、(この時点では、ハートビートC2をカバーするために契約することを起こるので)消費者グループG1の下にサブスクリプション情報TOPICAを見つけることができませんでした、エラーメッセージが消費者のサブスクリプションがブローカーの終わりに存在しないと報告されます。