pulsar topic级别消费限速

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派发器推送消息到客户端

相关推荐
无名之逆8 分钟前
lombok-macros
开发语言·windows·后端·算法·面试·rust·大学期末
m0_7482478032 分钟前
SpringBoot集成Flowable
java·spring boot·后端
散一世繁华,颠半世琉璃42 分钟前
SpringBoot揭秘:URL与HTTP方法如何定位到Controller
spring boot·后端·http
安晴晚风2 小时前
从0开始在linux服务器上部署SpringBoot和Vue
linux·运维·前端·数据库·后端·运维开发
海绵波波1078 小时前
flask后端开发(10):问答平台项目结构搭建
后端·python·flask
网络风云9 小时前
【魅力golang】之-反射
开发语言·后端·golang
Q_19284999069 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
运维&陈同学10 小时前
【Kibana01】企业级日志分析系统ELK之Kibana的安装与介绍
运维·后端·elk·elasticsearch·云原生·自动化·kibana·日志收集
Javatutouhouduan13 小时前
如何系统全面地自学Java语言?
java·后端·程序员·编程·架构师·自学·java八股文
后端转全栈_小伵13 小时前
MySQL外键类型与应用场景总结:优缺点一目了然
数据库·后端·sql·mysql·学习方法