[Pulsar 源码] Pulsar client 原理解析

Pulsar client原理解析

导语
Pulsar 作为一个消息传输的解决方案,最基本的功能是提供了pub/sub模型的消息服务,即作为一个消息中间件的能力,本文主要以Java Client为例讲述生产者、消费者和Broker之间的交互过程。

1. Client 与 broker 交互流程

和常见的MQ一样,Topic(分区)由 Broker 持有,因此生产者和消费者首先需要创建连接到 Broker,然后查询对应发布或者订阅的 Topic 所在的 Broker 信息。
这个过程起始分为以下几个步骤:

  • 创建到 Broker 的连接
  • 连接创建完毕之后,向 Broker 查询 Topic 的分区数信息
  • 根据分区数,查询对应分区当前被哪个 Broker 持有
  • 创建到分区所在 Broker 的连接

当 producer/consumer 已经连接到具体分区所在的 Broker 之后,
生产者可以进行生产:

  • 向 Broker 发送请求注册 Producer
  • 发送数据

消费者可以进行消费:

  • 向 Broker 发送请求注册 Subscription
  • 请求数据
  • 发送 Ack 请求到 Broker

当 Broker 接收到上述请求之后,会一一处理并且回应:

  • 对于创建链接请求,响应创建成功
  • 对于分区查询请求,响应分区数
  • 对于分区所有 Broker 查询请求,响应 Broker URL
  • 对于生产者/消费者注册请求,响应生产者、消费者注册成功
  • 对于生产者注册请求,响应生产注册成功
  • 对于生产数据请求,响应生产已经接受
  • 对于消费数据请求,响应数据

总体上的生产者、消费者和Broker的交互逻辑如上所述,下面结合代码详细描述整个过程。

2. PulsarClient 初始化过程

初始化Pulsar Producer和Consumer都需要先初始化 Pulsar client。创建一个 pulsar client 的代码如下:

PulsarClient pulsarClient =
                PulsarClient.builder()
                        .serviceUrl(url)
                        .statsInterval(intervalInSecs, TimeUnit.SECONDS)
			.build();

这里会涉及到两个参数

  • serviceUrl: broker 地址列表,也可以通过ServiceUrlProvider提供
  • statsInterval: client状态采集的周期,默认是60s,比如生产者会采集生产速率,消费者会采集消费速率,当配置大于0时生效,如果小于等于0则不采集状态

初始化 PulsarClientImpl

当所有的参数设置完成之后,最后build()会初始化一个PulsarClientImpl对象。在这个过程中,首先会初始化 EventLoopGroup 和 ConnectionPool ,然后执行 PulsarClientImpl的初始化。

初始化EventLoopGroup
  • 根据平台是否支持epoll,决定初始化一个 EpollEventLoopGroup 或者NioEventLoopGroup(netty可以使用native transport来提升性能),并且设置EventLoopGroup的IO线程数。

一个EventLoopGroup包含多个EventLoop, Netty使用EventLoop来处理连接上的读写事件,而一个连接上的所有请求都保证在一个EventLoop中被处理,一个EventLoop中只有一个Thread,所以也就实现了一个连接上的所有事件只会在一个线程中被执行。

初始化ConnectionPool

主要是初始化一下内容

  • eventLoopGroup
  • 连接池缓存pool,pool用来保存 链接地址和ClientCnx的映射关系
  • 初始化 Netty Bootstrap,设置一些参数以及handler,PulsarChannelInitializer, 在链接建立之后,会初始化 Channel 的 handler pipeline,主要的handler是 ClientCnx
  • 初始化 Netty dnsResolver
public ConnectionPool(ClientConfigurationData conf, EventLoopGroup eventLoopGroup,
            Supplier<ClientCnx> clientCnxSupplier) throws PulsarClientException {
        this.eventLoopGroup = eventLoopGroup;
        this.maxConnectionsPerHosts = conf.getConnectionsPerBroker();

        pool = new ConcurrentHashMap<>();
        bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup);
        bootstrap.channel(EventLoopUtil.getClientSocketChannelClass(eventLoopGroup));

        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, conf.getConnectionTimeoutMs());
        bootstrap.option(ChannelOption.TCP_NODELAY, conf.isUseTcpNoDelay());
        bootstrap.option(ChannelOption.ALLOCATOR, PulsarByteBufAllocator.DEFAULT);

        try {
            bootstrap.handler(new PulsarChannelInitializer(conf, clientCnxSupplier));
        } catch (Exception e) {
            log.error("Failed to create channel initializer");
            throw new PulsarClientException(e);
        }

        this.dnsResolver = new DnsNameResolverBuilder(eventLoopGroup.next()).traceEnabled(true)
                .channelType(EventLoopUtil.getDatagramChannelClass(eventLoopGroup)).build();
    }

EventLoopGroup 和 ConnectionPool初始化完毕之后,继续进行PulsarClientImpl的初始化工作

  • 初始化lookup服务 :Lookup服务用来查找topic 的元数据信息,比如分区数、分区所在broker地址、Schema信息等,有两种类型,HttpLookupService 和 BinaryProtoLookupService
  • 初始化producer/consumer缓存(set)
  • 初始化MemoryLimitController:MemoryLimitController用来做生产消息的内存占用限制,内部会维护内存占用的计数器,当生产消息时,会增加计数;生产完毕时减少计数;如果计数达到上限时,根据blockIfQueueFull来决定阻塞请求或者抛出异常

完成初始化工作之后,将Client的状态置为Open

3. Publish工作原理

向Pulsar Publish数据,需要首先初始化一个producer,初始化的过程中可以为producer指定一些属性

pulsarClient.newProducer()
                .enableBatching(true) // 开启batch
                .batchingMaxPublishDelay(100, TimeUnit.MILLISECONDS) // batch的最大等待时间
                .batchingMaxBytes(12800) // batch的最大大小
                .batchingMaxMessages(10) // batch中最多保留的消息条数
                .messageRoutingMode(MessageRoutingMode.RoundRobinPartition) // Message Routing Mode
                .roundRobinRouterBatchingPartitionSwitchFrequency(10) // batch开启时,分区切换频率
                .maxPendingMessages(50000) // 最大缓存的生成请求数
                .maxPendingMessagesAcrossPartitions(100000)// 分区topic配置,即所有分区的最大缓存数量
                .blockIfQueueFull(true) // 当达到最大缓存数时是否block客户端
                .sendTimeout(5, TimeUnit.SECONDS) // 发送超时时间
                .topic("test") 
                .compressionType(CompressionType.SNAPPY) // 压缩类型
                .producerName("producer-name")
                .enableChunking(true) // 是否开启Chunking特性
                .accessMode(ProducerAccessMode.Shared) // producer mode
  							 .intercept(new ProducerInterceptor() { // 设置interceptor,实现自定义的功能
                    @Override
                    public void close() {}
                   
                    @Override
                    public boolean eligible(Message message) {return false;}

                    @Override
                    public Message beforeSend(Producer producer, Message message) { return null;}

                    @Override
                    public void onSendAcknowledgement(Producer producer, Message message, MessageId msgId, Throwable exception) {}
                })
                .create();

Producer配置

Batch 配置

Producer可以配置Batch发送消息和单条发送消息,开启了batch之后,可以通过:

  • 消息条数
  • 消息大小
  • batch等待时间

以上三个条件来产生batch,上述配置只要满足一个就会触发一个新的batch

Routing 配置

这个配置是针对分区topic的,可以为message设置路由策略来决定message会被发到哪个分区,包含三种:

  • SinglePartition:随机选择一个分区,将所有消息写到这个分区
  • RoundRobinPartition:轮询策略,会将消息轮询的发送到每个分区,如果producer没有开启batch,则每发送一条message,就会切换一个分区;如果开启了batch,则按照batch的最大等待时间batchingMaxPublishDelayMicros的倍数来决定,具体的倍数由参数 batchingPartitionSwitchFrequencyByPublishDelay 指定,即每经过 batchingMaxPublishDelayMicros * batchingPartitionSwitchFrequencyByPublishDelay 的时间,分区切换一次,在切换的时间内message被发送到同一个分区
  • CustomPartition: 自定义分区策略,分区方式有MessageRouter决定,即需要自己实现MessageRouter

对于 SinglePartition 和 RoundRobinPartition,如果message设置了key,都会按照hash的方式决定message写到哪个分区。

默认配置RoundRobinPartition;如果存在customMessageRouter,则RoutingMode只能是CustomPartition。

Pending配置

Producer可以最多hold的message数量,如果超过这个配置,则按照blockIfQueueFull来决定是阻塞生产者或者抛出异常信息。

如果是分区topic,则每个分区可以hold的最大消息数量为:

Math.min(conf.getMaxPendingMessages(), conf.getMaxPendingMessagesAcrossPartitions() / numPartitions);
Chunk 配置

当一条Message的大小超出了broker配置的最大消息大小时,开启Chunk配置,可以让producer自动将消息split成为多个小的消息,并且按照顺序发送到Broker。如果消费者希望按照获取切分之前的Message,需要对producer和consumer进行以下配置:

  • 目前只支持 non-shared subscriptionpersistent topic
  • disable batch
  • Pulsar-client 在接收到 ack之后才能继续发送消息到Broker(防止chunk乱序),所以可以调小maxPendingMessages来避免太高的内存占用
  • 配置message 的 ttl/retention来清理不完整的chunk message, 有些场景比如producer或者broker重启,会导致broker接收到了不完整的chunk message,这一部分消息consumer无法ack,需要按照ttl或者retention被清理掉;或者通过配置ConsumerBuilder#expireTimeOfIncompleteChunkedMessage(long, TimeUnit)来执行message expire逻辑
  • 消费者最好配置 receiverQueueSize 和 maxPendingChuckedMessage

如果一个消息大小超过了最大值,那么会默认做切分,这是后会根据producerName-sequenceId拼接出一个UUid,所有的chunk都有一样的sequenceId和uuid。

Producer Mode 配置

生产者的模式,目前包括三种:

  • Shared :默认多个producer可以同时向一个topic publish消息
  • Exclusive:请求独占式生产,如果已经存在其他producer,则立即失败
  • WaitForExclusive:producer的创建会pending,直至获取到独占访问权限

Producer 与 Broker 交互

完成了配置之后,就可以对Producer进行初始化,然后publish message 到broker,这一系列的过程需要和Broker进行不断的交互,主要的交互流程如下:
在这里插入图片描述

接下来就围绕Producer的整个初始化以及生产过程分析Producer/Broker交互的整个过程

创建ProducerImpl/PartitionedProducerImpl

首先是一些参数校验和补齐的工作:

  • 校验是否batch和chunk同时开启,如果是,抛出异常
  • 校验topic

然后获取元数据信息:

  • 获取元数据信息
  • 创建ProdducerImpl或者PartitionedProducerImpl
  • 在Client的Producer缓存中保存Producer信息
获取元数据

Producer会向Broker发出元数据请求,通过lookup.getPartitionedTopicMetadata来完成,这个过程会涉及到几个交互命令,我们逐个进行分析(以BinaryProtoLookupService为例):

  • 第一步,Producer根据serviceUrl指定的Broker地址连接到Broker;如果配置了多个Broker地址,第一次连接时会随机选择一个,之后按照配置的顺序选取下一个Broker地址,通过bootstrap.connect(remoteAddress)完成,不会发送任务Pulsar Command到Broker,链接创建完毕之后,会触发Netty Handler的channelActive方法:
    • 如果keepAliveIntervalSeconds > 0,则每经过keepAliveIntervalSeconds的时间就启动keepAlive的任务,发送PING到Broker,Broker会返回PONG响应
    • 启动 request 超时定时检查任务,检查requestTimeoutQueue中是否存在超时的请求,如果有则从pendingRequests移除对应的请求并将请求结果置为超时
    • 发送CONNECT命令到Broker,并将Client状态置为 SentConnectFrame; Broker接收到CONNECT请求之后,会返回CONNECTED响应,并且携带Broker端的maxMessageSize配置信息
  • 第二步,链接创建完毕之后,Producer 向 Broker 发送PARTITIONED_METADATA请求,Broker接收到请求之后,会获取topic的元数据信息,然后返回PARTITIONED_METADATA_RESPONSE 给 Producer
创建Producer实例

根据 PARTITIONED_METADATA_RESPONSE 中的分区数,决定创建那种类型的Producer(ProducerImpl或者PartitionedProducerImpl),我们这里以ProducerImpl为例:

  • 将Producer状态置为Uninitialized
  • 初始化 pendingMessages 和 pendingCallbacks 队列,用来存放 pending 的 Message 和 Callback
  • 初始化 sequenceId,如果Producer 有配置 initialSequenceId,则将 lastSequenceIdPublished 和 lastSequenceIdPushed 都置为 initialSequenceId ,将msgIdGenerator 置为 initialSequenceId+1 ;如果没有配置,则 lastSequenceIdPublished 和 lastSequenceIdPushed 都置为-1,msgIdGenerator 置为0; lastSequenceIdPushed 表示已经 send 到 broker 的消息的 sequenceId,lastSequenceIdPublished 表示已经 publish 成功的 message 的 sequenceId
  • 启动 sendRequest超时检查任务,从 pendingMessages选取第一个请求,
    • 如果没有pending的Message,按照配置的超时时间新建一个新的超时检查任务
    • 如果存在pending的request,则判断是否超时,
      • 如果不超时,则计算剩余的等待时间,按照剩余时间new一个新的超时检查任务
      • 如果超时,则释放 pendingMessages 中所有的请求,并且清空 pendingMessages 和 pendingCallbacks,如果开启了batch,丢弃batchContainer中的所有内容,然后按照配置的超时时间创建一个新的超时检查任务
  • 计算出producer的创建超时时间
  • 如果开启Batch,初始化一个BatchContainer
  • 初始StatsRecorder
  • 初始化ConnectionHandler,然后调用ConnectionHandler.grabCnx 查找topic所在的broker地址,然后建立到broker的连接
grabCnx

grabCnx分为两个阶段,首先是lookup topic所在的broker,然后创建producer到broker的连接。先看findBroker的过程,依旧是通过LookupService来查找:

  • 首先根据serviceUrl向一个Broker发送 LOOKUP 请求,broker接收到lookup请求之后,首先根据请求的topic,查找对应的namespacebundle信息,然后查找bundle所在的broker地址,最后将结果返回给producer
  • producer得到topic所在的broker地址之后,创建到对应broker的连接,连接建立之后,回调ProducerImpl的connectionOpened方法,执行接下来的初始化工作:
    • 发送 PRODUCER 请求到broker,broker接收到 PRODUCER 请求之后,会进行以下操作:
      • 获取Topic信息
      • 检查 Topic 是否超出了BacklogQuota的限制,如果超过则返回异常,异常根据BacklogQuota.RetentionPolicy的不同而不同,如果policy是producer_request_hold,client接收到异常之后,会阻塞Producer的创建;如果是producer_exception则会抛出异常
      • 创建服务端的Producer对象,并将Producer注册到Topic中
      • 返回 PRODUCER_SUCCESS响应给Producer,会携带消息去重中的最大sequenceId
    • 接收到PRODUCER_SUCCESS响应之后,Producer会根据sequenceId来初始化lastSequenceIdPublished以及msgIdGenerator,逻辑是如果msgIdGenerator为0并且producer没有指定InitialSequenceId,则将lastSequenceIdPublished置为sequenceId,msgIdGenerator置为sequenceId+1
    • 如果开启了Batch,则初始化一个定时任务,根据配置的batchingMaxPublishDelayMicros时间来定时发送消息,发送是会将BatchContainer中的所有message遍历封装到send请求中发送到Broker,请求中会携带第一条message的sequenceId和最后一条mesage的sequenceId
    • 重新发送pendingMessages中的消息

到这里,producer的初始化工作全部完成。

生产数据

Producer初始化完成之后,就可以使用Producer来发送数据到broker。发送数据之前需要首先构造出Message,然后将Message发送到Broker。

TypedMessageBuilder<byte []> messageBuilder = producer.newMessage()
                .key("Key" + key) // 分区路由使用的key,即partition key
                .value("Value" + value) // 内容
                .deliverAfter(100, TimeUnit.SECONDS) // 延迟投递
                .eventTime(System.currentTimeMillis()) // eventtime时间
                .orderingKey("orkerkey".getBytes()) // 用于SubscriptionType#Key_Shared模式的key,如果没有会使用partition key
                .property("myKey-async","myValue-async"); // 消息属性
MessageIdImpl messageId = (MessageIdImpl) messageBuilder.sendAsync().get();

在构建Message的过程中可以指定一些参数,如上代码所示。生产数据的流程

  • Producer首先判断是否超过MaxPendingMessage的限制,超出则抛出异常
  • 判断是否是replicated的消息并且具有ProducerName,如果不是replicated的消息但是具有producerName,则抛出异常,因为producerName是在序列化send请求是设置的,在这里不应该存在producerName
  • 判断是否是不支持Batch 或者 hasDeliverAtTime,这两种场景都需要单独发送,不需要batch,并且如果请求的大小超过了最大限制,则需要判断是否支持chunk,不支持则抛出异常
  • 如果需要切分,那么切分之后会增加send请求数,再次判断是否超过MaxPendingMessage的限制,对每个chunk单独进行发送,切分之后的chunk具有相同的sequenceId(UUID,组合了producerName和sequenceId的字符串),不同的chunkId,相同的totalChunk,以及相同的压缩/非压缩size、producerName
  • 如果是chunked message,不用考虑batch,因为初始化producer的时候已经做了校验,batch和chunk不能同时存在,直接将消息发送到Broker
  • 如果是普通message,判断是否支持可以batch发送
    • 如果可以Batch(开启batch,并且没有deliverAtTime),判断是否可以添加到当前Batch
      • 如果可以添加到当前Batch(batch的消息大小和条数都可以容纳当前message),判断sequenceId是否小于等于 lastSequenceIdPushed
        • 如果sequenceId <= lastSequenceIdPushed,说明message可能重复,isLastSequenceIdPotentialDuplicated = true,需要则先把BatchContainer中的消息打包发送到Broker,原因是把可能重复的消息和不重复的消息区分开,然后将当前Message添加到BatchContainer
        • 否则,现将Message加到BatchContainer中,然后判断batch是否full,如果batch是full状态,则把BatchContainer中的消息打包发送到Broker
      • 如果不能添加到当前Batch,则先把BatchContainer中的消息打包发送到Broker,然后将当前Message添加到BatchContainer
    • 如果不可以Batch,直接发送消息到Broker

Broker接收到send请求之后,将通过ManagedLedger将数据写入到Bookie,然后返回 SEND_RECEIPT 给client。 客户端拿到SEND_RECEIPT之后,可以获取messageId,包括LedgerId、EntryId (batchIndex)。

4. Subscription 工作原理

从Pulsar subscription 数据,需要首先初始化一个 consumer,初始化的过程中可以为consumer指定一些属性

 Consumer<byte[]> consumer = pulsarClient.newConsumer()
    .topic("persistent://public/default/topic")
    .subscriptionName("sub")
    .consumerName("consumer")
    .subscriptionMode(SubscriptionMode.Durable)
    .subscriptionType(SubscriptionType.Exclusive)
    .ackTimeout(3, TimeUnit.SECONDS)
    .ackTimeoutTickTime(5, TimeUnit.SECONDS)
    .acknowledgmentGroupTime(2, TimeUnit.SECONDS)
    .isAckReceiptEnabled(true)
    .maxPendingChuckedMessage(100)
    .autoAckOldestChunkedMessageOnQueueFull(true)
    .isAckReceiptEnabled(ackReceiptEnabled)
    .startMessageIdInclusive()// 是否包含seek的messageId
    .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) // 订阅的起始positino,默认lastest
    .enableRetry(true)
    .deadLetterPolicy(DeadLetterPolicy.builder()
            .maxRedeliverCount(5)
            .deadLetterTopic("persistent://public/default/dead-letter-topic")
            .retryLetterTopic("persistent://public/default/retry-letter-topic")
            .build())
    .receiverQueueSize(5000)
    .maxTotalReceiverQueueSizeAcrossPartitions(50000)
    .batchReceivePolicy(BatchReceivePolicy.DEFAULT_POLICY)
    .priorityLevel(1)
    .enableBatchIndexAcknowledgment(true)
    .maxPendingChuckedMessage(1000)
    .subscriptionName("my-subscriber-name")
    .subscribe();

Consuemr配置

Consumer的配置相比于Producer要多一些,我们详细的来看一下:

基本配置
  • topic:订阅的topic名称
  • subscriptionName:订阅名称
  • consumerName:消费者名称
订阅模式

subscriptionMode:订阅模式,包括durable和non-durable两种,

  • durable的订阅,会有cursor来记录订阅的位置信息
  • non-durable的订阅则没有cursor
订阅类型

subscriptionType:订阅类型,包括:

  • Exclusive:一个订阅中只能有一个consumer,可以保证消费的顺序
  • Failover:一个订阅中可以存在多个consumer,只有一个consumer可以接收消息,当这个consumer断开连接之后,其他的consumer才可以接收消息,可以保证消费的顺序;对于多分区topic,每个分区都可以保证消费的顺序,多个分区会在所有可用的consumer上分配,对于一个分区,最多有一个active的consumer(类似于kafka、tubemq)
  • Shared:一个订阅中可以存在多个consumer,消息以 round-robin 的方式发送给所有连接的consumer,不能保证消费顺序
  • Key_Shared:一个订阅中可以存在多个consumer,所有具有相同key的消息会给发送给同一个consumer
ACK设置
  • ackTimeout : message ack的超时时间,配置时需要大于1s;默认ackTimeout是disable的,即所有已经投递的消息都不会重新投递;当enable ack timeout时,如果一个投递的消息在timeout的时间内没有ack,这个消息将被重新投递

  • ackTimeoutTickTime: UnAckedMessageTracker中ack 超时检查任务的周期,不能超过ackTimeout,这里之所以不用ackTimeout是为了提供一个更加细粒度的超时控制

  • acknowledgmentGroupTime:ack在默认情况下并不是每次都提交到broker,而是通过group的方式提交组合成一个ack请求提交到broker,默认group时间是100ms

  • enableBatchIndexAcknowledgment:是否开启batch index级别的ack,默认pulsar的ack级别是<LedgerId, EntryID>级别,这个配置开启之后,可以ack到batch内的index级别

ChunkMessage消费配置
  • maxPendingChuckedMessage: 最大可以缓存的chunk message数量
  • autoAckOldestChunkedMessageOnQueueFull: 是否自动对超出 maxPendingChuckedMessage的chunk message进行ack,如果设置为true,则自动ack,否则不自动ack(会有超时检查任务触发重新投递)
  • expireTimeOfIncompleteChunkedMessage:chunk message的超时时间,如果consumer超过这个时间没有接收到一个message的所有chunk message,consumer可以expire这些不完整的chunk
Deadletter / RetryLetter 配置
  • enableRetry :是否支持自动retry

  • deadLetterPolicy: 死信队列配置,可以配置最大 re-deliver 次数,超过这个次数之后,consumer会将数据写入到deadLetterTopic中,然后对message进行ack;如果enableRetry =true,consumer会自动订阅RetryLetterTopic,在调用 reconsumeLater 方法之后,会首先将数据写入到RetryLetterTopic中并对message进行ack,当retry次数超过max re-deliver次数之后,会将topic写入deadLetter中,并且对message进行ack

如果enableRetry =false,ack超时或者nack的次数大于 maxRedeliverCount ,就会将 message 写入DeadLetterTopic,并且ack,重试是在broker的RedeliveryTracker记录的;

如果enableRetry =true, 那么retry也会生效,如果调用了reconsumeLater方法,consumer会将message写到RetryLetterTopic中,并且进行ack (不需要等待 重试次数大于maxRedeliverCount),因为consumer会自动订阅 RetryLetterTopic 所以consumer可以从RetryLetterTopic中继续消费到这个message;当retry的次数超过了maxRedeliverCount时,会将message写入到DeadLetterTopic,并且 ack RetryLetterTopic

接收缓存队列配置
  • receiverQueueSize:consumer最大的接收缓存大小
  • maxTotalReceiverQueueSizeAcrossPartitions:分区topic的最大接收缓存

这两个配置和producer的配置类似,决定了consumer一次可以从broker的最大的数据接收量

优先级
  • priorityLevel: 0表示最高优先级。在Share模式的订阅中,broker会有限将数据dispatch到最高优先级的consumer中(如果这个consumer的接收队列没有满),当高优先级的consumer没有permits时,才会dispatch到低一级优先级的consumer。

对于Share的订阅,Consumer-A 优先级为0,Consumer-B 优先级为 1,那么broker会有限将数据dispatch到A,直到A的permits使用完才会向B发送数据。下面是一个示例:

Consumer PriorityLevel Permits

  • C1 0 2

  • C2 0 1

  • C3 0 1

  • C4 1 2

  • C5 1 1

Broker dispatch的顺序为C1,C2,C3,C1,C4,C5,C4

对于Failover订阅,Broker会根据优先级和consumerName的字典序选择active 的 consumer,

如果优先级一样,按照name的字典序选择,如下,active consumer为C1

Consumer PriorityLevel Name

  • C1 0 aaa
  • C2 0 bbb

如果优先级币一样,按照优先级选择,如下,active consumer 为C2

Consumer PriorityLevel Name

  • C1 1 aaa
  • C2 0 bbb

对于分区topic,Broker会将partition平均的分配给最高优先级的consumer。

BatchReceive 配置
  • batchReceivePolicy : batch receive策略可以现在在一个batch中的消息数量和大小,也可以指定一个等待消息的超时时间,batch receive会在数量、大小或者超时三个条件中任何一个触发的条件下完成。

自定义batch receive policy。

client.newConsumer().batchReceivePolicy(BatchReceivePolicy.builder()
              .maxNumMessages(100)
              .maxNumBytes(5 * 1024 * 1024)
              .timeout(100, TimeUnit.MILLISECONDS);

Consumer 和 Broker 交互

完成了配置之后,就可以对Consumer(后续均使用ConsumerImpl为例,并且topic为Non-Paititioned topic)进行初始化,然后从broker消费数据,这一系列的过程需要和Broker进行不断的交互,主要的交互流程如下:
在这里插入图片描述

创建ConsumerImpl

首先是一些参数校验和补齐的工作:

  • 校验topic和subscription name
  • 初始化DLQ相关配置
  • 校验 Compact topic,如果是读取Compact topic,那么订阅类型只能是Exclusive或者Failover
  • 校验ConsumerEventListener,用来监听Failover模式下active consumer切换时间

根据配置的Topic信息,决定创建那种类型的Topic,包括三种类型:配置一个Topic,配置多个Topic,Topic parttern,以一个topic为例:

和Producer一样首先要获取元数据信息,

  • 如果是一个分区Topic,则初始化MultiTopicsConsumerImpl,否则,初始化ConsumerImpl
  • 在consumers缓存中记录consumer信多个Topic信息
ConsumerImpl

如果Topic是一个非分区Topic,client会创建一个ConsumerImpl对象,并且做初始化的相关工作,关键的初始化如下:

  • 最大接收队列长度,maxReceiverQueueSize
  • incomingMessages,接收队列,用于存放broker 发送过来的message
  • unAckedChunkedMessageIdSequenceMap,保存没有完全消费的chunk message信息的map
  • 配置batch receive policy ,如果policy 中配置了超时时间,则会启动一个TimeoutTask,这个超时任务会定期检查pending的batch receive 请求,如果请求超时,那么会从incomming中取出message,封装在MessagesImpl中返回,这个过程会将所有的message添加到unAckTracker中跟踪ack状态
  • 设置 receiverQueueRefillThreshold,这个值用来重新出发FlOW请求,当permit数量大于这个值时就会想broker发送FLOW请求,是maxReceiveQueueSize的一半
  • 初始化NegativeAcksTracker,NegativeAcksTracker 用来跟踪nack的message
    • consumer调用nack时,NegativeAcksTracker 会记录messageId信息
    • 然后由一个定时任务去检查nack的时间是否超过nackDelayNanos,如果没有超过,则用剩余的时间初始化一个新的定时任务;如果超过,则触发nack的逻辑
      • 首先将chunkmessage和nack的message 都加到 messagesToRedeliver中
      • 如果订阅类型不是share,那么为了保证有序,需要重传所有 的消息,清空incomingqueue和unackTracker,然后向Broker发送 REDELIVER_UNACKNOWLEDGED_MESSAGES 请求,broker会重新投递所有的unack的消息
      • 如果订阅类型是share,那么首先从incomming queue中的移除所有需要messagesToRedeliver的所有message,然后向broker 发送REDELIVER_UNACKNOWLEDGED_MESSAGES ,并携带 需要重传的MessageId信息,broker会投递所有messageId对应的Message
  • 初始化chunk message相关内容,maxPendingChunkedMessage,pendingChunkedMessageUuidQueue,expireTimeOfIncompleteChunkedMessageMillis,autoAckOldestChunkedMessageOnQueueFull
  • 初始化unAckTracker,用来跟踪没有ack的message信息,当message在timeout时间内没有ack时,unAckTracker会触发消息的重新投递
  • 初始化acknowledgmentsGroupingTracker,ack在默认情况下并不是每次都提交到broker,而是通过group的方式提交组合成一个ack请求提交到broker,默认group时间是100ms
grabcnx

grabcnx的过程和producer一致,grabCnx分为两个阶段,首先是lookup topic所在的broker,然后创建consumer到broker的连接。先看findBroker的过程,依旧是通过LookupService来查找:

  • 首先根据serviceUrl向一个Broker发送 LOOKUP 请求,broker接收到lookup请求之后,首先根据请求的topic,查找对应的namespacebundle信息,然后查找bundle所在的broker地址,最后将结果返回给consumer
  • consumer得到topic所在的broker地址之后,创建到对应broker的连接,连接建立之后,回调ConsumerImpl的connectionOpened方法,执行接下来的初始化工作:
    • 清理内部的接收队列,并且为startMessageId赋值,如果incomming queue 有message,那么startMessageId为第一个message的上一个message(如果是batch message,返回batchIndex -1;否则返回 entryId -1);如果incomming queue 中没有message,则startMessageId为lastDequeuedMessageId(如果lastDequeuedMessageId != Position.earliest); 如果没有接收和处理过任务数据,则使用startMessageId
    • 清理possibleSendToDeadLetterTopicMessages
    • 发送Subscribe请求到Broker,如果是durable的订阅,那么subscribe请求中的startMessageId为空,因为对于durable的订阅,起始位置有broker的cursor决定;另外注意是否允许自动创建Topic, 由subscribe请求中携带的 forCreateTopic 和Broker的 isAllowAutoTopicCreation 共同;是否可以自动创建 Subscription,由 broker 配置 isAllowAutoSubscriptionCreation 和 topic 配置 autoSubscriptionCreationOverride 共同决定
      • Broker 接收到SubScribe请求之后,首先获取topic对应的Topic对象,然后调用Topic#Subscribe创建新的subscription,以duarable为例,会调用Topic#getDurableSubscription-> ML#asyncOpenCursor
        • 初始化一个 ManagedCursorImpl,主要是individualDeletedMessages,batchDeletedIndexes以及一些状态信息的初始化,cursor状态为uninitialized
        • 对cursor进行初始化操作,将curosr的markDeletePosition置为lac,readposition置为lac的下一个位置,cursor状态为No_Ledger
        • 为cursor创建一个ledger,根据订阅配置的消费起始位置初始化cursor,重置markDeletePosition和readposition
      • 通过ML创建cursor完毕之后,为这个订阅和cursor初始化一个subscription,以PersistentSubscription为例,初始化包括PersistentMessageExpiryMonitor,PersistentMessageExpiryMonitor用来处理消息的过期逻辑
      • 实例化一个broker侧的Consumer,consumer会跟踪individual ack的状态信息
      • 将consumer添加到subscription中,这个过程会根据订阅类型为subscription初始化dispatcher,
        • Exclusive,对应PersistentDispatcherSingleActiveConsumer
        • Shared,对应PersistentDispatcherMultipleConsumers
        • Failover,对应PersistentDispatcherSingleActiveConsumer
        • Key_Shared, 对应PersistentStickyKeyDispatcherMultipleConsumers
        • dispatcher初始化之后,将consumer记录在dispatcher中,
          • 对于Exclusive,会选择第一个consumer作为active的consumer;
          • 对于Failover,会选择第partitionIndex % consumersSize个consumer作为active的consumer,如果是费分区topic,则选择第一个consumer;
          • 然后会开始为active consumer读数据,过程如下
            • 如果是Exclusive,或者Failover并且activeConsumerFailoverDelayTimeMillis <0,对cursor进行rewind,即将cursor重置到mark delete的位置,将readposition置为markdelete的下一个位置
            • 如果是failover的consuemr,通知所有consumer,active consumer的变更
            • 然后尝试读消息,不过consumer初始化的permit数为0,所有不会执行读取
          • 如果是Shared,会将consumer保存在consumerList中,之后会根据优先级进行轮训发送
          • 如果是Key_Shared,则会根据key的hash值选择consumer,这里选择策略有ConsistentHashingStickyKeyConsumerSelector、HashRangeAutoSplitStickyKeyConsumerSelector和HashRangeExclusiveStickyKeyConsumerSelector,这里不展开详细说明。
      • consumer添加到subscription之后,进行检查backlogged cursor信息
        • 如果consumers不为空并且cursor的entry数量小于backloggedCursorThresholdEntries,则将cursor置为active
        • 否则将cursor置为inactive,active可以从cache中读数据,inactive的数据只能充bookkeeper中读取
      • 返回SUCCESS响应给Consumer
    • 接收到Broker响应之后,首先将状态设置为Ready,并将permit数量设置为0,如果是非分区的consumer或者是一个重连的分区consumer,将permit数增加为receiveQueue的大小,并且向broker 发送flow请求。

到这里,consumer的初始化工作全部完成。

FLOW

可以看到对于一个非分区的topic,初始化完成之后,就会想broker 发送 FLOW 请求来获取消息,flow请求中会携带permit数量,broker 接收到 FLOW 之后的处理流程如下:

  • 使用Broker侧的consumer对象,执行Consumer#flowPermits ,这个过程会增加consumer的permit值,然后 Subscription#consumerFlow->dispatcher#consumerFlow
  • 计算可以读取的entry数量
  • ManagedCursorImpl#asyncReadEntriesOrWait,如果有数据就读取,如果没有,则等待10ms之后再去读取,
  • ManagerLedger#asyncReadEntry -> EntryCacheImpl#asyncReadEntry,首先从cache中读取,否则从bookie读取entry
  • 过滤entry,将可以发送给consumer的数据准备好,过滤的原因包括:checksum 或者metadata corrupt,内部标记数据,延迟消息等
  • 发送数据给consuemr,发送是每个entry单独发送的,并且增加broker侧Consumer的permit值
  • 如果没有pending的读请求,发起一次新的读请求(push过程)

consumer接收到entry之后,如果entry中是一个message,将其加入到incommingqueue中;如果是batch message,则将解析出来的每个message加到incomming queue中;

receive

consumer下一步会调用receive方法处理message,处理过程如下:

  • 从incommingqueue中取出一个message
  • 增加consumer的permit
  • 在 unAckedMessageTracker 中记录messageId, 如果在ack 超时时间内没有ack,就会向broker 发送redeliver请求
ACK

执行完消费逻辑之后,可以进行ack,ack分为两种类型Cumulative和Individual,默认是Individual类型的ack。Individual会ack一条message,Cumulative会ack这个message之前的所有数据,Shared和Key_Shared模式不能使用Cumulative数据。ack默认不是每个请求都会发到broker,而是group ack的方式,聚集一批ack一起发送。

  • 如果是Individual,记录指标,重unAckTracker和PossibleSendToDeadLetterTopicMessages中删除messageId,
    • 如果acknowledgementGroupTimeMicros==0,说明不用等待直接发送ack到broker
    • 否则,将ack请求放在pendingIndividualAcks中,当ack数量大于MAX_ACK_GROUP_SIZE或者超时时,将pendingIndividualAcks的所有ack一起发送给broker
  • 如果是Cumulative,
    • 如果acknowledgementGroupTimeMicros==0,说明不用等待直接发送ack到broker
    • 否则,更新LAST_CUMULATIVE_ACK_UPDATER中的messageId信息,当超时时,发送Cumulative ack 给broker

Broker接收到ack请求之后的处理如下:

  • 如果是individual类型的ack,Subscription#acknowledgeMessage -> ManagedCursor#asyncDelete
    • 对于每个ack的position,
      • 查看individualDeletedMessages是否包含,如果包含,说明position已经全部ack,则从batchDeletedIndexes中删除
      • 在individualDeletedMessages中保存position信息,如果是batch消息则在batchDeletedIndexes保存响应的信息,如果对应的ackset是empty说明所有batchMessage中的singleMessage都已经ack,则从batchDeletedIndexes删除,并且individualDeletedMessages保存position的range信息
    • 检查individualDeletedMessages第一个range,如果range的下限小于等于markDeletePosition,说明第一个ranger的下限位置对应的已经ack,将markDeletePosition移动至range的上限位置,
    • 更新markDeletePosition,并且将individualDeletedMessages中所有在新markDeletePosition之前的信息删除
    • 更新readPosition
    • 持久化ack信息,包括position、IndividualDeletedMessages、BatchedEntryDeletionIndexInfo信息
  • 如果是cumulative类型的ack,只有一个ack的position
    • 如果支持batchIndex,从batchIndex中删除position之前的所有内容
    • 更新markDeletePosition为ack position的前一个position
    • 更新readPosition
    • 持久化ack信息,包括position、IndividualDeletedMessages、BatchedEntryDeletionIndexInfo信息

整个消费的流程大致如上文所述,MultiTopicsConsumerImpl以及PatternMultiTopicsConsumerImpl的没有涉及,另外消费过程中的一些细节和特性,比如Batch消息生产、消费,Chunk message的生产、消费,Key_Shared消费的数据分发模式,也没有展开描述,会在后续的文章中逐个讲述。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值