//调用adminClient创建Topic
val createResult = adminClient.createTopics(Collections.singleton(newTopic))
createResult.all().get()
println(s"Created topic ${topic.name}.")
} else {
throw new IllegalArgumentException(s"Topic ${topic.name} already exists")
}
}
-
检查各项入参是否有问题
-
adminClient.listTopics()
,然后比较是否已经存在待创建的Topic;如果存在抛出异常; -
判断是否配置了参数
--replica-assignment
; 如果配置了,那么Topic就会按照指定的方式来配置副本情况 -
解析配置
--config
配置放到configsMap
中;configsMap
给到NewTopic
对象 -
调用
adminClient.createTopics
创建Topic; 它是如何创建Topic的呢?往下分析源码
3.1 KafkaAdminClient.createTopics(NewTopic) 创建Topic
@Override
public CreateTopicsResult createTopics(final Collection newTopics,
final CreateTopicsOptions options) {
//省略部分源码…
Call call = new Call(“createTopics”, calcDeadlineMs(now, options.timeoutMs()),
new ControllerNodeProvider()) {
@Override
public CreateTopicsRequest.Builder createRequest(int timeoutMs) {
return new CreateTopicsRequest.Builder(
new CreateTopicsRequestData().
setTopics(topics).
setTimeoutMs(timeoutMs).
setValidateOnly(options.shouldValidateOnly()));
}
@Override
public void handleResponse(AbstractResponse abstractResponse) {
//省略
}
@Override
void handleFailure(Throwable throwable) {
completeAllExceptionally(topicFutures.values(), throwable);
}
};
}
这个代码里面主要看下Call里面的接口; 先不管Kafka如何跟服务端进行通信的细节; 我们主要关注创建Topic的逻辑;
createRequest
会构造一个请求参数CreateTopicsRequest
例如下图
- 选择ControllerNodeProvider这个节点发起网络请求
可以清楚的看到, 创建Topic这个操作是需要Controller来执行的;
4. 发起网络请求
5. Controller角色的服务端接受请求处理逻辑
首先找到服务端处理客户端请求的 源码入口 ⇒ KafkaRequestHandler.run()
主要看里面的 apis.handle(request)
方法; 可以看到客户端的请求都在request.bodyAndSize()
里面
5.1 KafkaApis.handle(request) 根据请求传递Api调用不同接口
进入方法可以看到根据request.header.apiKey
调用对应的方法,客户端传过来的是CreateTopics
5.2 KafkaApis.handleCreateTopicsRequest 处理创建Topic的请求
def handleCreateTopicsRequest(request: RequestChannel.Request): Unit = {
// 部分代码省略
//如果当前Broker不是属于Controller的话,就抛出异常
if (!controller.isActive) {
createTopicsRequest.data.topics.asScala.foreach { topic =>
results.add(new CreatableTopicResult().setName(topic.name).
setErrorCode(Errors.NOT_CONTROLLER.code))
}
sendResponseCallback(results)
} else {
// 部分代码省略
}
adminManager.createTopics(createTopicsRequest.data.timeoutMs,
createTopicsRequest.data.validateOnly,
toCreate,
authorizedForDescribeConfigs,
handleCreateTopicsResults)
}
}
-
判断当前处理的broker是不是Controller,如果不是Controller的话直接抛出异常,从这里可以看出,CreateTopic这个操作必须是Controller来进行, 出现这种情况有可能是客户端发起请求的时候Controller已经变更;
-
调用
adminManager.createTopics()
5.3 adminManager.createTopics()
创建主题并等等主题完全创建,回调函数将会在超时、错误、或者主题创建完成时触发
该方法过长,省略部分代码
def createTopics(timeout: Int,
validateOnly: Boolean,
toCreate: Map[String, CreatableTopic],
includeConfigsAndMetatadata: Map[String, CreatableTopicResult],
responseCallback: Map[String, ApiError] => Unit): Unit = {
// 1. map over topics creating assignment and calling zookeeper
val brokers = metadataCache.getAliveBrokers.map { b => kafka.admin.BrokerMetadata(b.id, b.rack) }
val metadata = toCreate.values.map(topic =>
try {
//省略部分代码
//检查Topic是否存在
//检查 --replica-assignment参数和 (–partitions || --replication-factor ) 不能同时使用
// 如果(–partitions || --replication-factor ) 没有设置,则使用 Broker的配置(这个Broker肯定是Controller)
// 计算分区副本分配方式
createTopicPolicy match {
case Some(policy) =>
//省略部分代码
adminZkClient.validateTopicCreate(topic.name(), assignments, configs)
if (!validateOnly)
adminZkClient.createTopicWithAssignment(topic.name, configs, assignments)
case None =>
if (validateOnly)
//校验创建topic的参数准确性
adminZkClient.validateTopicCreate(topic.name, assignments, configs)
else
//把topic相关数据写入到zk中
adminZkClient.createTopicWithAssignment(topic.name, configs, assignments)
}
}
- 做一些校验检查
①.检查Topic是否存在
②. 检查--replica-assignment
参数和 (--partitions || --replication-factor
) 不能同时使用
③.如果(--partitions || --replication-factor
) 没有设置,则使用 Broker的配置(这个Broker肯定是Controller)
④.计算分区副本分配方式
-
createTopicPolicy
根据Broker是否配置了创建Topic的自定义校验策略; 使用方式是自定义实现org.apache.kafka.server.policy.CreateTopicPolicy
接口;并 在服务器配置create.topic.policy.class.name=自定义类
; 比如我就想所有创建Topic的请求分区数都要大于10; 那么这里就可以实现你的需求了 -
createTopicWithAssignment
把topic相关数据写入到zk中; 进去分析一下
5.4 写入zookeeper数据
我们进入到adminZkClient.createTopicWithAssignment(topic.name, configs, assignments)
看看有哪些数据写入到了zk中;
def createTopicWithAssignment(topic: String,
config: Properties,
partitionReplicaAssignment: Map[Int, Seq[Int]]): Unit = {
validateTopicCreate(topic, partitionReplicaAssignment, config)
// 将topic单独的配置写入到zk中
zkClient.setOrCreateEntityConfigs(ConfigType.Topic, topic, config)
// 将topic分区相关信息写入zk中
writeTopicPartitionAssignment(topic, partitionReplicaAssignment.mapValues(ReplicaAssignment(_)).toMap, isUpdate = false)
}
源码就不再深入了,这里直接详细说明一下
写入Topic配置信息
- 先调用
SetDataRequest
请求往节点/config/topics/Topic名称
写入数据; 这里
一般这个时候都会返回 NONODE (NoNode)
;节点不存在; 假如zk已经存在节点就直接覆盖掉
- 节点不存在的话,就发起
CreateRequest
请求,写入数据; 并且节点类型是持久节点
这里写入的数据,是我们入参时候传的topic配置--config
; 这里的配置会覆盖默认配置
写入Topic分区副本信息
- 将已经分配好的副本分配策略写入到
/brokers/topics/Topic名称
中; 节点类型 持久节点
具体跟zk交互的地方在
ZookeeperClient.send()
这里包装了很多跟zk的交互;
6. Controller监听 /brokers/topics/Topic名称
, 通知Broker将分区写入磁盘
Controller 有监听zk上的一些节点; 在上面的流程中已经在zk中写入了
/brokers/topics/Topic名称
; 这个时候Controller就监听到了这个变化并相应;
KafkaController.processTopicChange
private def processTopicChange(): Unit = {
//如果处理的不是Controller角色就返回
if (!isActive) return
//从zk中获取 `/brokers/topics 所有Topic
val topics = zkClient.getAllTopicsInCluster
//找出哪些是新增的
val newTopics = topics – controllerContext.allTopics
//找出哪些Topic在zk上被删除了
val deletedTopics = controllerContext.allTopics – topics
controllerContext.allTopics = topics
registerPartitionModificationsHandlers(newTopics.toSeq)
val addedPartitionReplicaAssignment = zkClient.getFullReplicaAssignmentForTopics(newTopics)
deletedTopics.foreach(controllerContext.removeTopic)
addedPartitionReplicaAssignment.foreach {
case (topicAndPartition, newReplicaAssignment) => controllerContext.updatePartitionFullReplicaAssignment(topicAndPartition, newReplicaAssignment)
}
info(s"New topics: [ n e w T o p i c s ] , d e l e t e d t o p i c s : [ newTopics], deleted topics: [ newTopics],deletedtopics:[deletedTopics], new partition replica assignment " +
s"[$addedPartitionReplicaAssignment]")
if (addedPartitionReplicaAssignment.nonEmpty)
onNewPartitionCreation(addedPartitionReplicaAssignment.keySet)
}
-
从zk中获取
/brokers/topics
所有Topic跟当前Broker内存中所有BrokercontrollerContext.allTopics
的差异; 就可以找到我们新增的Topic; 还有在zk中被删除了的Broker(该Topic会在当前内存中remove掉) -
从zk中获取
/brokers/topics/{TopicName}
给定主题的副本分配。并保存在内存中 -
执行
onNewPartitionCreation
;分区状态开始流转
6.1 onNewPartitionCreation 状态流转
关于Controller的状态机 详情请看: 【kafka源码】Controller中的状态机
/**
-
This callback is invoked by the topic change callback with the list of failed brokers as input.
-
It does the following -
-
- Move the newly created partitions to the NewPartition state
-
- Move the newly created partitions from NewPartition->OnlinePartition state
*/
private def onNewPartitionCreation(newPartitions: Set[TopicPartition]): Unit = {
info(s"New partition creation callback for ${newPartitions.mkString(“,”)}")
partitionStateMachine.handleStateChanges(newPartitions.toSeq, NewPartition)
replicaStateMachine.handleStateChanges(controllerContext.replicasForPartition(newPartitions).toSeq, NewReplica)
partitionStateMachine.handleStateChanges(
newPartitions.toSeq,
OnlinePartition,
Some(OfflinePartitionLeaderElectionStrategy(false))
)
replicaStateMachine.handleStateChanges(controllerContext.replicasForPartition(newPartitions).toSeq, OnlineReplica)
}
- 将待创建的分区状态流转为
NewPartition
;
- 将待创建的副本 状态流转为
NewReplica
;
- 将分区状态从刚刚的
NewPartition
流转为OnlinePartition
0. 获取leaderIsrAndControllerEpochs
; Leader为副本的第一个;
1. 向zk中写入/brokers/topics/{topicName}/partitions/
持久节点; 无数据
2. 向zk中写入/brokers/topics/{topicName}/partitions/{分区号}
持久节点; 无数据
3. 向zk中写入/brokers/topics/{topicName}/partitions/{分区号}/state
持久节点; 数据为leaderIsrAndControllerEpoch
-
向副本所属Broker发送
leaderAndIsrRequest
请求 -
向所有Broker发送
UPDATE_METADATA
请求 -
将副本状态从刚刚的
NewReplica
流转为OnlineReplica
,更新下内存
关于分区状态机和副本状态机详情请看【kafka源码】Controller中的状态机
7. Broker收到LeaderAndIsrRequest 创建本地Log
上面步骤中有说到向副本所属Broker发送
leaderAndIsrRequest
请求,那么这里做了什么呢
其实主要做的是 创建本地Log
代码太多,这里我们直接定位到只跟创建Topic相关的关键代码来分析
KafkaApis.handleLeaderAndIsrRequest->replicaManager.becomeLeaderOrFollower->ReplicaManager.makeLeaders...LogManager.getOrCreateLog
/**
- 如果日志已经存在,只返回现有日志的副本否则如果 isNew=true 或者如果没有离线日志目录,则为给定的主题和给定的分区创建日志 否则抛出 KafkaStorageException
*/
def getOrCreateLog(topicPartition: TopicPartition, config: LogConfig, isNew: Boolean = false, isFuture: Boolean = false): Log = {
logCreationOrDeletionLock synchronized {
getLog(topicPartition, isFuture).getOrElse {
// create the log if it has not already been created in another thread
if (!isNew && offlineLogDirs.nonEmpty)
throw new KafkaStorageException(s"Can not create log for $topicPartition because log directories ${offlineLogDirs.mkString(“,”)} are offline")
val logDirs: List[File] = {
val preferredLogDir = preferredLogDirs.get(topicPartition)
if (isFuture) {
if (preferredLogDir == null)
throw new IllegalStateException(s"Can not create the future log for $topicPartition without having a preferred log directory")
else if (getLog(topicPartition).get.dir.getParent == preferredLogDir)
throw new IllegalStateException(s"Can not create the future log for $topicPartition in the current log directory of this partition")
}
if (preferredLogDir != null)
List(new File(preferredLogDir))
else
nextLogDirs()
}
val logDirName = {
if (isFuture)
Log.logFutureDirName(topicPartition)
else
Log.logDirName(topicPartition)
}
val logDir = logDirs
.toStream // to prevent actually mapping the whole list, lazy map
.map(createLogDirectory(_, logDirName))
.find(_.isSuccess)
.getOrElse(Failure(new KafkaStorageException(“No log directories available. Tried " + logDirs.map(_.getAbsolutePath).mkString(”, "))))
.get // If Failure, will throw
val log = Log(
dir = logDir,
config = config,
logStartOffset = 0L,
recoveryPoint = 0L,
maxProducerIdExpirationMs = maxPidExpirationMs,
producerIdExpirationCheckIntervalMs = LogManager.ProducerIdExpirationCheckIntervalMs,
scheduler = scheduler,
time = time,
brokerTopicStats = brokerTopicStats,
logDirFailureChannel = logDirFailureChannel)
if (isFuture)
futureLogs.put(topicPartition, log)
else
currentLogs.put(topicPartition, log)
info(s"Created log for partition $topicPartition in KaTeX parse error: Expected '}', got 'EOF' at end of input: …perties " + s"{{config.originals.asScala.mkString(", “)}}.”)
// Remove the preferred log dir since it has already been satisfied
preferredLogDirs.remove(topicPartition)
log
}
}
}
- 如果日志已经存在,只返回现有日志的副本否则如果 isNew=true 或者如果没有离线日志目录,则为给定的主题和给定的分区创建日志 否则抛出
KafkaStorageException
详细请看 【kafka源码】LeaderAndIsrRequest请求
如果上面的源码分析,你不想看,那么你可以直接看这里的简洁叙述
- 根据是否有传入参数
--zookeeper
来判断创建哪一种 对象topicService
如果传入了--zookeeper
则创建 类 ZookeeperTopicService
的对象
否则创建类AdminClientTopicService
的对象(我们主要分析这个对象)
-
如果有入参
--command-config
,则将这个文件里面的参数都放到mapl类型commandConfig
里面, 并且也加入bootstrap.servers
的参数;假如配置文件里面已经有了bootstrap.servers
配置,那么会将其覆盖 -
将上面的
commandConfig
作为入参调用Admin.create(commandConfig)
创建 Admin; 这个时候调用的Client模块的代码了, 从这里我们就可以猜测,我们调用kafka-topic.sh
脚本实际上是kafka模拟了一个客户端Client来创建Topic的过程; -
一些异常检查
①.如果配置了副本副本数–replication-factor 一定要大于0
②.如果配置了–partitions 分区数 必须大于0
③.去zk查询是否已经存在该Topic
-
判断是否配置了参数
--replica-assignment
; 如果配置了,那么Topic就会按照指定的方式来配置副本情况 -
解析配置
--config
配置放到configsMap
中; configsMap给到NewTopic对象 -
将上面所有的参数包装成一个请求参数
CreateTopicsRequest
;然后找到是Controller
的节点发起请求(ControllerNodeProvider
) -
服务端收到请求之后,开始根据
CreateTopicsRequest
来调用创建Topic的方法; 不过首先要判断一下自己这个时候是不是Controller
; 有可能这个时候Controller重新选举了; 这个时候要抛出异常 -
服务端进行一下请求参数检查
①.检查Topic是否存在
②.检查 --replica-assignment
参数和 (--partitions
|| --replication-factor
) 不能同时使用
-
如果(
--partitions
||--replication-factor
) 没有设置,则使用 Broker的默认配置(这个Broker肯定是Controller) -
计算分区副本分配方式;如果是传入了
--replica-assignment
;则会安装自定义参数进行组装;否则的话系统会自动计算分配方式; 具体详情请看 【kafka源码】创建Topic的时候是如何分区和副本的分配规则 -
createTopicPolicy
根据Broker是否配置了创建Topic的自定义校验策略; 使用方式是自定义实现org.apache.kafka.server.policy.CreateTopicPolicy
接口;并 在服务器配置create.topic.policy.class.name
=自定义类; 比如我就想所有创建Topic的请求分区数都要大于10; 那么这里就可以实现你的需求了 -
zk中写入Topic配置信息 发起
CreateRequest
请求,这里写入的数据,是我们入参时候传的topic配置--config
; 这里的配置会覆盖默认配置;并且节点类型是持久节点;path =/config/topics/Topic名称
-
zk中写入Topic分区副本信息 发起
CreateRequest
请求 ,将已经分配好的副本分配策略 写入到/brokers/topics/Topic名称
中; 节点类型 持久节点 -
Controller
监听zk上面的topic信息; 根据zk上变更的topic信息;计算出新增/删除了哪些Topic; 然后拿到新增Topic的 副本分配信息; 并做一些状态流转 -
向新增Topic所在Broker发送
leaderAndIsrRequest
请求, -
Broker收到
发送leaderAndIsrRequest请求
; 创建副本Log文件;
创建Topic的时候 在Zk上创建了哪些节点
接受客户端请求阶段:
- topic的配置信息
/config/topics/Topic名称
持久节点
- topic的分区信息
/brokers/topics/Topic名称
持久节点
Controller监听zk节点
/brokers/topics
变更阶段
/brokers/topics/{topicName}/partitions/
持久节点; 无数据
- 向zk中写入
/brokers/topics/{topicName}/partitions/{分区号}
持久节点; 无数据
- 向zk中写入
/brokers/topics/{topicName}/partitions/{分区号}/state
持久节点;
创建Topic的时候 什么时候在Broker磁盘上创建的日志文件
当Controller监听zk节点
/brokers/topics
变更之后,将新增的Topic 解析好的分区状态流转
NonExistentPartition
->NewPartition
->OnlinePartition
当流转到OnlinePartition
的时候会像分区分配到的Broker发送一个leaderAndIsrRequest
请求,当Broker们收到这个请求之后,根据请求参数做一些处理,其中就包括检查自身有没有这个分区副本的本地Log;如果没有的话就重新创建;
如果我没有指定分区数或者副本数,那么会如何创建
我们都知道,如果我们没有指定分区数或者副本数, 则默认使用Broker的配置, 那么这么多Broker,假如不小心默认值配置不一样,那究竟使用哪一个呢? 那肯定是哪台机器执行创建topic的过程,就是使用谁的配置;
所以是谁执行的? 那肯定是Controller啊! 上面的源码我们分析到了,创建的过程,会指定Controller这台机器去进行;
如果我手动删除了/brokers/topics/
下的某个节点会怎么样?
如果我手动在zk中添加/brokers/topics/{TopicName}
节点会怎么样
先说结论: 根据上面分析过的源码画出的时序图可以指定; 客户端发起创建Topic的请求,本质上是去zk里面写两个数据
- topic的配置信息
/config/topics/Topic名称
持久节点
- topic的分区信息
/brokers/topics/Topic名称
持久节点
所以我们绕过这一步骤直接去写入数据,可以达到一样的效果;不过我们的数据需要保证准确
因为在这一步已经没有了一些基本的校验了; 假如这一步我们写入的副本Brokerid不存在会怎样,从时序图中可以看到,`leaderAndIsrRequest请求`; 就不会正确的发送的不存在的BrokerId上,那么那台机器就不会创建Log文件;
下面不妨让我们来验证一下;
创建一个节点
/brokers/topics/create_topic_byhand_zk
节点数据为下面数据;
{“version”:2,“partitions”:{“2”:[3],“1”:[3],“0”:[3]},“adding_replicas”:{},“removing_replicas”:{}}
这里我用的工具
PRETTYZOO
手动创建的,你也可以用命令行创建;
创建完成之后我们再看看本地有没有生成一个Log文件
可以看到我们指定的Broker,已经生成了对应的分区副本Log文件;
而且zk中也写入了其他的数据
在我们写入zk数据的时候,就已经确定好了哪个每个分区的Leader是谁了,那就是第一个副本默认为Leader
如果写入/brokers/topics/{TopicName}
节点之后Controller挂掉了会怎么样
先说结论:Controller 重新选举的时候,会有一些初始化的操作; 会把创建过程继续下去
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/49ac6e155422dbdb32b2aedd3581ba64.jpeg)
最后
经过日积月累, 以下是小编归纳整理的深入了解Java虚拟机文档,希望可以帮助大家过关斩将顺利通过面试。
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。
由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
[外链图片转存中…(img-NeyuEQDj-1713398260491)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/49ac6e155422dbdb32b2aedd3581ba64.jpeg)
最后
经过日积月累, 以下是小编归纳整理的深入了解Java虚拟机文档,希望可以帮助大家过关斩将顺利通过面试。
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。
[外链图片转存中…(img-4e5Tdwmx-1713398260491)]
[外链图片转存中…(img-DQHeXDnq-1713398260491)]
[外链图片转存中…(img-Mey47mRR-1713398260492)]
[外链图片转存中…(img-HwHZuDGG-1713398260492)]
[外链图片转存中…(img-vilxSeIH-1713398260492)]
[外链图片转存中…(img-Yo7XWTeR-1713398260492)]
[外链图片转存中…(img-Ck9f7MUR-1713398260492)]
由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!