pulsar topic级别发布限流

338 阅读8分钟

pulsar topic级别发布限流

pulsar生产消费客户端和服务端的通信是通过netty框架实现的,正如其他消息中间件一样。要了解pulsar限速,就需要跟踪pulsar生产消费的通信逻辑,逐步找到对应的实现代码

netty

netty是一个基于java nio的客户,服务器端的网络编程框架,使用netty可以快速简单的开发出一个网络应用,它提供异步的,事件驱动的网络应用程序框架和工具。 netty的核心组件包括Bootstrap,Channel,ChannelHandler,ChannelhandlerContext,ChannelPipline,ByteBuf,Codec等,由于pulsar客户端服务端通信使用的netty框架,所以带着这些核心组件去pulsar里跟踪

netty服务端

启动netty服务端

pulsar broker启动入口之一在PulsarBrokerStarter类的main方法,该方法中又启动pulsarService服务

image.png

pulsarService服务启动,会创建brokerService对象,并启动

image-1.png

在启动brokerService服务的代码中就可以看到netty相关的核心组件了以及创建netty服务端的代码

image-2.png

pulsarChannelInitializer是channelInitializer的子类,初始化器,用于在channel的pipeline中添加具体的handler逻辑处理器 ServiceBootStrap是netty服务端的启动引导器,向其中添加子处理器用于处理来到服务端的channel请求,参数中实例化pulsarChannelInitializer初始化器,bind方法绑定监听地址端口,之后netty服务端算是启动了

构造netty服务端channel处理器

pulsarChannelInitializer的initChannel方法是会被netty自动触发的,initChannel方法中在channel的channelPipeline中创建channelHandler处理器调用链,这是一个双向链表,其中需要主要关注的是ServerCnx类,该类是ChannelInboundHandlerAdapter子类,用于生成处理进入netty服务端channel请求逻辑的处理器

image-3.png

当netty客户端请求发送到服务端channel时,会触发channelRead方法,该方法一般会进行重写,实现自己的逻辑,所以pulsar重写了该方法,解析传入的请求,而根据pulsar客户端发送的消息请求,在解析后会在该方法中找到具体的处理入口,即handleSend方法

image-4.png

image-5.png

主题消息发布限流逻辑

仔细阅读和查看handleSend方法,第一直觉发现两个地方的调用可能与主题发送消息限速有关,一个是调用本身的startSendOperation方法,和调用producer生产者对象的publishMessage方法

image-6.png

先查看startSendOperation方法,该方法内部直接就发现了消息发布限速相关的代码逻辑

image-7.png

查看这部分代码

if (preciseTopicPublishRateLimitingEnable) {
    boolean isPreciseTopicPublishRateExceeded =
            producer.getTopic().isTopicPublishRateExceeded(numMessages, msgSize);
    if (isPreciseTopicPublishRateExceeded) {
        producer.getTopic().disableCnxAutoRead();
        return;
    }
    isPublishRateExceeded = producer.getTopic().isBrokerPublishRateExceeded();
} else {
    if (producer.getTopic().isResourceGroupRateLimitingEnabled()) {
        final boolean resourceGroupPublishRateExceeded =
            producer.getTopic().isResourceGroupPublishRateExceeded(numMessages, msgSize);
        if (resourceGroupPublishRateExceeded) {
            producer.getTopic().disableCnxAutoRead();
            return;
        }
    }
    isPublishRateExceeded = producer.getTopic().isPublishRateExceeded();
}

preciseTopicPublishRateLimitingEnable取值为配置文件中的preciseTopicPublishRateLimiterEnable参数,默认为false,因此逻辑走到else块中,其中producer.getTopic().isResourceGroupRateLimitingEnabled()判断的是租户命名空间级别的限速开关,暂时放到一边,主要关注的则是

isPublishRateExceeded = producer.getTopic().isPublishRateExceeded();

该处代码判断进入主题的消息是否达到发布限速阈值

image-8.png

此时就会产生一个疑问,pulsar是如何判断进入主题消息达到阈值的呢?

主题会持有一个topicPublishRateLimiter主题发布限速器对象,在主题初始化或者更改主题策略时根据是否开启了主题限速策略而创建该对象,该对象判断是否达到阈值是通过维护publishRateExceeded属性,该属性值的更新是通过该对象的checkPublishRate方法

image-9.png

@Override
public void checkPublishRate() {
    if (this.publishThrottlingEnabled && !publishRateExceeded) {
        if (this.publishMaxByteRate > 0) {
            long currentPublishByteRate = this.currentPublishByteCount.sum();
            if (currentPublishByteRate > this.publishMaxByteRate) {
                publishRateExceeded = true;
                return;
            }
        }

        if (this.publishMaxMessageRate > 0) {
            long currentPublishMsgRate = this.currentPublishMsgCount.sum();
            if (currentPublishMsgRate > this.publishMaxMessageRate) {
                publishRateExceeded = true;
            }
        }
    }
}

查看该处源代码可知,开启主题发布限速的情况下,当发布的字节总数或者消息总数超过设定的阈值时,会更新publishRateExceeded属性为true,标志达到阈值了,publishMaxByteRate和publishMaxMessageRate是设定的主题策略的发布限速字节和消息数阈值,this.currentPublishByteCount或者this.currentPublishMsgCount的值是如果获取的这个在后面会提到,需要查看到上文中提到的publishMessage方法中

此时可能会有疑问,如果当publishRateExceeded标志达到阈值设为true后,currentPublishByteRate或currentPublishMsgRate一直增长,该标志位一直为true,不就一直处于限速状态么

this.currentPublishByteCount和this.currentPublishMsgCount是LongAdder类型的变量,该变量是java并发中的一个类,用于实现多线程下的加法操作,这个属性中的值会被重置因此不会一直不断的增长,PublishRateLimiter的resetPublishCount方法不仅重置这两个属性,还会重置publishRateExceeded标志位false

image-11.png

pulsar在初始化brokerService时会更新配置以及注册监听器

image-12.png

该方法中注册了主题发布限速心跳的监听器

image-13.png

该监听器执行setupTopicPublishRateLimiterMonitor方法,查看该方法源代码可知,该监听器创建了两个定时任务,一个为10ms执行一次checkTopicPublishThrottlingRate方法,一个为1s执行一次refreshTopicPublishRate方法

image-14.png

checkTopicPublishThrottlingRate方法做的事情就是执行前面提到的checkPublishRate方法

image-15.png

refreshTopicPublishRate方法做的事情就是执行前面提到的resetPublishCount,并且开启netty层对于数据的读取,这个后面再提

image-16.png

image-17.png

源码看到这里,基本可以做出阶段性总结,当开启主题发布限速时,如果发布到主题的消息字节数或条数达到设定阈值,主题通过主题限速器设置publishRateExceeded为false,对于是否达到阈值的判断是每10ms进行一次的,这个值是默认10ms,但是可以改变,配置文件中的topicPublisherThrottlingTickTimeMillis值即为该值

image-18.png

当1s过后,主题又通过主题限速器清空发布到主题的字节数和消息数计数器,并重置publishRateExceeded标志位false,此时消息发布限流流程重新开始,整体看起来就像是每1s限制发布到主题指定字节数或条数

此时会有疑问,publishRateExceeded这个标志有什么用,看起来好像设置为false后,就暂时没有消息字节数或条数进入到主题中了,怎么做到的?

回到ServerCnx类中startSendOperation方法对于主题限速的判断代码处,此时可知当达到限速阈值时isPublishRateExceeded为true,会进入到以下代码中

if (++pendingSendRequest == maxPendingSendRequests || isPublishRateExceeded) {
    // When the quota of pending send requests is reached, stop reading from socket to cause backpressure on
    // client connection, possibly shared between multiple producers
    ctx.channel().config().setAutoRead(false);
    recordRateLimitMetrics(producers);
    autoReadDisabledRateLimiting = isPublishRateExceeded;
    throttledConnections.inc();
}

pendingSendRequest值为正在处理的发送请求,因为可能存在多个生产者在发送消息,所以这里的值不一定为1,maxPendingSendRequests的值为配置文件中的maxPendingPublishRequestsPerConnection值,默认1000个请求

image-19.png

该代码语句做的事情是关闭channel自动读,统计生产者的速率指标,统计节流的连接数,其中重要的是这两句代码

ctx.channel().config().setAutoRead(false);
autoReadDisabledRateLimiting = isPublishRateExceeded;

ctx对象是ChannelHandlerContext组件,是netty的组件,它存在于channel中,前面提到channel中存在一个ChannelPipeline,Channelpipeline中存在由多个ChannelHandlerContext类型组成的双向链表,ChannelHandlerContext中又存在这个ChannelHandler,在前文可知,ServerCnx即是ChannelHandler类型的,所以在上面的代码中其实就是暂时停止了该ServerCnx的handlerSend对于消息的处理,因为channel方法指定了当前这个处理发布消息的channel,关闭了它的自动读,此时即使生产者发送消息过来也不会触发handlerSend方法了

此时有个疑问,主题中的字节数消息数计数器即currentPublishByteCount和currentPublishMsgCount是如何增长的呢?

回到handlerSend方法,继续向后看,代码会进入到publishMessage方法中

image-20.png

其中有一个checkAndStartPublish方法

image-21.png

其中又有一个startPublishOperation方法

image-22.png

在这个方法中可以看到很关键的代码

this.getTopic().incrementPublishCount(batchSize, msgSize);

image-23.png

incrementPublishCount方法中,更新了PublishRateLimiter限速器中的currentPublishByteCount和currentPublishMsgCount两个属性值

最后一个疑问,就是既然channel的autoRead自动读关闭了,又是怎么开启的?

在前文中提到的监听器中的1s执行1次的定时任务中就重置了,定时任务中的resetTopicPublishCountAndEnableReadIfRequired方法中还存在着一个enableProducerReadForPublishRateLimiting方法

image-24.png

该方法重置前文提到的autoReadDisabledRateLimiting标志位false,并开启channel的autoRead自动读,此时handlerSend方法就可以继续处理生产者发送过来的消息了

image-25.png

到这里,基本上就明白了pulsar主题生产限速的基本逻辑了,可以做出总结: 当开启主题发布限速时,如果发布到主题的消息字节数或条数达到设定阈值,主题通过主题限速器暂时停止新发送过来的消息处理, 当1s过后,主题又通过主题限速器清空发布到主题的字节数和消息数计数器,重新处理新发送过来的消息,综合来看就是主题限制了每秒发布到主题的消息为指定字节数或条数

最后还有一点个人的思考:pulsar生产限流有误差的原因会不会是在1s周期内每10ms扫描期间读取的字节数?