背景
メッセージの保守消費スケジュール、メッセージのフィルタリング、負荷分散、メッセージハンドリング、ポストバック確認:Cosumerニュース消費プロセスがより複雑で、より重要なのは、以下のモジュールです。スペース制限が原因、この記事では、消費者の消費者が進行され得るし、維持する方法について説明します。これらのステップは密接に関連しているのので、相互浸透状況が発生する可能性があります。
消費者の進捗ファイル
前回の記事のConsumerQueueを構築する方法をRocketMQ?話では、サービススレッドの非同期からすでにCommitLogメッセージを取得し、キー情報メッセージの位置、大きさ、tagsCodeとそうで右ConsumerQueueを選択するために保存されてによって書かれました。ConsumerQueueは、トピックに関する情報CommitLog内の位置に次のメッセージの保存、インデックスファイルです。私たちは、メッセージを消費このConsumerQueue位置にメッセージを読み始め、その後、CommitLogは時間のための練習スペースの典型的なニュースを、フェッチ行きます。我々はまた、進行状況を保存するために、消費者を提出する必要があるので、同時に、我々は、我々が読んでどこ記録する必要がある、とあなたが前方に中断したところから読み続けるために次回をお読みください。次のようRocketMQに本書でConsumserOffset.jsonストア/ configディレクトリであり、
このようなconsumerOffset.jsonの内容を:
前記test_url @ sub_localtest主キー、consumerGroup @ルールトピックで、各コンテンツはConsumerQueueであります消費者の進捗状況。0番キュー番号2599,1キューは次消費するべき次の例は、次の消費べき号2602,6キュー102を消費しなければなりません。このスケジュールはConsumerQueueに対応し、1が累積20バイトに固定されます。
次のように今実験、最初のプロデューサを介してメッセージを送信する(コードは非常に単純であり、掲載されていない)、送信結果です。
SendResult [sendStatus=SEND_OK, msgId=C0A84D05091058644D4664E1427C0000, offsetMsgId=C0A84D0000002A9F00000000001ED9BA, messageQueue=MessageQueue [topic=test_url, brokerName=broker-a, queueId=6], queueOffset=102]
その結果、ブローカーMesssageQueue-6の102位置に格納されたメッセージから見ることができます。その後、我々は、このメッセージの消費者、消費を開始します。
2020-01-20 14:08:04.230 INFO [MessageThread_3] c.c.r.example.simplest.Consumer - 收到消息:topic=test_url,msgId=C0A84D05091058644D4664E1427C0000
メッセージが正常に消費された後、我々はConsumerOffset.jsonファイルの内容を見ることができる:
参照してください?最後のショットは、6号キュー消費スケジュールは、メッセージを正常に消費された、この103になって、102です。
今、私たちを開始するには、下部に着く:消費者におけるときの消費メッセージを、この進歩の消費量はどのように入手し、それを維持するのですか?
支出を開始するにはどこに?
呼び出すことにより、消費者
consumer.start();
次のように開始時間、消費がロードされたスケジュールは、次のとおりです。
//DefaultMQPushConsumerImpl.start()方法
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore(); // 如果本地有直接加载
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup()); // 集群模式下生成一个RemoteBrokerOffsetSotre,消费进度就保存在broker端
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
this.offsetStore.load(); // 加载本地进度
以下に示すように、インタフェースであるOffsetStore上記目的:
public interface OffsetStore {
void load() throws MQClientException;
void updateOffset(final MessageQueue mq, final long offset, final boolean increaseOnly);
long readOffset(final MessageQueue mq, final ReadOffsetType type);
void persistAll(final Set<MessageQueue> mqs);
void persist(final MessageQueue mq);
void removeOffset(MessageQueue mq);
Map<MessageQueue, Long> cloneOffsetTable(String topic);
void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway) throws RemotingException,
MQBrokerException, InterruptedException, MQClientException;
}
ような進歩の動作消費OffsetStore方法であって、負荷消費スケジュール、リード消費者の進歩は、消費の進行状況を更新するように。クラスタ消費パターンにおいて、消費が進行しないと消費者側の持続が、遠隔ブローカー端に格納されている、上記使用たとえばRemoteBrokerOffsetStoreカテゴリです。
public class RemoteBrokerOffsetStore implements OffsetStore {
private final static InternalLogger log = ClientLogger.getLog();
private final MQClientInstance mQClientFactory; // 客户端实例
private final String groupName; // 集群名称
private ConcurrentMap<MessageQueue, AtomicLong> offsetTable =
new ConcurrentHashMap<MessageQueue, AtomicLong>(); // 每个Queue的消费进度
}
スケジュールをロードするために地方消費Loadメソッドのためのブローカー端末での進捗状況を節約消費は何もしない、でRemoteBrokerOffsetStoreで空であるため、実際に読ん消費の進行状況がreadOffset方法によって達成されます。
これまで私たちが知っている、消費の進行がconsumerOffset.jsonファイルが最後にブローカーが存在するで、支出を開始する場所を知っているreadOffset法により、このファイルを読み取ります。
ロードバランシング簡単
消費者の進捗状況を読み取るreadOffset方法を理解する前に、それは単にこのメソッドを呼び出します際に知っておく必要があります。
消費者は、このモジュールをロードバランシングを含む、メッセージを消費したときに我々はarticleモジュールの冒頭で述べました。:話題の複数コンシューマ・キュー、および同じグループには、以下のグループは、複数の消費者は、例えば、以下の平均分布、これらのキューは、以下の個人消費を持つグループに割り当てられた特定のポリシーに従ってくださいする必要があるので、このトピックにサブスクライブしていることが
地図上にTOPIC_Aに5つのキューが存在する二消費者は、平均的な分布によると、それはConsumer1消費の3つのキュー、Consumer2消費の2つのキューことで、そこに加入されています。これは、負荷分散です。
消費者は、いくつかのそれぞれの処理キュー(ProcessQueue、消費者がメッセージを使用する)を作成するために使用され、要求が生成され、プルメッセージ(PullRequest、要求メッセージいくつかのメッセージをメッセージキューを割り当て)、要求ブローカに送信された要求を取得するメッセージの真の終わりではなく、閉塞内部キューに格納され、それは専用サービススレッドプルメッセージと組み立てられた取得要求メッセージによって読み取られ、ブローカ側に送信(SO当然のことながら)非同期の利益を得ることですか。次のように、次の中間進捗消費を保持し、この要求PullRequestは:
キューPullRequestを使用して、後続の引っ張りconsumerGroupすべてのメッセージが繰り返され、ここでのみPullRequestを生成する、請求だけnextOffset等更新パラメータ。PullRequestクラスを以下に示します。
public class PullRequest {
private String consumerGroup; // 消费分组
private MessageQueue messageQueue; // 消费队列
private ProcessQueue processQueue; // 消息处理队列
private long nextOffset; // 消费进度
private boolean lockedFirst = false; // 是否锁住
}
算出した消費スケジュール
PullRequestを生成するとき、それは(computPullFromWhere)の消費を開始する場所を計算します。RocketMQ消費者のデフォルトの開始点をCONSUME_FROM_LAST_OFFSET(オフセット最後から支出開始)で、computPullFromWhereアプローチので、この場合ことを来ります:
case CONSUME_FROM_LAST_OFFSET: {
long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);// 读取偏移
if (lastOffset >= 0) { // 正常偏移
result = lastOffset;
}
// First start,no offset
else if (-1 == lastOffset) { // 初次启动,broker没有偏移,readOffset返回-1
if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
result = 0L; // 消息重试
} else {
try {
result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq);// 获取该消息队列消费的最大偏移
} catch (MQClientException e) {
result = -1;
}
}
} else {
result = -1; // 异常情况readOffset返回-2,这里再返-1
}
break;
}
ここreadOffset我々は、上記の方法で来ます!
readOffset
readOffse方法t的入参是当前分配到的messageQueue和 固定的ReadOffsetType.READ_FROM_STORE,意思就从远程Broker读取该MessageQueue的消费进度。因此走到的是READ_FROM_STORE这个分支,如下所示:
case READ_FROM_STORE: {
try {
long brokerOffset = this.fetchConsumeOffsetFromBroker(mq);// 从broker获取消费进度
AtomicLong offset = new AtomicLong(brokerOffset);
this.updateOffset(mq, offset.get(), false); // 更新消费进度,更新的是RemoteBrokerOffsetStore.offsetTable这个表
return brokerOffset;
}
// No offset in broker
catch (MQBrokerException e) {
return -1;
}
//Other exceptions
catch (Exception e) {
log.warn("fetchConsumeOffsetFromBroker exception, " + mq, e);
return -2;
}
}
fetchConsumerOffsetFromBroker就是往该MessageQueue所在的broker发送获取消费进度的请求了,底层通讯之前的文章已经讲过了,这里就不在赘述了。在Broker端,消费进度保存在ConsumerOffsetManager里面:
key是Topic@ConsumerGroup,value存的是queueId和offset的映射关系。查找消费进度的代码如下:
long offset =
this.brokerController.getConsumerOffsetManager().queryOffset(
requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId());
从key来看,topic的消费进度是按照ConsumerGroup来区分的,不同的ConsumerGroup下该MessageQueue的消费进度互不影响,这一点也很好理解。
消息获取与更新消费进度
到目前为止,我们知道了每个MessageQueue的消费进度存在对应的Broker端,在负载均衡服务对每个Topic做负载均衡的时候,创建了PullRequest,并读取了消费进度offset。然后将PullRequest放入了一个阻塞队列中(pullRequestQueue),供专门的拉取消息线程服务(PullMessageService)读取,然后发起真正的拉取消息请求。这里更新消费进度与获取消息密切相关,因此会涉及一些获取消息的内容。
PullMessageSevice是一个服务线程,专门用于拉取消息,其run方法如下所示:
@Override
public void run() {
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
PullRequest pullRequest = this.pullRequestQueue.take(); // 从阻塞队列中获取一个PullsRequest
this.pullMessage(pullRequest); // 拉取消息
} catch (InterruptedException ignored) {
} catch (Exception e) {
log.error("Pull Message Service Run Method exception", e);
}
}
log.info(this.getServiceName() + " service end");
}
首先从阻塞队列pullRequestQueue中取出PullRequest,然后就是开始调用pullMessage方法获取消息了。调用最终会走到DefaultMQPushConsumerImpl的pullMessage方法中来,代码很多并且如何拉取消息不是本次重点内容,我们这里只贴最后的发送请求部分,理解的其中的参数,也就明白了消息获取请求。
try {
this.pullAPIWrapper.pullKernelImpl(
pullRequest.getMessageQueue(), //消息队列
subExpression,// 订阅表达式,例如"TAG_A"
subscriptionData.getExpressionType(), // 表达式类型,例如"TAG"
subscriptionData.getSubVersion(), // 版本号
pullRequest.getNextOffset(), // 下个消息进度
this.defaultMQPushConsumer.getPullBatchSize(), // 一次性拉取多少条,默认32
sysFlag,// 一些标志位集合,暂时不关心
commitOffsetValue, //
BROKER_SUSPEND_MAX_TIME_MILLIS,// 长轮询时被hold住时间,默认15秒
CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,// 调用超时时间,默认30秒
CommunicationMode.ASYNC, // 异步通讯
pullCallback// 回调
);
} catch (Exception e) {
log.error("pullKernelImpl exception", e);
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
}
Consumer端更新消费进度
由于发送消息获取请求是异步操作,返回处理在pullCallback里面,因此我们可以大胆猜测,Consumer端消费进度的更新也肯定在这里面。确实,在pullCallback里面会将PullRequest的nextOffset更新:
broker端更新消费进度
由于消息处理不是本次重点内容,所以Broker端对获取消息的处理,我们不打算深入,仅仅需要知道Broker端获取消息后,会计算出一个nextBeginOffset,它就是下个消费进度,然后会返回到Consumer端去供Consumer端更新进度,如下所示:
nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
其次,获取完消息后,broker端也会更新消费进度,如下所示:
boolean storeOffsetEnable = brokerAllowSuspend; // brokerAllowSuspend默认是true,如果没有消息就会hold住请求
storeOffsetEnable = storeOffsetEnable && hasCommitOffsetFlag; // 拉取消息时如果允许提交消费进度,commitOffsetFlag就有
storeOffsetEnable = storeOffsetEnable
&& this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE;// 所以Broker master节点默认情况下这个是true
if (storeOffsetEnable) {
this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(channel),
requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset()); // 保存消费进度,写入offsetTable
}
消费进度持久化
Broker更新消费进度,仅仅是更新了offsetTable这个表,并没有涉及到ConsumerOffset.json这个文件。其实,在Broker初始化时,会启动一项定时任务,定期保存tableOffset到ConsumerOffset.json文件中,如下所示:
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.consumerOffsetManager.persist(); // 保存文件
} catch (Throwable e) {
log.error("schedule persist consumerOffset error.", e);
}
}
}, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);
flushConsumerOffsetIntervval默认是5s,也就是每隔5保存一次消费进度到文件中。保存的过程是先将原来的文件存到ConsumerOffset.json.bak文件中,然后将新的内容存入ConsumerOffset.json文件。
至此,ConsumerQueue的消费进度维护就算完成了。
小结
トピックRocketMQ各ConsumerGroup内の各メッセージは、各メッセージキュー消費スケジュールは、ブローカ端consumerOffset.jsonファイルの存在です。アクティブに消費者側は、PullRequest要求を作成し、その後、次の消費ブローカーの進捗状況を読んで、消費者側に戻す、ブローカは次の消費者の進捗状況を取得し、要求を送信します。消費者は、その後、メッセージを取得するために、エンドブローカーた後、それに応じて個別のサービスPullRequestスレッドとプルメッセージによって読み取ら消費の進捗状況を更新します。そこ定期的に別々のタイミングタスクには、消費電力を節約するファイルの進展もあり、そして元のファイルをバックアップします。