pulsar topic级别消费限流
在了解了pulsar topic级别生产限流中的关于pulsar中netty框架的运用,本文中pulsar topic级别消费限流很多关于netty的部分就可以跳过了,直接看有关消费限流的部分
pulsar 消费者订阅
在编写消费者消费pulsar消息时,需要创建comsumer对象,其中最后需要调用subscribe方法生成消费者对象返回给客户端,在subscribe方法中通过netty发送了类型为subscribe的请求
该subscribe的请求对应到服务端接收处理netty请求的handler为ServerCnx的handleSubscribe方法
handlerSubscribe方法中主要做的事情就是资源的准备工作,例如没有主题和订阅就创建主题和订阅
还有主题关联订阅,进入subscribe方法中,可知该方法主要做的事是激活主题,订阅关联消费者
进入addConsumerToSubscription方法查看,再进入查看addConsumer方法,在这个方法中,可以看到根据消费者的不同类型,会创建不同的Dispatcher派发器对象,派发器对象又关联了消费者,Dispatcher派发器是pulsar中负责与消费者对接并处理消息的服务端接口
在初始化Dispatcher派发器后会初始化DispatchRateLimiter派发限速器,这个DispatchRateLimiter就和我们的消费限流有紧密关联了
看到这里基本上就了解了在客户端consumer调用subscribe方法后,进行了资源的各种校验和准备工作,但不止于此,subscribe方法在发送了subscribe类型的命令后又发送了flow类型的命令,该命令的作用实际就是将消息推送到consumer进行消费的命令
进入该方法内部,内部更新了availablePermits,这个可以看成是允许客户端接收的消息数,数值是接收队列大小,默认1000
接下来该方法就执行了向broker发送flow类型命令的代码
服务端通过handleFlow方法接收并处理该请求
pulsar 消费者接收消息
在handleFlow方法中主要关注consumer调用的flowPermits方法
该方法暂存消息数令牌,并使用subscription对象调用consumerFlow方法处理
再用subscription对象持有的Dispatcher派发器对象调用consumerFlow方法处理
不断深入该方法,直到readMoreEntries方法
该方法中调用了calculateToRead方法,calculateToRead方法中就存在限流相关的逻辑
该方法计算并返回messagesToRead,bytesToRead两个值
java
int messagesToRead = Math.min(availablePermits, readBatchSize);
long bytesToRead = serviceConfig.getDispatcherMaxReadSizeBytes();
// if turn of precise dispatcher flow control, adjust the records to read
if (consumer.isPreciseDispatcherFlowControl()) {
int avgMessagesPerEntry = Math.max(1, consumer.getAvgMessagesPerEntry());
messagesToRead = Math.min((int) Math.ceil(availablePermits * 1.0 / avgMessagesPerEntry), readBatchSize);
}
// throttle only if: (1) cursor is not active (or flag for throttle-nonBacklogConsumer is enabled) bcz
// active-cursor reads message from cache rather from bookkeeper (2) if topic has reached message-rate
// threshold: then schedule the read after MESSAGE_RATE_BACKOFF_MS
if (serviceConfig.isDispatchThrottlingOnNonBacklogConsumerEnabled() || !cursor.isActive()) {
if (topic.getBrokerDispatchRateLimiter().isPresent()) {
DispatchRateLimiter brokerRateLimiter = topic.getBrokerDispatchRateLimiter().get();
if (reachDispatchRateLimit(brokerRateLimiter)) {
if (log.isDebugEnabled()) {
log.debug("[{}] message-read exceeded broker message-rate {}/{}, schedule after a {}", name,
brokerRateLimiter.getDispatchRateOnMsg(), brokerRateLimiter.getDispatchRateOnByte(),
MESSAGE_RATE_BACKOFF_MS);
}
return Pair.of(-1, -1L);
} else {
Pair<Integer, Long> calculateToRead =
updateMessagesToRead(brokerRateLimiter, messagesToRead, bytesToRead);
messagesToRead = calculateToRead.getLeft();
bytesToRead = calculateToRead.getRight();
}
}
if (topic.getDispatchRateLimiter().isPresent()) {
DispatchRateLimiter topicRateLimiter = topic.getDispatchRateLimiter().get();
if (reachDispatchRateLimit(topicRateLimiter)) {
if (log.isDebugEnabled()) {
log.debug("[{}] message-read exceeded topic message-rate {}/{}, schedule after a {}", name,
topicRateLimiter.getDispatchRateOnMsg(), topicRateLimiter.getDispatchRateOnByte(),
MESSAGE_RATE_BACKOFF_MS);
}
return Pair.of(-1, -1L);
} else {
Pair<Integer, Long> calculateToRead =
updateMessagesToRead(topicRateLimiter, messagesToRead, bytesToRead);
messagesToRead = calculateToRead.getLeft();
bytesToRead = calculateToRead.getRight();
}
}
if (dispatchRateLimiter.isPresent()) {
if (reachDispatchRateLimit(dispatchRateLimiter.get())) {
if (log.isDebugEnabled()) {
log.debug("[{}] message-read exceeded subscription message-rate {}/{}, schedule after a {}",
name, dispatchRateLimiter.get().getDispatchRateOnMsg(),
dispatchRateLimiter.get().getDispatchRateOnByte(),
MESSAGE_RATE_BACKOFF_MS);
}
return Pair.of(-1, -1L);
} else {
Pair<Integer, Long> calculateToRead =
updateMessagesToRead(dispatchRateLimiter.get(), messagesToRead, bytesToRead);
messagesToRead = calculateToRead.getLeft();
bytesToRead = calculateToRead.getRight();
}
}
}
// If messagesToRead is 0 or less, correct it to 1 to prevent IllegalArgumentException
messagesToRead = Math.max(messagesToRead, 1);
bytesToRead = Math.max(bytesToRead, 1);
return Pair.of(messagesToRead, bytesToRead);
代码中可以看到,messagesToRead初始值为availablePermits和readBatchSize最小值,availablePermits默认值为1000,readBatchSize默认值为100,取值自配置文件中的dispatcherMaxReadBatchSize值,所以在这里messagesToRead初始值为100,而bytesToRead初始值为配置文件中的dispatcherMaxReadSizeBytes值,默认5MB
因为未开启精确流控制,所以跳过,主要看主题的派发限速控制逻辑
reachDispatchRateLimit(topicRateLimiter)方法判断是否达到限速阈值,判断的基准是dispatchRateLimiterOnMessage和dispatchRateLimiterOnByte,
这两个值的初始值都为用户设置的限速阈值,在未达到限速阈值的情况下,执行计算messagesToRead和bytesToRead值
深入计算值的方法查看,会发现messagesToRead和bytesToRead值都被更新为了用户设定的限速阈值并返回这两个值
往下看可以看到havingPendingRead标志置为true,接着使用cursor对象调用asyncReadEntriesOrWait方法
asyncReadEntriesOrWait方法中读取消息entry的部分分为if else块,其中else块在第一次读取消息entry的时候触发,if块在后续读消息的时候触发,判断标志为hasMoreEntries方法返回值,该方法中可以看到确保写位置为ledger最新的位置
找到asyncReadEntries方法,继续向后看,直到找到internalReadFromLedger方法,继续进入该方法
该方法内部又调用了asyncReadEntry方法,进入该方法
其中可以看到entryCache.asyncReadEntry()
的调用代码,这就说明ledger读取消息是从entryCache缓存对象中取的,并不是直接从bookkeeper中读取的进入asyncReadEntry方法,接着进入asyncReadEntry0方法
该方法其中大概的逻辑便是从bookkeeper中读取的消息数据缓存到entryCache中,重点需要关注的是代码 callback.readEntriesComplete((List) entriesToReturn, ctx);
该回调函数最后会触发dispatchEntriesToConsumer方法
该方法中使用consumer对象调用sendMessages方法,sendMessages方法中有调用sendMessagesToConsumer方法
sendMessagesToConsumer方法中有调用newMessageAndIntercept方法,该方法向客户端发送了message类型的命令,触发了客户端ClientCnx类中的handleMessage方法
该方法使用consumer对象调用messageReceived方法进行处理消息
在该方法的最后一般情况下会触发receiveIndividualMessagesFromBatch方法,该方法主要作用是处理批量消息入队
在该方法中有需要重点关注的两个方法,一个是构造message对象的newSingleMessage方法,一个是回调方法executeNotifyCallback
java
try {
for (int i = 0; i < batchSize; ++i) {
final MessageImpl<T> message = newSingleMessage(i, batchSize, brokerEntryMetadata, msgMetadata,
singleMessageMetadata, uncompressedPayload, batchMessage, schema, true,
ackBitSet, acker, redeliveryCount, consumerEpoch);
if (message == null) {
skippedMessages++;
continue;
}
if (possibleToDeadLetter != null) {
possibleToDeadLetter.add(message);
// Skip the message which reaches the max redelivery count.
if (redeliveryCount > deadLetterPolicy.getMaxRedeliverCount()) {
skippedMessages++;
continue;
}
}
if (acknowledgmentsGroupingTracker.isDuplicate(message.getMessageId())) {
skippedMessages++;
continue;
}
executeNotifyCallback(message);
}
if (ackBitSet != null) {
ackBitSet.recycle();
}
} catch (IllegalStateException e) {
log.warn("[{}] [{}] unable to obtain message in batch", subscription, consumerName, e);
discardCorruptedMessage(messageId, cnx, ValidationError.BatchDeSerializeError);
}
newSingleMessage方法将服务端推送到客户端的消息转为message对象,内部细节就不继续往下看,进入另一个方法executeNotifyCallback中查看
executeNotifyCallback方法中一般情况下会触发enqueueMessageAndCheckBatchReceive方法,该方法就是将消息入队,该队列为incomingMessages
看到这里基本上可以截止了,在consumer调用receive方法接收消息的方法内深入查看直到internalReceive方法,该方法内就是表明当consumer调用receive方法接收消息时,从incomingMessages队列中取出消息消费
在基本了解了消息消费流程后,回到主题消费限流部分,即Dispatcher派发器对象的calculateToRead方法中来看,当开启了消息限流且达到了用户设定的阈值后发生了什么
还是topic级别判断是否限流的if else块处,查看reachDispatchRateLimit方法,该方法判断是否达到阈值
因为假设是达到阈值的情况,所以dispatchRateLimiter.hasMessageDispatchPermit方法的返回值应该为false,根据该条件可知hasMessageDispatchPermit方法内的getAvailablePermits方法返回值为不满足大于0的条件,因为在上文可知dispatchRateLimiterOnMessage和dispatchRateLimiterOnByte是用户设定的阈值,因此至少有一个是不为null值的
进一步进入方法内查看推算可知acquiredPermits值是动态变化的,且在达到阈值时,该值不满足小于permits值的情况,而permits值是用户设定的阈值
那么是什么机制在不断的刷新acquiredPermits值,达到动态判断是否阈值的效果?
就是上文提到的在发送完消息后,调用的dispatchEntriesToConsumer方法,上文提到该方法使用consumer对象调用了sendMessages方法后又调用了addListener方法创建监听器,如果成功将消息发送到客户端就执行判断限速的逻辑
java
if (future.isSuccess()) {
int permits = dispatchThrottlingOnBatchMessageEnabled ? entries.size()
: sendMessageInfo.getTotalMessages();
// acquire message-dispatch permits for already delivered messages
if (serviceConfig.isDispatchThrottlingOnNonBacklogConsumerEnabled() || !cursor.isActive()) {
if (topic.getBrokerDispatchRateLimiter().isPresent()) {
topic.getBrokerDispatchRateLimiter().get().tryDispatchPermit(permits,
sendMessageInfo.getTotalBytes());
}
if (topic.getDispatchRateLimiter().isPresent()) {
topic.getDispatchRateLimiter().get().tryDispatchPermit(permits,
sendMessageInfo.getTotalBytes());
}
dispatchRateLimiter.ifPresent(rateLimiter ->
rateLimiter.tryDispatchPermit(permits,
sendMessageInfo.getTotalBytes()));
}
// Schedule a new read batch operation only after the previous batch has been written to the socket.
topic.getBrokerService().getTopicOrderedExecutor().executeOrdered(topicName,
SafeRun.safeRun(() -> {
synchronized (PersistentDispatcherSingleActiveConsumer.this) {
Consumer newConsumer = getActiveConsumer();
readMoreEntries(newConsumer);
}
}));
}
因为是主题策略开启的限速,所以会调用主题的dispatcher派发器限速器的tryDispatchPermit方法,后面还开启多线程不断的读取消息数据到Dispatcher派发器中并推送给客户端
继续查看tryDispatchPermit方法,方法的两个参数为成功发送到客户端的消息数和字节数,该方法内dispatchRateLimiterOnMessage和dispatchRateLimiterOnByte会调用tryAcquire方法
查看tryAcquire方法,该方法中如果开启了限速,会更新增加acquiredPermits的值为发送的消息数或字节数,到这里就明白了acquiredPermits的值是如何动态更新的,回到上文的Dispatcher派发器判断节流阈值的部分,hasMessageDispatchPermit方法的返回值就会变成false了,接着会调用reScheduleRead方法
该方法如果没有定时任务存在则会开启一个定时任务,按设定的时间调用readMoreEntries方法,即读取消息到Dispatcher并发送给客户端,该设定的时间值为MESSAGE_RATE_BACKOFF_MS值,而MESSAGE_RATE_BACKOFF_MS值为1000ms即1s,并且返回值为true,calculateToRead返回值为-1,-1对,并跳过当前的readMoreEntries方法,此时就是由定时任务来决定Dispatcher派发消息的速度了,即消息消费速度
看到这里基本上就了解了pulsar消息消费限速的基本流程了,可以做一个总结:
客户端消费者订阅主题,服务端主题持有的Dispatcher派发器主动推动消息到消费者的阻塞队列中,客户端消费者从阻塞队列中取消息进行消费,假如开启了消费限速,在消费的消息数或者字节数达到用户设定的阈值后,会停止当前的Dispatcher消息推送,开启定时任务由定时任务每秒钟通过Dispatcher派发器推送消息到客户端