【kafka源码】Topic的创建源码分析(附视频),字节跳动正式启动2024届秋季校招

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)

}

}

  1. 做一些校验检查

①.检查Topic是否存在

②. 检查--replica-assignment参数和 (--partitions || --replication-factor ) 不能同时使用

③.如果(--partitions || --replication-factor ) 没有设置,则使用 Broker的配置(这个Broker肯定是Controller)

④.计算分区副本分配方式

  1. createTopicPolicy 根据Broker是否配置了创建Topic的自定义校验策略; 使用方式是自定义实现org.apache.kafka.server.policy.CreateTopicPolicy接口;并 在服务器配置 create.topic.policy.class.name=自定义类; 比如我就想所有创建Topic的请求分区数都要大于10; 那么这里就可以实现你的需求了

  2. 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配置信息

  1. 先调用SetDataRequest请求往节点/config/topics/Topic名称 写入数据; 这里

一般这个时候都会返回 NONODE (NoNode);节点不存在; 假如zk已经存在节点就直接覆盖掉

  1. 节点不存在的话,就发起CreateRequest请求,写入数据; 并且节点类型是持久节点

这里写入的数据,是我们入参时候传的topic配置--config; 这里的配置会覆盖默认配置

写入Topic分区副本信息

  1. 将已经分配好的副本分配策略写入到 /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)

}

  1. 从zk中获取 /brokers/topics 所有Topic跟当前Broker内存中所有BrokercontrollerContext.allTopics的差异; 就可以找到我们新增的Topic; 还有在zk中被删除了的Broker(该Topic会在当前内存中remove掉)

  2. 从zk中获取/brokers/topics/{TopicName} 给定主题的副本分配。并保存在内存中在这里插入图片描述

  3. 执行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 -

    1. Move the newly created partitions to the NewPartition state
    1. 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)

}

  1. 将待创建的分区状态流转为NewPartition;

在这里插入图片描述

  1. 将待创建的副本 状态流转为NewReplica;

在这里插入图片描述

  1. 将分区状态从刚刚的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在这里插入图片描述

  1. 向副本所属Broker发送leaderAndIsrRequest请求

  2. 向所有Broker发送UPDATE_METADATA 请求

  3. 将副本状态从刚刚的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

}

}

}

  1. 如果日志已经存在,只返回现有日志的副本否则如果 isNew=true 或者如果没有离线日志目录,则为给定的主题和给定的分区创建日志 否则抛出KafkaStorageException

详细请看 【kafka源码】LeaderAndIsrRequest请求

源码总结


如果上面的源码分析,你不想看,那么你可以直接看这里的简洁叙述

  1. 根据是否有传入参数--zookeeper 来判断创建哪一种 对象topicService

如果传入了--zookeeper 则创建 类 ZookeeperTopicService的对象

否则创建类AdminClientTopicService的对象(我们主要分析这个对象)

  1. 如果有入参--command-config ,则将这个文件里面的参数都放到mapl类型 commandConfig里面, 并且也加入bootstrap.servers的参数;假如配置文件里面已经有了bootstrap.servers配置,那么会将其覆盖

  2. 将上面的commandConfig作为入参调用Admin.create(commandConfig)创建 Admin; 这个时候调用的Client模块的代码了, 从这里我们就可以猜测,我们调用kafka-topic.sh脚本实际上是kafka模拟了一个客户端Client来创建Topic的过程;

  3. 一些异常检查

①.如果配置了副本副本数–replication-factor 一定要大于0

②.如果配置了–partitions 分区数 必须大于0

③.去zk查询是否已经存在该Topic

  1. 判断是否配置了参数--replica-assignment ; 如果配置了,那么Topic就会按照指定的方式来配置副本情况

  2. 解析配置--config 配置放到configsMap中; configsMap给到NewTopic对象

  3. 将上面所有的参数包装成一个请求参数CreateTopicsRequest ;然后找到是Controller的节点发起请求(ControllerNodeProvider)

  4. 服务端收到请求之后,开始根据CreateTopicsRequest来调用创建Topic的方法; 不过首先要判断一下自己这个时候是不是Controller; 有可能这个时候Controller重新选举了; 这个时候要抛出异常

  5. 服务端进行一下请求参数检查

①.检查Topic是否存在

②.检查 --replica-assignment参数和 (--partitions || --replication-factor ) 不能同时使用

  1. 如果(--partitions || --replication-factor ) 没有设置,则使用 Broker的默认配置(这个Broker肯定是Controller)

  2. 计算分区副本分配方式;如果是传入了 --replica-assignment;则会安装自定义参数进行组装;否则的话系统会自动计算分配方式; 具体详情请看 【kafka源码】创建Topic的时候是如何分区和副本的分配规则

  3. createTopicPolicy根据Broker是否配置了创建Topic的自定义校验策略; 使用方式是自定义实现org.apache.kafka.server.policy.CreateTopicPolicy接口;并 在服务器配置 create.topic.policy.class.name=自定义类; 比如我就想所有创建Topic的请求分区数都要大于10; 那么这里就可以实现你的需求了

  4. zk中写入Topic配置信息 发起CreateRequest请求,这里写入的数据,是我们入参时候传的topic配置--config; 这里的配置会覆盖默认配置;并且节点类型是持久节点;path = /config/topics/Topic名称

  5. zk中写入Topic分区副本信息 发起CreateRequest请求 ,将已经分配好的副本分配策略 写入到 /brokers/topics/Topic名称中; 节点类型 持久节点

  6. Controller监听zk上面的topic信息; 根据zk上变更的topic信息;计算出新增/删除了哪些Topic; 然后拿到新增Topic的 副本分配信息; 并做一些状态流转

  7. 向新增Topic所在Broker发送leaderAndIsrRequest请求,

  8. Broker收到发送leaderAndIsrRequest请求; 创建副本Log文件;

在这里插入图片描述

Q&A


创建Topic的时候 在Zk上创建了哪些节点

接受客户端请求阶段:

  1. topic的配置信息 /config/topics/Topic名称 持久节点
  1. topic的分区信息/brokers/topics/Topic名称 持久节点

Controller监听zk节点/brokers/topics变更阶段

  1. /brokers/topics/{topicName}/partitions/持久节点; 无数据
  1. 向zk中写入/brokers/topics/{topicName}/partitions/{分区号} 持久节点; 无数据
  1. 向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/下的某个节点会怎么样?

详情请看 【kafka实战】一不小心删除了/brokers/topics/下的某个Topic

如果我手动在zk中添加/brokers/topics/{TopicName}节点会怎么样

先说结论: 根据上面分析过的源码画出的时序图可以指定; 客户端发起创建Topic的请求,本质上是去zk里面写两个数据

  1. topic的配置信息 /config/topics/Topic名称 持久节点
  1. 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文件

在这里插入图片描述

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

image.png

学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-uNMVISYA-1711806646481)]
[外链图片转存中…(img-IuBbNCBZ-1711806646482)]
[外链图片转存中…(img-GtMWmtD6-1711806646482)]
[外链图片转存中…(img-22IVwdLJ-1711806646483)]
[外链图片转存中…(img-ambTbO36-1711806646483)]
[外链图片转存中…(img-ayeHsqtI-1711806646483)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-Yf7rIKyb-1711806646484)]

最后

[外链图片转存中…(img-STXvZCPH-1711806646484)]

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值