文章目录
引言
今天一起来看看Pulsar服务端消息相关最重要的一个类PersistentTopic,看看它都负责哪些事情以及是如何设计的
正文
在Topic被创建时,Pulsar集群中会有一台Broker维护这个Topic,在实现上就是维护一个PersistentTopic对象,这个PersistentTopic对象处理针对该Topic相关的一切操作,具体负责的相关操作如下
- 处理订阅以及下线订阅
- 新增生产者对象以及下线生产者对象
- 处理消息写入
- 进行跨集群复制
- 记录Topic度量状态以及做Topic限流
- 检查消息的TTL、积压、压缩、配置策略更新
绘成表格的形式如下图
本篇文章不会深入讲解每一项,主要是大概过下这个类都做了哪些事情以及大致逻辑,因为服务端的代码会经常跟这个类打交道,因此专门弄清楚这个类的相关知识还是很有必要的。接下来就让我们带着以下三个疑问去看下面的内容
- PersistentTopic在什么时候会被创建?都有哪些重要的成员变量?
- PersistentTopic的创建流程都会发生什么?
- 上面那些相关操作大概是什么实现的?
一、何时创建
以下列出创建的时机,基本上流程都是发起创建Topic的时候会通过一致性哈希计算出这个Topic所归属的Bundle,然后去zookeeper获取这个Bundle所归属的Broker机器,最后请求这台Broker节点创建对应的PersistentTopic对象
- 管理流创建
- cli管理命令行
- 多语言Client
- Http方式
- 写入流创建
- 生产者写入流
- 消费者写入流
接下来看看这个类的主要成员变量,重要的一些已经加上注释解释了
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方法
初始化方法的逻辑相比下要丰富写,归纳起来有以下四点
-
获取压缩服务
通过双重判断获取单例TwoPhaseCompactor进行压缩,有专门的线程池进行处理压缩动作
-
遍历游标恢复订阅的状态
当Topic重新分配后,在新的Broker中要根据游标恢复订阅、跨集群复制的状态,这样才能让客户端“无感”
-
跨集群复制
遍历这个Topic的游标,如果游标中存在跨集群复制游标则创建对应的GeoPersistentReplicator对象进行消息的复制
-
更新配置
- 更新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类的重要性、聊聊它都负责哪些事情以及大概实现,具体每个细节展开都能单独写一篇文章,这个敬请期待~