Pulsar源码解析之PersistentTopic类

引言

今天一起来看看Pulsar服务端消息相关最重要的一个类PersistentTopic,看看它都负责哪些事情以及是如何设计的

正文

在Topic被创建时,Pulsar集群中会有一台Broker维护这个Topic,在实现上就是维护一个PersistentTopic对象,这个PersistentTopic对象处理针对该Topic相关的一切操作,具体负责的相关操作如下

  • 处理订阅以及下线订阅
  • 新增生产者对象以及下线生产者对象
  • 处理消息写入
  • 进行跨集群复制
  • 记录Topic度量状态以及做Topic限流
  • 检查消息的TTL、积压、压缩、配置策略更新

绘成表格的形式如下图
在这里插入图片描述

本篇文章不会深入讲解每一项,主要是大概过下这个类都做了哪些事情以及大致逻辑,因为服务端的代码会经常跟这个类打交道,因此专门弄清楚这个类的相关知识还是很有必要的。接下来就让我们带着以下三个疑问去看下面的内容

  • PersistentTopic在什么时候会被创建?都有哪些重要的成员变量?
  • PersistentTopic的创建流程都会发生什么?
  • 上面那些相关操作大概是什么实现的?

一、何时创建

以下列出创建的时机,基本上流程都是发起创建Topic的时候会通过一致性哈希计算出这个Topic所归属的Bundle,然后去zookeeper获取这个Bundle所归属的Broker机器,最后请求这台Broker节点创建对应的PersistentTopic对象

  1. 管理流创建
    • cli管理命令行
    • 多语言Client
    • Http方式
  2. 写入流创建
    • 生产者写入流
    • 消费者写入流

接下来看看这个类的主要成员变量,重要的一些已经加上注释解释了

public class PersistentTopic extends AbstractTopic implements Topic, AddEntryCallback {

    // 管理Bookkeeper的Ledger,在做消息读取或者写入时会通过该对象
    protected final ManagedLedger ledger;

    // 存储订阅当前Topic的所有订阅对象,key是订阅名,value是订阅对象
    private final ConcurrentOpenHashMap<String, PersistentSubscription> subscriptions;

    // 管理对端集群,负责做跨集群数据复制
    private final ConcurrentOpenHashMap<String/*RemoteCluster*/, Replicator> replicators;
  
    //跟replicators类似
    private final ConcurrentOpenHashMap<String/*ShadowTopic*/, Replicator> shadowReplicators;
    @Getter
    private volatile List<String> shadowTopics;
    private final TopicName shadowSourceTopic;

    //调度限流器
    private Optional<DispatchRateLimiter> dispatchRateLimiter = Optional.empty();
    //调度限流锁
    private final Object dispatchRateLimiterLock = new Object();
    //订阅限流器
    private Optional<SubscribeRateLimiter> subscribeRateLimiter = Optional.empty();
    //积压游标阈值条数
    private final long backloggedCursorThresholdEntries;
    public static final int MESSAGE_RATE_BACKOFF_MS = 1000;

  	//处理消息重复情况
    protected final MessageDeduplication messageDeduplication;

    //处理消息压缩服务
    private TopicCompactionService topicCompactionService;

    // 在对外开放压缩策略配置时,根据用户配置创建对应的压缩策略
    private static Map<String, TopicCompactionStrategy> strategicCompactionMap = Map.of(
            ServiceUnitStateChannelImpl.TOPIC,
            new ServiceUnitStateCompactionStrategy());

    //未知
    private CompletableFuture<MessageIdImpl> currentOffload = CompletableFuture.completedFuture(
            (MessageIdImpl) MessageId.earliest);

    //负责跨集群复制时订阅相关事项
    private volatile Optional<ReplicatedSubscriptionsController> replicatedSubscriptionsController = Optional.empty();

  	//记录Topic度量相关信息,如这个Topic的写入速率、消费速率等
    private static final FastThreadLocal<TopicStatsHelper> threadLocalTopicStats =
            new FastThreadLocal<TopicStatsHelper>() {
                @Override
                protected TopicStatsHelper initialValue() {
                    return new TopicStatsHelper();
                }
            };
   }

二、创建流程

创建流程主要分为构建和初始化两个阶段,一般是先通过构造函数创建PersistentTopic,创建好后在调用其initialize方法进行初始化

1. 构造函数

构造函数主要做三件事:1. 更新Broker级别策略 2. 创建发布限流 3. 初始化容器。具体代码实现如下

    public AbstractTopic(String topic, BrokerService brokerService) {
      	//基本都是赋值操作
        this.topic = topic;
        this.brokerService = brokerService;
        this.producers = new ConcurrentHashMap<>();
        this.isFenced = false;
        ServiceConfiguration config = brokerService.pulsar().getConfiguration();
        this.replicatorPrefix = config.getReplicatorPrefix();

        topicPolicies = new HierarchyTopicPolicies();
        updateTopicPolicyByBrokerConfig();

        this.lastActive = System.nanoTime();
        this.preciseTopicPublishRateLimitingEnable = config.isPreciseTopicPublishRateLimiterEnable();
      	//创建发布限流
        topicPublishRateLimiter = new PublishRateLimiterImpl(brokerService.getPulsar().getMonotonicSnapshotClock());
      	//更新限流策略
        updateActiveRateLimiters();
}  

public PersistentTopic(String topic, ManagedLedger ledger, BrokerService brokerService) {
        super(topic, brokerService);
        //赋值操作
        this.orderedExecutor = brokerService.getTopicOrderedExecutor() != null
                ? brokerService.getTopicOrderedExecutor().chooseThread(topic)
                : null;
        this.ledger = ledger;
  			//初始化容器,分别是维护当前Topic的订阅情况、跨集群副本情况
        this.subscriptions = ConcurrentOpenHashMap.<String, PersistentSubscription>newBuilder()
                        .expectedItems(16)
                        .concurrencyLevel(1)
                        .build();
        this.replicators = ConcurrentOpenHashMap.<String, Replicator>newBuilder()
                .expectedItems(16)
                .concurrencyLevel(1)
                .build();
        this.shadowReplicators = ConcurrentOpenHashMap.<String, Replicator>newBuilder()
                .expectedItems(16)
                .concurrencyLevel(1)
                .build();
        this.backloggedCursorThresholdEntries =
                brokerService.pulsar().getConfiguration().getManagedLedgerCursorBackloggedThreshold();
        ....
    }

2. initialize方法

初始化方法的逻辑相比下要丰富写,归纳起来有以下四点

  1. 获取压缩服务

    通过双重判断获取单例TwoPhaseCompactor进行压缩,有专门的线程池进行处理压缩动作

  2. 遍历游标恢复订阅的状态

    当Topic重新分配后,在新的Broker中要根据游标恢复订阅、跨集群复制的状态,这样才能让客户端“无感”

  3. 跨集群复制

    遍历这个Topic的游标,如果游标中存在跨集群复制游标则创建对应的GeoPersistentReplicator对象进行消息的复制

  4. 更新配置

    • 更新Topic命名空间的配置策略
    • 初始化调度限流DispatchRateLimiter
    • 更新订阅/发布/资源组限流
    • 更新Topic级别的配置

具体代码实现如下

  public CompletableFuture<Void> initialize() {
        
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        //获取Broker压缩对象
        futures.add(brokerService.getPulsar().newTopicCompactionService(topic)
                .thenAccept(service -> {
            PersistentTopic.this.topicCompactionService = service;
            //遍历这个Topic的游标恢复订阅中的对象
            this.createPersistentSubscriptions();
        }));

        //遍历这个Topic的游标,如果游标中存在跨集群复制游标则创建对应的GeoPersistentReplicator对象进行消息的复制
        for (ManagedCursor cursor : ledger.getCursors()) {
            if (cursor.getName().startsWith(replicatorPrefix)) {
                String localCluster = brokerService.pulsar().getConfiguration().getClusterName();
                String remoteCluster = PersistentReplicator.getRemoteCluster(cursor.getName());
                futures.add(addReplicationCluster(remoteCluster, cursor, localCluster));
            }
        }
        return FutureUtil.waitForAll(futures).thenCompose(__ ->
            brokerService.pulsar().getPulsarResources().getNamespaceResources()
                .getPoliciesAsync(TopicName.get(topic).getNamespaceObject())
                .thenAcceptAsync(optPolicies -> {
                    if (!optPolicies.isPresent()) {
                        isEncryptionRequired = false;
                        updatePublishDispatcher();
                        //进行资源组级别的隔离
                        updateResourceGroupLimiter(new Policies());
                        initializeDispatchRateLimiterIfNeeded();
                        updateSubscribeRateLimiter();
                        return;
                    }

                    Policies policies = optPolicies.get();

                    //更新命名空间级别的策略
                    this.updateTopicPolicyByNamespacePolicy(policies);
                    //初始化调度限流
                    initializeDispatchRateLimiterIfNeeded();
                    //更新订阅限流
                    updateSubscribeRateLimiter();
                    //更新发布限流
                    updatePublishDispatcher();
                    //更新资源组限流
                    updateResourceGroupLimiter(policies);

                    this.isEncryptionRequired = policies.encryption_required;

                    isAllowAutoUpdateSchema = policies.is_allow_auto_update_schema;
                }, getOrderedExecutor())
                .thenCompose(ignore -> initTopicPolicy())
                .exceptionally(ex -> {
                    ....
                }));
    }

三、负责内容

1、生产者相关

新增

生产者客户端启动时会跟Topic归属的Broker建立TCP连接并请求Broker端ServerCnx的handleProducer方法进行处理,这个方法最终会调用PersistentTopic的addProducer方法进行创建,咱们来看看实现

    public CompletableFuture<Optional<Long>> addProducer(Producer producer,
            CompletableFuture<Void> producerQueuedFuture) {
      	//进去看看父类的实现
        return super.addProducer(producer, producerQueuedFuture).thenCompose(topicEpoch -> {
            messageDeduplication.producerAdded(producer.getProducerName());

            // Start replication producers if not already
            return startReplProducers().thenApply(__ -> topicEpoch);
        });
    }
    
   public CompletableFuture<Optional<Long>> addProducer(Producer producer,
                                                         CompletableFuture<Void> producerQueuedFuture) {
     ....
     //继续跟踪
     return internalAddProducer(producer);
     ....
   }

    protected CompletableFuture<Void> internalAddProducer(Producer producer) {
        ....
        //producers是ConcurrentHashMap结构,相当于会在PersistentTopic中维护服务端的生产者对象
        Producer existProducer = producers.putIfAbsent(producer.getProducerName(), producer);
        ....
        return CompletableFuture.completedFuture(null);
    }

删除

删除的逻辑很简单,也是通过TCP接收到客户端断开连接的请求后,会调用Producer的close方法进行资源释放,同时从维护的producers中移除此对象

2. 订阅相关

消费者客户端启动时会跟Topic归属的Broker建立TCP连接并请求Broker端,请求由ServerCnx的handleSubscribe方法进行处理,这个方法最终会调用PersistentTopic的internalSubscribe方法进行创建,咱们来看看实现

新增

    private CompletableFuture<Consumer> internalSubscribe(final TransportCnx cnx, String subscriptionName,
                                                          long consumerId, SubType subType, int priorityLevel,
                                                          String consumerName, boolean isDurable,
                                                          MessageId startMessageId,
                                                          Map<String, String> metadata, boolean readCompacted,
                                                          InitialPosition initialPosition,
                                                          long startMessageRollbackDurationSec,
                                                          boolean replicatedSubscriptionStateArg,
                                                          KeySharedMeta keySharedMeta,
                                                          Map<String, String> subscriptionProperties,
                                                          long consumerEpoch,
                                                          SchemaType schemaType) {
   ....
   //创建对应的客户端对象
   Consumer consumer = new Consumer(subscription, subType, topic, consumerId, priorityLevel,
                        consumerName, isDurable, cnx, cnx.getAuthRole(), metadata,
                        readCompacted, keySharedMeta, startMessageId, consumerEpoch, schemaType);
   ....
   //将消费者对象加到订阅里
   return addConsumerToSubscription(subscription, consumer);                                                    
}

    protected CompletableFuture<Void> addConsumerToSubscription(Subscription subscription, Consumer consumer) {
				....
        //最终将将创建的消费者放到Dispatcher中进行管理以及加到subscriptions这个Map中进行维护
        return subscription.addConsumer(consumer);
    }

删除

删除的逻辑也是比较简单的,就是从subscriptions中进行移除

3. 处理消息写入

消息写入的流程相对比较复杂,后面再单独分析,暂时跳过

4. 跨集群复制

跨集群复制这里实现逻辑其实不难,相当于在Broker上去读取Bookkeeper获取数据并通过启动生产者往其他集群进行数据写入,具体细节后面也单独写文章进行分析

5. 消息清理

Pulsar的消息TTL、积压、压缩都是在Broker启动时往线程池中加入定时任务轮询触发的

TTL

TTL会循环扫描每个Topic过期的消息位置,并通过改变游标进行标记,而清理是由专门的线程根据游标进行处理的

积压

消息积压也是会定期检测,分两种情况,一种是消息超过限制额度,另一种是消息时间超过额度。这两种情况都会触发积压策略的处理,具体的策略在BacklogQuota.RetentionPolicy中定义

压缩

跟上面一样,轮询检测进行处理

配置更新

在PersistentTopic启动的初始化initialize方法中会调用onUpdate进行策略更新,那Pulsar又是如何做的支持配置动态更新的呢?这个问题保留给大家

四、总结

本文主要强调PersistentTopic类的重要性、聊聊它都负责哪些事情以及大概实现,具体每个细节展开都能单独写一篇文章,这个敬请期待~

  • 21
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏洛克·林

有钱的捧个钱💰场或人场~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值