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

相关推荐
程序员爱钓鱼40 分钟前
Go语言实战案例-创建模型并自动迁移
后端·google·go
javachen__1 小时前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
uzong6 小时前
技术故障复盘模版
后端
GetcharZp7 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程7 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研7 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi7 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国9 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy9 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack9 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt