RocketMq深入分析講解兩種削峰方式
何時需要削峰
當上游調用下游服務速率高于下游服務接口QPS時,那么如果不對調用速率進行控制,那么會發(fā)生很多失敗請求
通過消息隊列的削峰方法有兩種
控制消費者消費速率和生產者投放延時消息,本質都是控制消費速度
通過消費者參數(shù)控制消費速度
先分析那些參數(shù)對控制消費速度有作用
1.PullInterval: 設置消費端,拉取mq消息的間隔時間。
注意:該時間算起時間是rocketMq消費者從broker消息后算起。經過PullInterval再次向broker拉去消息
源碼分析:
首先需要了解rocketMq的消息拉去過程
拉去消息的類
PullMessageService
public class PullMessageService extends ServiceThread { private final InternalLogger log = ClientLogger.getLog(); private final LinkedBlockingQueue<PullRequest> pullRequestQueue = new LinkedBlockingQueue<PullRequest>(); private final MQClientInstance mQClientFactory; private final ScheduledExecutorService scheduledExecutorService = Executors .newSingleThreadScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, "PullMessageServiceScheduledThread"); } }); public PullMessageService(MQClientInstance mQClientFactory) { this.mQClientFactory = mQClientFactory; } public void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) { if (!isStopped()) { this.scheduledExecutorService.schedule(new Runnable() { @Override public void run() { PullMessageService.this.executePullRequestImmediately(pullRequest); } }, timeDelay, TimeUnit.MILLISECONDS); } else { log.warn("PullMessageServiceScheduledThread has shutdown"); } } public void executePullRequestImmediately(final PullRequest pullRequest) { try { this.pullRequestQueue.put(pullRequest); } catch (InterruptedException e) { log.error("executePullRequestImmediately pullRequestQueue.put", e); } } public void executeTaskLater(final Runnable r, final long timeDelay) { if (!isStopped()) { this.scheduledExecutorService.schedule(r, timeDelay, TimeUnit.MILLISECONDS); } else { log.warn("PullMessageServiceScheduledThread has shutdown"); } } public ScheduledExecutorService getScheduledExecutorService() { return scheduledExecutorService; } private void pullMessage(final PullRequest pullRequest) { final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup()); if (consumer != null) { DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer; impl.pullMessage(pullRequest); } else { log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest); } } @Override public void run() { log.info(this.getServiceName() + " service started"); while (!this.isStopped()) { try { 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"); } @Override public void shutdown(boolean interrupt) { super.shutdown(interrupt); ThreadUtils.shutdownGracefully(this.scheduledExecutorService, 1000, TimeUnit.MILLISECONDS); } @Override public String getServiceName() { return PullMessageService.class.getSimpleName(); } }
繼承自ServiceThread,這是一個單線程執(zhí)行的service,不斷獲取阻塞隊列中的pullRequest,進行消息拉取。
executePullRequestLater會延時將pullrequest放入到pullRequestQueue,達到延時拉去的目的。
那么PullInterval參數(shù)就是根據(jù)這個功能發(fā)揮的作用,在消費者拉去消息成功的回調
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: 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()); boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); 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); } } 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: pullRequest.setNextOffset(pullResult.getNextBeginOffset()); DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest); DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); break; case NO_MATCHED_MSG: pullRequest.setNextOffset(pullResult.getNextBeginOffset()); DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest); DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); break; case OFFSET_ILLEGAL: log.warn("the pull request offset illegal, {} {}", pullRequest.toString(), pullResult.toString()); pullRequest.setNextOffset(pullResult.getNextBeginOffset()); pullRequest.getProcessQueue().setDropped(true); DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() { @Override public void run() { try { DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(), pullRequest.getNextOffset(), false); DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue()); 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, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION); } };
在 case found的情況下,也就是拉取到消息的q情況,在PullInterval>0的情況下,會延時投遞到pullRequestQueue中,實現(xiàn)拉取消息的間隔
if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) { DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval()); } else { DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); }
2.PullBatchSize: 設置每次pull消息的數(shù)量,該參數(shù)設置是針對邏輯消息隊列,并不是每次pull消息拉到的總消息數(shù)
消費端分配了兩個消費隊列來監(jiān)聽。那么PullBatchSize 設置為32,那么該消費端每次pull到 64個消息。
消費端每次pull到消息總數(shù)=PullBatchSize*監(jiān)聽隊列數(shù)
源碼分析
消費者拉取消息時
org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage中
會執(zhí)行
this.pullAPIWrapper.pullKernelImpl( pullRequest.getMessageQueue(), subExpression, subscriptionData.getExpressionType(), subscriptionData.getSubVersion(), pullRequest.getNextOffset(), this.defaultMQPushConsumer.getPullBatchSize(), sysFlag, commitOffsetValue, BROKER_SUSPEND_MAX_TIME_MILLIS, CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND, CommunicationMode.ASYNC, pullCallback );
其中 this.defaultMQPushConsumer.getPullBatchSize(),就是配置的PullBatchSize,代表的是每次從broker的一個隊列上拉取的最大消息數(shù)。
3.ThreadMin和ThreadMax: 消費端消費pull到的消息需要的線程數(shù)量。
源碼分析:
還是在消費者拉取消息成功時
boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( pullResult.getMsgFoundList(), processQueue, pullRequest.getMessageQueue(), dispatchToConsume);
通過consumeMessageService執(zhí)行
默認情況下是并發(fā)消費
org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService#submitConsumeRequest
@Override public void submitConsumeRequest( final List<MessageExt> msgs, final ProcessQueue processQueue, final MessageQueue messageQueue, final boolean dispatchToConsume) { final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize(); if (msgs.size() <= consumeBatchSize) { ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue); try { this.consumeExecutor.submit(consumeRequest); } catch (RejectedExecutionException e) { this.submitConsumeRequestLater(consumeRequest); } } else { for (int total = 0; total < msgs.size(); ) { List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize); for (int i = 0; i < consumeBatchSize; i++, total++) { if (total < msgs.size()) { msgThis.add(msgs.get(total)); } else { break; } } ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue); try { this.consumeExecutor.submit(consumeRequest); } catch (RejectedExecutionException e) { for (; total < msgs.size(); total++) { msgThis.add(msgs.get(total)); } this.submitConsumeRequestLater(consumeRequest); } } } }
其中consumeExecutor初始化
this.consumeExecutor = new ThreadPoolExecutor( this.defaultMQPushConsumer.getConsumeThreadMin(), this.defaultMQPushConsumer.getConsumeThreadMax(), 1000 * 60, TimeUnit.MILLISECONDS, this.consumeRequestQueue, new ThreadFactoryImpl("ConsumeMessageThread_"));
對象線程池最大和核心線程數(shù)。對于順序消費ConsumeMessageOrderlyService也會使用最大和最小線程數(shù)這兩個參數(shù),只是消費時會鎖定隊列。
以上三種情況:是針對參數(shù)配置,來調整消費速度。
除了這三種情況外還有兩種服務部署情況,可以調整消費速度:
4.rocketMq 邏輯消費隊列配置數(shù)量 有消費端每次pull到消息總數(shù)=PullBatchSize*監(jiān)聽隊列數(shù)
可知rocketMq 邏輯消費隊列配置數(shù)量即上圖中的 queue1 ,queue2,配置數(shù)量越多每次pull到的消息總數(shù)也就越多。如果下邊配置讀隊列數(shù)量:修改tocpic的邏輯隊列數(shù)量
5.消費端節(jié)點部署數(shù)量 :
部署數(shù)量無論一個節(jié)點監(jiān)聽所有隊列,還是多個節(jié)點按照分配策略分配監(jiān)聽隊列數(shù)量,理論上每秒pull到的數(shù)量都一樣的,但是多節(jié)點消費端消費線程數(shù)量要比單節(jié)點消費線程數(shù)量多,也就是多節(jié)點消費速度大于單節(jié)點。
消費延時控流
針對消息訂閱者的消費延時流控的基本原理是,每次消費時在客戶端增加一個延時來控制消費速度,此時理論上消費并發(fā)最快速度為:
單節(jié)點部署:
ConsumInterval :延時時間單位毫秒
ConcurrentThreadNumber:消費端線程數(shù)量
MaxRate :理論每秒處理數(shù)量
MaxRate = 1 / ConsumInterval * ConcurrentThreadNumber
如果消息并發(fā)消費線程(ConcurrentThreadNumber)為 20,延時(ConsumInterval)為 100 ms,代入上述公式可得
如果消息并發(fā)消費線程(ConcurrentThreadNumber)為 20,延時(ConsumInterval)為 100 ms,代入上述公式可得
200 = 1 / 0.1 * 20
由上可知,理論上可以將并發(fā)消費控制在 200 以下
如果是多個節(jié)點部署如兩個節(jié)點,理論消費速度最高為每秒處理400個消息。
如下延時流控代碼:
/** * 測試mq 并發(fā) 接受 */ @Component @RocketMQMessageListener(topic = ConstantTopic.WRITING_LIKE_TOPIC,selectorExpression = ConstantTopic.WRITING_LIKE_ADD_TAG, consumerGroup = "writing_like_topic_add_group") class ConsumerLikeSave implements RocketMQListener<LikeWritingParams>, RocketMQPushConsumerLifecycleListener{ @SneakyThrows @Override public void onMessage(LikeWritingParams params) { System.out.println("睡上0.1秒"); Thread.sleep(100); long begin = System.currentTimeMillis(); System.out.println("mq消費速度"+Thread.currentThread().getName()+" "+DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").format(LocalDateTime.now())); //writingLikeService.saveLike2Db(params.getUserId(),params.getWritingId()); long end = System.currentTimeMillis(); // System.out.println("消費:: " +Thread.currentThread().getName()+ "毫秒:"+(end - begin)); } @Override public void prepareStart(DefaultMQPushConsumer defaultMQPushConsumer) { defaultMQPushConsumer.setConsumeThreadMin(20); //消費端拉去到消息以后分配線索去消費 defaultMQPushConsumer.setConsumeThreadMax(50);//最大消費線程,一般情況下,默認隊列沒有塞滿,是不會啟用新的線程的 defaultMQPushConsumer.setPullInterval(0);//消費端多久一次去rocketMq 拉去消息 defaultMQPushConsumer.setPullBatchSize(32); //消費端每個隊列一次拉去多少個消息,若該消費端分賠了N個監(jiān)控隊列,那么消費端每次去rocketMq拉去消息說為N*1 defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP); defaultMQPushConsumer.setConsumeTimestamp(UtilAll.timeMillisToHumanString3(System.currentTimeMillis())); defaultMQPushConsumer.setConsumeMessageBatchMaxSize(2); } }
注釋:如上消費端,單節(jié)點每秒處理速度也就是最高200個消息,實際上要小于200,業(yè)務代碼執(zhí)行也是需要時間。
但是要注意實際操作中并發(fā)流控實際是默認存在的,
spring boot 消費端默認配置
this.consumeThreadMin = 20;
this.consumeThreadMax = 20;
this.pullInterval = 0L;
this.pullBatchSize = 32;
若業(yè)務邏輯執(zhí)行需要20ms,那么單節(jié)點處理速度就是:1/0.02*20=1000
這里默認拉去的速度1s內遠大于1000
注意: 這里雖然pullInterval 等于0 當時受限于每次拉去64個,處理完也是需要一端時間才能回復ack,才能再次拉取,所以消費速度應該小于1000
所以并發(fā)流控要消費速度大于消費延時流控 ,那么消費延時流控才有意義
使用rokcetMq支持的延時消息也可以實現(xiàn)消息的延時消費,通過對delayLevel對應的時間進行配置為我們的需求。為不同的消息設置不同delayLevel,達到延時消費的目的。
總結
rocketMq 肖鋒流控兩種方式:
并發(fā)流控:就是根據(jù)業(yè)務流控速率要求,來調整topic 消費隊列數(shù)量(read queue),消費端部署節(jié)點,消費端拉去間隔時間,消費端消費線程數(shù)量等,來達到要求的速率內
延時消費流控:就是在消費端延時消費消息(sleep),具體延時多少要根據(jù)業(yè)務要求速率,和消費端線程數(shù)量,和節(jié)點部署數(shù)量來控制
到此這篇關于RocketMq深入分析講解兩種削峰方式的文章就介紹到這了,更多相關RocketMq削峰內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳解spring security 配置多個AuthenticationProvider
這篇文章主要介紹了詳解spring security 配置多個AuthenticationProvider ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05