文章部分图片来自参考资料,侵删
アウトライン
レッツ・デザインのメッセージコンシューマ・キュー・プロセスは、その後、どのように消費者がより多くのメッセージキュー、それが少数よりも消費する必要がありますか?私たちは、トピックmessagequeブローカーのにメッセージを送信するために、前工程から知っていますか 2つの消費者支出のパターンがあります。ブロードキャストモードおよびトランキングモードは、ブロードキャストモードは、よく消費者がすべてのニュースを理解され、クラスタモードは、全体として、複数の消費者に、論理的に考えることと等価である、最も人気の理解は、クラスタ内のメッセージがあります消費者が支出しても唯一の完全な消費。そして、消費のクラスタモデルは、どのような戦略、それに従うべきか?それ以来、クラスタモードのみ、他の消費者を防ぐために、どのように消費者支出は、それを消費することができますか?、消費の2つの方法があります取得ブローカープッシュ自分はそれを超えている、または消費者自身がそれを引っ張って?これらの質問によると、私たちはどのようにrocketmqデザインを見て質問をお受けします。
メッセージキューのロードバランシング
コンシューマ・キュー負荷分散ソリューションは、消費者支出の問題は、そのようにバランスの取れた消費を達成するためにどこに行くことです。
クラスタモード
長いグループは、人間の消費のためにそこにあるとして、消費者が成功した場合でも、グループ、サブ様々な戦略
平均配分戦略
平均グループ割り当てメッセージ、ノード9、除算を使用して例えば3つのメッセージ、メッセージあたり3。参考資料からの写真は、侵略を削除しました。
一貫したハッシュ戦略
(分散ハッシュ一貫性)[https://www.cnblogs.com/Benjious/p/11899188.html]この記事一貫したハッシュを通じて学ぶことができます
- 割当てるポーリング計画はまた、上記の例では、ポーリング割り当て、同じ戦略、rocketmqが不均一ハッシュノード環、環上のキーを防止する上で同じ仮想ノードを使用して、キュー・ブローカにドロップすることを意味します。
ブロードキャストモード。
全消費、よく理解
消費パターンは、コードで実装します
時限式実行でバランスをとるサービスのバランスをとるメッセージキューの負荷が継続して実行、実行ロジックRebalanceImplは、達成しました
公共ボイドdoRebalance(最終ブールisOrder){ サブテーブル= this.getSubscriptionInner地図<、SubscriptionData文字列>(); IF(!サブテーブル= NULL){ ため(最終のMap.Entry <文字列、SubscriptionData>エントリ:subTable.entrySet()){ 最終的な文字列のトピック= entry.getKey()。 してみてください{ this.rebalanceByTopic(トピック、isOrder)。 }キャッチ(ThrowableをE){ IF(topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)!){ log.warn( "rebalanceByTopic例外"、E)。 } } } } this.truncateMessageQueueNotMyTopic(); } 公共のConcurrentMap <文字列、SubscriptionData> getSubscriptionInner(){ 戻りsubscriptionInner; } プライベート無効rebalanceByTopic(最終文字列トピック、決勝ブールisOrder){ スイッチ(messageModel){ //ブロードキャストは、ローカルクラスタに保存されていますブローカに記憶されている {:ケース放送 //放送、そこ見つけることがローカルでなければならない ... } ケースクラスタリング:{ 遠隔ブローカーに格納されている//クラスタモード消費スケジュール端 ... 戦略= this.allocateMessageQueueStrategyでAllocateMessageQueueStrategy。 一覧<メッセージキュー> allocateResult = NULL; 試みは{ //コールアロケーション戦略 allocateResult = strategy.allocate( this.consumerGroup、 this.mQClientFactory.getClientId()、 mqAll、 cidAll)。 }キャッチ(ThrowableをE){ log.error( "AllocateMessageQueueStrategy.allocate例外allocateMessageQueueStrategyName = {。}"、strategy.getName()、 E)。 リターン; } 設定<メッセージキュー> allocateResultSet =新しいHashSetの<メッセージキュー>(); (allocateResult = NULL!){もし allocateResultSet.addAll(allocateResult)。 } } } }
デフォルトでは、我々は問題を考えるAllocateMessageQueueAveragely配分戦略を使用することです、私たちは、消費者の数は時間のメッセージキューよりも大きい場合、次のクラスタモードのメッセージのみが消費者支出は、メッセージは、メッセージキューに置かれていることを知っていますそれを割り当てるためにどのように?メッセージキューは、複数の消費者を割り当てることができますか?今私はテスト、同じ割り当てロジックとAllocateMessageQueueAveragelyを書きました。
パブリック静的無効メイン(文字列[] args){ メインM =新メイン()。 m.test(); } パブリックリスト<メッセージキュー> OP(文字列currentCID、リスト<メッセージキュー> mqAll、 リストの<string> cidAll){ リスト<メッセージキュー>結果=新規のArrayList <>(); INTインデックス= cidAll.indexOf(currentCID)。 INT MOD = mqAll.size()%のcidAll.size()。 int型averageSize = mqAll.size()<= cidAll.size()?1:(MOD> 0 &&インデックス<MOD mqAll.size()/ cidAll.size()? + 1:mqAll.size()/ cidAll.size())。 int型のstartIndex =(MOD> 0 &&指数<MOD)?インデックス* averageSize: INT範囲= Math.min(averageSize、mqAll.size() -のstartIndex)。 私は0を= INT(のために、I <範囲。I ++){ result.add(mqAll.get((のstartIndex + I)%mqAll.size()))。 } 結果を返します。 } 公共ボイド試験(){ リストの<string> cidAll =新規のArrayList <>(); {(I ++; I <8 INTがI = 0)するための 文字列CID = "192.168.10.86 @" + iが、 cidAll.add(CID)。 } リスト<メッセージキュー> mqAll =新規のArrayList <>(); 以下のために(INT I = 0; I <3; I ++){ メッセージキューMQ =新しいメッセージキュー(I)。 mqAll.add(MQ)。 } :(文字列CID用 {cidAll) リスト<メッセージキュー> OP = OP(CID、mqAll、cidAll) のSystem.out.println( "当前CID: (メッセージキューMQ:OP)のために{ System.out.printlnは( ""行きます+ mq.no + "メッセージのメッセージを取得する番号"); } } }
現在のCID:192.168.10.86@0は メッセージ0メッセージを取得するには 、現在のCIDを:192.168.10.86@1は、 メッセージ番号のメッセージを取るために 、現在のCID:192.168.10.86@2を メッセージ第2のためのメッセージ取るために 現在のCID:192.168。 10.86@3 現在のCID:192.168.10.86@4 現在のCID:192.168.10.86@5 現在のCID:192.168.10.86@6 現在のCID:192.168.10.86@7
彼らは間違いなく飢えているので、消費者の余剰がある場合は、クラスタモードで見ることができます(ハハ、私の非標準名を無視します)メッセージキューよりも少ない割り当てます。
消費者の進捗状況
消費者は、消費者を識別するために、どのように、メッセージを消費し、あなたの進行状況を保存、ブロードキャストモードのために、あなたの進行状況を保存するための問題であること「それが私の消費されている」ブローカーの終了に保存されており、このクラスタモードれますこれは、クライアント上でローカルに保存されています。進行は、サブクラスがLocalFileOffsetStoreとRemoteBrokerOffsetStoreたそれぞれ対応する、ローカルストレージおよびリモートストレージを実現するストレージ・インターフェース、offsetStore主に
クラスタモード
以下は参照から来て、著者は非常に書きます
在消费者客户端,RebalanceService 服务会定时地 (默认 20 秒) 从 Broker 服务器获取当前客户端所需要消费的消息队列,并与当前消费者客户端的消费队列进行对比,看是否有变化。对于每个消费队列,会从 Broker 服务器查询这个队列当前的消费偏移量。然后根据这几个消费队列,创建对应的拉取请求 PullRequest 准备从 Broker 服务器拉取消息,如下图所示:
ブローカーサーバーからのメッセージをプルダウンすると、ユーザーが正常に消費は、ローカルオフセットテーブルを更新する場合のみです。サービスを介してローカルオフセットテーブル5秒ごとに、サーバーBrokerにタイミング同期:
パブリッククラスMQClientInstance { プライベート無効startScheduledTask(){ this.scheduledExecutorService.scheduleAtFixedRate(新しいRunnableを(){ @Override ます。public void実行(){ MQClientInstance.this.persistAllConsumerOffset(); } }、* 10 1000年、this.clientConfig.getPersistConsumerOffsetInterval( )、TimeUnit.MILLISECONDS)。 } }
ブローカーとサーバ側オフセットテーブルで維持もシーケンスでディスクに5秒ごとに更新されます。
public class BrokerController { public boolean initialize() throws CloneNotSupportedException { this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { BrokerController.this.consumerOffsetManager.persist(); } }, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS); } }
拉取消费
rocketmq 的 push 实际都是利用不断地去 pull 来达到 push 的效果。 push 实际是用 pull 实现的,开始的时候内存为空,生成 pullRequest 然后去 broker 请求数据,请求回来后再次生成 pullRequest再次去请求,去broker拉取消费进行的消费的服务 : PullMessageService ,它接受 PullRequest
public class PullRequest { private MessageQueue messageQueue; private ProcessQueue processQueue; } public class ProcessQueue { private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>(); }
PullRequest 关联MessageQueue 和 ProcessQueue ,ProcessQueue 是指某个MessageQueue的消费进度抽象
/** * Queue consumption snapshot * */ public class ProcessQueue { ... private final Logger log = ClientLogger.getLog(); //读写锁 private final ReadWriteLock lockTreeMap = new ReentrantReadWriteLock(); // TreeMap 是可以排序的 map(红黑树实现) private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>(); private final AtomicLong msgCount = new AtomicLong(); private final AtomicLong msgSize = new AtomicLong(); private final Lock lockConsume = new ReentrantLock(); /** * A subset of msgTreeMap, will only be used when orderly consume */ private final TreeMap<Long, MessageExt> consumingMsgOrderlyTreeMap = new TreeMap<Long, MessageExt>(); private final AtomicLong tryUnlockTimes = new AtomicLong(0); }
可以看到ProcessQueue维护两个消息树为了就是记录消费的进度,这在后面会介,我们也可以大概地猜测到 PullRequest 实际应该的含义是某个指定的 MessageQueue 进度发生了变化,就会生成一个 PullRequest 去远程拉取消费进行消费。 服务器在收到客户端的请求之后,会根据话题和队列 ID 定位到对应的消费队列。然后根据这条请求传入的 offset 消费队列偏移量,定位到对应的消费队列文件。偏移量指定的是消费队列文件的消费下限。
public class DefaultMessageStore implements MessageStore { public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, final int maxMsgNums, final MessageFilter messageFilter) { // ... ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId); if (consumeQueue != null) { // 首先根据消费队列的偏移量定位消费队列 SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset); if (bufferConsumeQueue != null) { try { status = GetMessageStatus.NO_MATCHED_MESSAGE; // 最大消息长度 final int maxFilterMessageCount = Math.max(16000, maxMsgNums * ConsumeQueue.CQ_STORE_UNIT_SIZE); // 取消息 for (; i < bufferConsumeQueue.getSize() && i < maxFilterMessageCount; i += ConsumeQueue.CQ_STORE_UNIT_SIZE) { long offsetPy = bufferConsumeQueue.getByteBuffer().getLong(); int sizePy = bufferConsumeQueue.getByteBuffer().getInt(); // 根据消息的偏移量和消息的大小从 CommitLog 文件中取出一条消息 SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy); getResult.addMessage(selectResult); status = GetMessageStatus.FOUND; } // 增加下次开始的偏移量 nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE); } finally { bufferConsumeQueue.release(); } } } // ... } }
客户端和 Broker 服务器端完整拉取消息的流程图如下所示:
消费消费
顺序消费和并发消费,顺序消费指的是消费同一个 messagequeue 里的消息,从而达到顺序消费的目的。
broker 中记录的信息
consumerFilter.json
消费者过滤相关
consumerOffset.json
消费者消费broker各个队列到了哪个位置
{ "offsetTable":{ "%RETRY%generalCallbackGroup@generalCallbackGroup":{0:0 }, "Jodie_topic_1023@CID_JODIE_1":{0:10,1:11,2:10,3:9 }, "PayTransactionTopic@mq_test_callback":{0:0,1:0,2:1,3:1 }, } }
可以看到是有json表示的是“topic + group ”中的四个队列的消费情况
delayOffset.json 、
延时相关
subscriptionGroup.json
订阅相关,group相关的配置, 消费者订阅了哪些topic
{ "dataVersion":{ "counter":1, "timestamp":1572054949837 }, "subscriptionGroupTable":{ "CID_ONSAPI_OWNER":{ "brokerId":0, "consumeBroadcastEnable":true, "consumeEnable":true, "consumeFromMinEnable":true, "groupName":"CID_ONSAPI_OWNER", "notifyConsumerIdsChangedEnable":true, "retryMaxTimes":16, "retryQueueNums":1, "whichBrokerWhenConsumeSlowly":1 }, "CID_ONSAPI_PERMISSION":{ "brokerId":0, "consumeBroadcastEnable":true, "consumeEnable":true, "consumeFromMinEnable":true, "groupName":"CID_ONSAPI_PERMISSION", "notifyConsumerIdsChangedEnable":true, "retryMaxTimes":16, "retryQueueNums":1, "whichBrokerWhenConsumeSlowly":1 }, ... } }
topics.json
topic相关配置,broker 中拥有那些topic
{ "dataVersion":{ "counter":5, "timestamp":1573745719274 }, "topicConfigTable":{ "TopicTest":{ "order":false, "perm":6, "readQueueNums":4, "topicFilterType":"SINGLE_TAG", "topicName":"TopicTest", "topicSysFlag":0, "writeQueueNums":4 }, "%RETRY%please_rename_unique_group_name_4":{ "order":false, "perm":6, "readQueueNums":1, "topicFilterType":"SINGLE_TAG", "topicName":"%RETRY%please_rename_unique_group_name_4", "topicSysFlag":0, "writeQueueNums":1 } ... } }
consumerQueue 图例
消费消息
消费消息有并发消费和顺序消费两种,主要的核心实现就是 ConsumeMessageConcurrentlyService 和 ConsumeMessageOrderlyService ,又它们继承的接口看的出来他们都是持有了一个线程池,并在线程池内进行消费。
public class DefaultMQPushConsumerImpl implements MQConsumerInner { public void pullMessage(final PullRequest pullRequest) { PullCallback pullCallback = new PullCallback() { @Override public void onSuccess(PullResult pullResult) { if (pullResult != null) { switch (pullResult.getPullStatus()) { case FOUND: // 消息放入处理队列的消息树中 boolean dispathToConsume = processQueue .putMessage(pullResult.getMsgFoundList()); // 提交一个消息消费请求 DefaultMQPushConsumerImpl.this .consumeMessageService .submitConsumeRequest( pullResult.getMsgFoundList(), processQueue, pullRequest.getMessageQueue(), dispathToConsume); break; } } } }; } }
下面的代码可以看到任务来自自己投到线程池执行。
public class ConsumeMessageConcurrentlyService implements ConsumeMessageService { class ConsumeRequest implements Runnable { @Override public void run() { // ... status = listener.consumeMessage(Collections.unmodifiableList(msgs), context); // ... } } }
消费完后
public class ConsumeMessageConcurrentlyService implements ConsumeMessageService { public void processConsumeResult(final ConsumeConcurrentlyStatus status, /** 其它参数 **/) { // 从消息树中删除消息 long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs()); //假如某个队列在这个时候刚好给移除了,不提交进度,这可能会存在重复消费的情况,所有客户端还是要自己做幂等处理 if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) { this.defaultMQPushConsumerImpl.getOffsetStore() .updateOffset(consumeRequest.getMessageQueue(), offset, true); } } }
而有序消费就有趣多了,我们先思考一下,顺序消费是在线程池执行了,那么如何保证有序呢,加锁。假如在执行的时候刚好进行rebalance,移除了该队列的消费,那么有序消费就不能进行了,什么意思呢?假设 Consumer-1 消费者客户端一开始需要消费 3 个消费队列,这个时候又加入了 Consumer-2 消费者客户端,并且分配到了 MessageQueue-2 消费队列。当 Consumer-1 内部的均衡服务检测到当前消费队列需要移除 MessageQueue-2 队列,
可以看到要是2号messagequeue 此时正在执行有序消费,然后却被另一个消费者进行消费,那么就不能保证有序消费了,于是在 broker 端应该也要有把锁,保证messagequeue在被有序消费时只有一个消费者持有,而上面的场景也一样,当消费者不再从messagequeue 消费的时候,也会向broker申请释放锁。
public abstract class RebalanceImpl { private boolean updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet, final boolean isOrder) { while (it.hasNext()) { // ... if (mq.getTopic().equals(topic)) { // 当前客户端不需要处理这个消息队列了 if (!mqSet.contains(mq)) { pq.setDropped(true); // 解锁 if (this.removeUnnecessaryMessageQueue(mq, pq)) { // ... } } // ... } } } }
class ConsumeRequest implements Runnable { private final ProcessQueue processQueue; private final MessageQueue messageQueue; public ConsumeRequest(ProcessQueue processQueue, MessageQueue messageQueue) { this.processQueue = processQueue; this.messageQueue = messageQueue; } public ProcessQueue getProcessQueue() { return processQueue; } public MessageQueue getMessageQueue() { return messageQueue; } @Override public void run() { if (this.processQueue.isDropped()) { log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue); return; } final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue); //获取锁,保证线程池内只有一个线程可以对该messageQueue 进行消费 synchronized (objLock) { //广播消费 ,或是processQueue.isLocked()已经锁住了,或是锁没过期 //那么 processQueue.isLocked() 什么时候返回true 呢?ConsumeMessageOrderlyService内有个定时任务,周期去broker 中锁住这个 messagequeue //上文已经讲了 processQueue 和 messagequeue 是一一对应的。 if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel()) || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) { ... try { //再次获取锁,这里的锁有什么用呢?我们通过查找processQueue上锁的地方,发现就是在 Rebalance重新分配消费队列的时候会上锁 //为了保证此刻不被其他消费者占用于是上锁 this.processQueue.getLockConsume().lock(); if (this.processQueue.isDropped()) { log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}", this.messageQueue); break; } //业务逻辑回调 status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context); } catch (Throwable e) { log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}", RemotingHelper.exceptionSimpleDesc(e), ConsumeMessageOrderlyService.this.consumerGroup, msgs, messageQueue); hasException = true; } finally { this.processQueue.getLockConsume().unlock(); } ..... } } } }
补充
service构建源码分析
Rocketmq 中创建一个service部分都继承了 ServiceThread,让我们开看一下源码
public abstract class ServiceThread implements Runnable { private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); private static final long JOIN_TIME = 90 * 1000; //保存了一个线程 protected final Thread thread; /** * 主要的阻塞方法是 await 方法,然后通过 countdown 来唤醒正在 await 的线程,每次多个线程调用进行 waitForRunning 的时候 * waitPoint 的 栅栏数量都会重置(阻塞,stop后被唤醒,又再次阻塞的情况)。 */ protected final CountDownLatch2 waitPoint = new CountDownLatch2(1); protected volatile AtomicBoolean hasNotified = new AtomicBoolean(false); protected volatile boolean stopped = false; //初始化的时候就创建一个线程 public ServiceThread() { this.thread = new Thread(this, this.getServiceName()); } public abstract String getServiceName(); public void start() { this.thread.start(); } public void shutdown() { this.shutdown(false); } public void shutdown(final boolean interrupt) { this.stopped = true; log.info("shutdown thread " + this.getServiceName() + " interrupt " + interrupt); if (hasNotified.compareAndSet(false, true)) { waitPoint.countDown(); // notify } try { if (interrupt) { this.thread.interrupt(); } long beginTime = System.currentTimeMillis(); if (!this.thread.isDaemon()) { this.thread.join(this.getJointime()); } long eclipseTime = System.currentTimeMillis() - beginTime; log.info("join thread " + this.getServiceName() + " eclipse time(ms) " + eclipseTime + " " + this.getJointime()); } catch (InterruptedException e) { log.error("Interrupted", e); } } public long getJointime() { return JOIN_TIME; } public void stop() { this.stop(false); } public void stop(final boolean interrupt) { this.stopped = true; log.info("stop thread " + this.getServiceName() + " interrupt " + interrupt); if (hasNotified.compareAndSet(false, true)) { waitPoint.countDown(); // notify } if (interrupt) { this.thread.interrupt(); } } public void makeStop() { this.stopped = true; log.info("makestop thread " + this.getServiceName()); } public void wakeup() { if (hasNotified.compareAndSet(false, true)) { waitPoint.countDown(); // notify } } /** * 当只有一个线程的时候直接案通过CAS 成功后执行,多个线程则需要等待一段时间间隔后执行 * * @param interval 等待的时间 */ protected void waitForRunning(long interval) { if (hasNotified.compareAndSet(true, false)) { this.onWaitEnd(); return; } //entry to wait waitPoint.reset(); try { waitPoint.await(interval, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { log.error("Interrupted", e); } finally { hasNotified.set(false); this.onWaitEnd(); } } 保護されたボイドonWaitEnd(){ } パブリックブールisStopped(){ 戻りが停止しました。 } }
彼は、サブクラスを実行するために実装スレッドの実行方法を作成し、それらたCountDownLatchと原子の原子クラスは、主に複数のスレッドの同時動作の場合に対処するために使用される構造を知ることができます。
参考資料
- http://silence.work/2019/03/03/RocketMQ%20%E6%B6%88%E8%B4%B9%E6%B6%88%E6%81%AF%E8%BF%87%E7% A8%8B%E5%88%86%E6%9E%90 /
- https://www.kunzhao.org/blog/2018/04/08/rocketmq-message-index-flow/
- https://cloud.tencent.com/developer/article/1554950