Kafka-Topic创建源码分析
在kafka中,创建topic通过使用kafka-topics.sh脚本或者直接调用AdminClient对外提供的adminApi来进行创建.
即使是使用kafka-topics.sh,其最终会通过生成并调用AdminClient来进行处理.
0,创建topic流程图

1,创建topic示例代码
通过引入AdminClient利用代码实现创建topic的示例代码.
//配置brokerServer的链接地址(`bootstrap.servers`):`host1:port,host2:port`
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
//`Admin.create(props)`:生成`KafkaAdminClient`实例.
//==>jdk8以后可用try来自动执行close
try (Admin admin = Admin.create(props)) {
String topicName = "test-topic";
int partitions = 12;
short replicationFactor = 2;
NewTopic newTopic = new NewTopic(topicName, partitions, replicationFactor).configs(
//配置topic的log的日志清理策略(适合于带有key,value消息的topic)
Collections.singletonMap(TopicConfig.CLEANUP_POLICY_CONFIG, TopicConfig.CLEANUP_POLICY_COMPACT)
);
//向brokerServer发起创建topic的请求(`CreateTopicsRequest`).
CreateTopicsResult result = admin.createTopics(Collections.singleton(newTopic));
//等待response的响应结果.
result.all.get();
}
2,AdminClient处理流程
2,1,Admin.create
Admin.create函数主要用于生成用于与broker节点进行通信的KafkaAdminClient实例.
static Admin create(Properties props) {
return KafkaAdminClient.createInternal(new AdminClientConfig(props, true), null);
}
从Admin.create的实现代码可以看到,其直接调用了KafkaAdminClient内部的createInternal函数来完成adminClient的初始化.
createInternal函数处理流程
Step=>1
生成用于管理client端metadataCache的AdminMetadataManager实例,实例初始化时依赖配置项:
retry.backoff.ms 默认值100ms,当metadata相关请求失败后,下一次重试的backOff时间.
metadata.max.age.ms 默认值5分钟,
client端metadataCache的过期时间,当metadata上一次更新时间超过这个值后,会重新请求获取新的metadata.
此时,其内部默认的updater实例默认为AdminMetadataUpdater.
//初始化AdminMetadataManager实例,其`metadataUpdate`.
AdminMetadataManager metadataManager = new AdminMetadataManager(logContext,
config.getLong(AdminClientConfig.RETRY_BACKOFF_MS_CONFIG),
config.getLong(AdminClientConfig.METADATA_MAX_AGE_CONFIG));
Step=>2
根据bootstrap.servers配置的brokers列表,初始化生成Cluster实例,并添加到metadataManager实例中.
此时: Cluster实例内部的isBootstrapConfigured属性默认值为true
当isBootstrapConfigured值为true时,表示metadataManager处于未准备好的状态.并同时设置metadataManager实例的state为QUIESCENT,
List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(
config.getList(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG),
config.getString(AdminClientConfig.CLIENT_DNS_LOOKUP_CONFIG));
//将初始化的Cluster设置到`metadataManager`中,并设置其`state`为`QUIESCENT`状态.
metadataManager.update(Cluster.bootstrap(addresses), time.milliseconds());
Step=>3
生成用于与broker进行网络通信的NetworkClient通道,其对应的metadataUpdater实例为AdminMetadataUpdater.
channelBuilder = ClientUtils.createChannelBuilder(config, time, logContext);
selector = new Selector(config.getLong(AdminClientConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG),
metrics, time, metricGrpPrefix, channelBuilder, logContext);
//生成`NetworkClient`实例,其对应的`medataUpdater`实例为`AdminMetadataUpdater`.
networkClient = new NetworkClient(
metadataManager.updater(),null,selector,clientId,1,
config.getLong(AdminClientConfig.RECONNECT_BACKOFF_MS_CONFIG),
config.getLong(AdminClientConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG),
config.getInt(AdminClientConfig.SEND_BUFFER_CONFIG),
config.getInt(AdminClientConfig.RECEIVE_BUFFER_CONFIG),
(int) TimeUnit.HOURS.toMillis(1),
config.getLong(AdminClientConfig.SOCKET_CONNECTION_SETUP_TIMEOUT_MS_CONFIG),
config.getLong(AdminClientConfig.SOCKET_CONNECTION_SETUP_TIMEOUT_MAX_MS_CONFIG),
time,true,apiVersions,null,logContext,
(hostResolver == null) ? new DefaultHostResolver() : hostResolver);
Step=>4
最后: 根据以上三歩初始化的信息,生成KafkaAdminClient实例.
在adminClient端,其clientId的值默认为adminclient-number这样的值.
在初始化KafkaAdminClient时,会同时初始化启动其内部的AdminClientRunnable线程,此线程用于处理与broker的通信.
return new KafkaAdminClient(config, clientId, time, metadataManager, metrics, networkClient,
timeoutProcessorFactory, logContext);
2,2,MetadataRequest
当KafkaAdminClient实例初始化后,其内部的AdminClientRunnableIO线程被启动,
此时在执行processRequests函数时会生成MetadataRequest请求,并立即向broker节点请求获取集群的metadata信息.
在KafkaAdminClient实例刚初始化完成时,
metadataManager中lastMetadataUpdateMs与lastMetadataFetchAttemptMs属性默认值为0,
因此:metadataManager.metadataFetchDelayMs(now)得到的delay时间为0,此时将执行makeMetadataCall生成MetadataRequest.
同时通过maybeDrainPendingCall立即发起请求.
processRequests函数中判断并发起Metdata请求的代码片段:
pollTimeout = Math.min(pollTimeout, maybeDrainPendingCalls(now));
//判断当前metadata是否过期,初始化实例时metadataFetchDelayMs函数返回0,表示过期需要重新获取metadata.
long metadataFetchDelayMs = metadataManager.metadataFetchDelayMs(now);
if (metadataFetchDelayMs == 0) {
metadataManager.transitionToUpdatePending(now);
//生成`MetadataRequest`请求实例,此时将向`bootstrap.servers`配置的broker节点的随机一个节点发起请求.
Call metadataCall = makeMetadataCall(now);
//将获取metadata信息的`Call`实例添加到`callsToSend`队列中.
if (!maybeDrainPendingCall(metadataCall, now))
pendingCalls.add(metadataCall);
}
//执行sendEligibleCalls函数,发起`MetadataRequest`请求.
pollTimeout = Math.min(pollTimeout, sendEligibleCalls(now));
Step=>1
metadataManager.transitionToUpdatePending(now) 将metadataManager对应的state的状态设置为UPDATE_PENDING.
Step=>2
makeMetadataCall函数,生成用于处理MetadataRequest的Call实例.
**==>**此Call实例对应的nodeProvider实现为MetadataUpdateNodeIdProvider.
其生成MetadataRequest参数的实现代码:
public MetadataRequest.Builder createRequest(int timeoutMs) {
return new MetadataRequest.Builder(new MetadataRequestData()
.setTopics(Collections.emptyList())
.setAllowAutoTopicCreation(true));
}
Step=>3
maybeDrainPendingCall函数,通过MetadataUpdateNodeIdProvider随机获取bootstrap.servers中的一个broker节点,发起Metadata请求.
//根据`MetadataUpdateNodeIdProvider`随机获取一个broker节点.
//==>同时把metadata请求信息添加到`callsToSend`队列中.
Node node = call.nodeProvider.provide();
if (node != null) {
log.trace("Assigned {} to node {}", call, node);
call.curNode = node;
getOrCreateListValue(callsToSend, node).add(call);
return true;
}
Step=>4
执行sendEligibleCalls函数,把callsToSend队列中已经准备好的请求发送到目标broker节点.
在初始化时此队列中默认只有MetadataRequest请求,因此此时只是向目标节点发起获取Metadata请求.
此时:,发起的MetadataRequest请求将由broker端的KafkaApis中的handleTopicMetadataRequest处理程序进行处理.
此请求在broker端将返回当前metadataImage中所有的activeBrokers节点信息,
并从activeBrokers中随机获取一个节点做为Controller节点.
Step=>5
当broker端接收并完成对MetadataRequest请求的处理后进行响应的response将由makeMetadataCall生成的Call实例处理.
=>1, 根据Metadata请求响应的activeBrokers与controller节点信息,重新生成Cluster实例,
此时生成的Cluster实例对应的isBootstrapConfigured属性的值为false(表示metadata已经准备好).
=>2,执行metadataManager的update函数,把最新生成的Cluster更新到metadataManager实例中.
此时:metadataManager对应的lastMetadataUpdateMs属性(metadata最后更新时间)将被更新为当前时间.
public void handleResponse(AbstractResponse abstractResponse) {
MetadataResponse response = (MetadataResponse) abstractResponse;
long now = time.milliseconds();
metadataManager.update(response.buildCluster(), now);
// Unassign all unsent requests after a metadata refresh to allow for a new
// destination to be selected from the new metadata
unassignUnsentCalls(node -> true);
}
2,3,createTopics
发起请求
从示例代码中可以看到,当利用AdminClient来创建topic时,在生成NewTopic实例后,会最终调用KafkaAdminClient.createTopics来进行处理.
createTopics代码实现:
=>1,先迭代传入的newTopics容器,取出每一个NewTopic实例,
根据其自定义配置,topicName,numPartitions以及replicationFactor生成对应的CreatableTopic实例.
=>2,根据新生成的CreatableTopic的容器(一到多个),通过调用getCreateTopicsCall生成用于网络请求的Call实例.此时:
a,Call实例用于查找请求的目标节点的nodeProvider实例为ControllerNodeProvider(metadata必须初始化,有对应的controller节点).
b,生成CreateTopicsRequest请求与其对应的响应处理程序handleResponse(由Call实例定义).
=>3,将新生成用于发起CreateTopicsRequest请求的Call添加到IO线程的newCalls队列中.
public CreateTopicsResult createTopics(final Collection<NewTopic> newTopics,
final CreateTopicsOptions options) {
//生成topic创建是否成功的future监听,client端可通过对函数调用的返回值来监听创建的成功失败.
final Map<String, KafkaFutureImpl<TopicMetadataAndConfig>> topicFutures = new HashMap<>(newTopics.size());
final CreatableTopicCollection topics = new CreatableTopicCollection();
//迭代传入的`NewTopic`集合参数,检查topicName是否为空,
//==>并根据其`name`,`numPartitions`,`replicationFactor`等配置信息生成`CreatableTopic`添加到`topics`集合中.
for (NewTopic newTopic : newTopics) {
if (topicNameIsUnrepresentable(newTopic.name())) {
KafkaFutureImpl<TopicMetadataAndConfig> future = new KafkaFutureImpl<>();
future.completeExceptionally(new InvalidTopicException("The given topic name '" +
newTopic.name() + "' cannot be represented in a request."));
topicFutures.put(newTopic.name(), future);
} else if (!topicFutures.containsKey(newTopic.name())) {
topicFutures.put(newTopic.name(), new KafkaFutureImpl<>());
topics.add(newTopic.convertToCreatableTopic());
}
}
//如果校验并转换为`CreatableTopic`的信息不为空,生成发起`CreateTopicsRequest`请求的`Call`实例.
//==>并添加到runnable线程的`newCalls`队列中.
if (!topics.isEmpty()) {
final long now = time.milliseconds();
final long deadline = calcDeadlineMs(now, options.timeoutMs());
final Call call = getCreateTopicsCall(options, topicFutures, topics,
Collections.emptyMap(), now, deadline);
runnable.call(call, now);
}
return new CreateTopicsResult(new HashMap<>(topicFutures));
}
此时:runnableIO线程(AdminClientRunnable)在processRequests处理时的执行顺序:
=>1, drainNewCalls:将newCalls队列中新生成的Request移动到pendingCalls队列中.
=>2, maybeDrainPendingCalls: 将pendingCalls队列的Request移动到callsToSend队列中(根据Call.nodeProvide查找到node).
=>3,sendEligibleCalls: callsToSend队列中的Request发起网络请求,向指定的目标节点.
此时,请求将由broker端接收到后,会被ForwardingManager组件包装为EnvelopeRequest后,直接转发给activeController进行处理.
通过ForwardingManager中的BrokerToControllerChannelManager 向activeController转发请求.
即: 请求最终将由Controler中的ReplicationControlManager组件来进行CreateTopicsRequest处理.
在controller中ReplicationControlManager组件用于管理集群的topic信息以及各ISR与partitionLeader的平衡.
并在向metadata中写入TopicRecord与PartitionRecord 消息后,由所有kafkaServer节点进行replay来执行真正意义上的创建工作.
同时broker端由到activeController的response后,再重新把response转发给client端.
接收响应
在broker端接收到activeController的响应后,会将response转发给client端,此时:
将由getCreateTopicsCall生成的Call实例的handleResponse来进行处理.
其处理response的代码片段:
ApiError error = new ApiError(result.errorCode(), result.errorMessage());
if (error.isFailure()) {
if (error.is(Errors.THROTTLING_QUOTA_EXCEEDED)) {
ThrottlingQuotaExceededException quotaExceededException = new ThrottlingQuotaExceededException(
response.throttleTimeMs(), error.messageWithFallback());
if (options.shouldRetryOnQuotaViolation()) {
retryTopics.add(topics.find(result.name()).duplicate());
retryTopicQuotaExceededExceptions.put(result.name(), quotaExceededException)

本文详细分析了Kafka中创建Topic的源码流程,从AdminClient的处理开始,包括MetadataRequest、createTopics的过程,再到Controller的Topic创建处理,以及Broker如何处理Topic创建。文中介绍了每个阶段的关键步骤和组件交互,如ReplicationControlManager的副本分配策略,并探讨了不同场景下的副本分配情况。最后,详细阐述了Broker如何通过MetadataDelta和ReplicaManager来处理Topic创建的相关操作,如applyDelta和applyLocalLeadersDelta等。
最低0.47元/天 解锁文章
473

被折叠的 条评论
为什么被折叠?



