一、kafka-topic.sh
为了便于操作Kafka集群,Kafka源码包中提供了多个shell脚本,其中kafka-topic.sh提供了Topic的创建、修改、列举、描述、删除功能,内部通过TopicCommand来实现。其脚本内容如下:
//kafka-run-class.sh加载kafka的classpath,执行其中kafka.admin.TopicCommand的main函数
exec $(dirname $0)/kafka-run-class.sh kafka.admin.TopicCommand $@
通过解析不同的action来执行不同的操作
object TopicCommand {
def main(args: Array[String]): Unit = {
//解析参数
val opts = new TopicCommandOptions(args)
//如果没有参数,则输出使用方法并退出
if(args.length == 0)
CommandLineUtils.printUsageAndDie(opts.parser, "Create, delete, describe, or change a topic.")
// 确认action的个数,如果有多个就退出
val actions = Seq(opts.createOpt, opts.listOpt, opts.alterOpt, opts.describeOpt, opts.deleteOpt).count(opts.options.has _)
if(actions != 1)
CommandLineUtils.printUsageAndDie(opts.parser, "Command must include exactly one action: --list, --describe, --create, --alter or --delete")
//校验参数的有效性
opts.checkArgs()
//创建zk连接
val zkClient = new ZkClient(opts.options.valueOf(opts.zkConnectOpt), 30000, 30000, ZKStringSerializer)
try {
//包含create关键字,则创建topic
if(opts.options.has(opts.createOpt))
createTopic(zkClient, opts)
//包含alter关键字,则修改topic
else if(opts.options.has(opts.alterOpt))
alterTopic(zkClient, opts)
//包含list关键字,则列举topic
else if(opts.options.has(opts.listOpt))
listTopics(zkClient, opts)
//包含describe,则描述topic
else if(opts.options.has(opts.describeOpt))
describeTopic(zkClient, opts)
//包含delete关键字,则删除topic
else if(opts.options.has(opts.deleteOpt))
deleteTopic(zkClient, opts)
} catch {
case e: Throwable =>
println("Error while executing topic command " + e.getMessage)
println(Utils.stackTrace(e))
} finally {
zkClient.close()
}
}
通过上面的代码可知,action的类型主要有:
--create 创建topic
--alter 修改topic配置
--list 列举topic列表
--describe 描数topic
--delete 删除topic
下面详细介绍创建topic的流程。
二、创建Topic
使用kafka-topic.sh来创建topic的命令如下:
./kafka-topic.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 2
--topic test
此时由于没有指定Partition的AR,则会根据负载均衡算法将Partition的Replica均分到各个Broker Server中,并且会选择AR列表中的第一个为Leader Replica。如果指定了--replica-assignment 1:2,2:3,3:1这样的参数,则设置了Partition的列表。createTopic的流程如下:
def createTopic(zkClient: ZkClient, opts: TopicCommandOptions) {
//解析出topic名称
val topic = opts.options.valueOf(opts.topicOpt)
//解析出配置文件
val configs = parseTopicConfigsToBeAdded(opts)
//如果包含--replica-assignment,则提前其中具体的参数
if (opts.options.has(opts.replicaAssignmentOpt)) {
val assignment = parseReplicaAssignment(opts.options.valueOf(opts.replicaAssignmentOpt))
//将topic的配置参数和replic的分片情况分别持久化到zookeeper的不同目录中。
AdminUtils.createOrUpdateTopicPartitionAssignmentPathInZK(zkClient, topic, assignment, configs)
} else {
//不包含--replica-assignment,则需要自动进行分配Replica
CommandLineUtils.checkRequiredArgs(opts.parser, opts.options, opts.partitionsOpt, opts.replicationFactorOpt)
val partitions = opts.options.valueOf(opts.partitionsOpt).intValue
val replicas = opts.options.valueOf(opts.replicationFactorOpt).intValue
//开始创建topic
AdminUtils.createTopic(zkClient, topic, partitions, replicas, configs)
}
println("Created topic \"%s\".".format(topic))
}
一般情况下,用户不会指定--replica-assignment,这个时候kafka会采用默认的分配算法来分配Partition的Replica,默认的分配算法主要有两个原则:
1、针对Topic内的所有的Replicas,要将它们均匀的分配到所有的Broker Servers上。
2、针对Partition内的Replicas,要将它们均匀的分配到所有的Broker Servers上。
因此分配算法的流程如下:
1、从Broker Server的随机位置开始按照轮询的方式选择每个Partition的First Replica。
2、不同Partition剩余的Replica按照一定的偏移量紧跟着各自的First Replica。
假设当前Kafka集群有三个Broker Servers,编号分别为Broker-0,Broker-1,Broker-2,此时准备创建分区个数为3,副本个数为2的Topic,其中P0-1代表分区为0的第一个副本,其它类推,其Replicas的分配流程如下:
其中P0-1恰好落在了Broker-1的机器上,之后的P1-1,P2-1按照轮询的方式分配,并且每个分区的第二个Replica距离第一个Replica恰好一个为一个分区,AdminUtils.createTopic的代码如下:
def createTopic(zkClient: ZkClient,
topic: String,
partitions: Int,
replicationFactor: Int,
topicConfig: Properties = new Properties) {
//在分配之前将broker servers排序
val brokerList = ZkUtils.getSortedBrokerList(zkClient)
//执行分配算法
val replicaAssignment = AdminUtils.assignReplicasToBrokers(brokerList, partitions, replicationFactor)
将topic的配置和Replicas的分配情况持久化至zookeeper
AdminUtils.createOrUpdateTopicPartitionAssignmentPathInZK(zkClient, topic, replicaAssignment, topicConfig)
}
其中assignReplicasToBrokers负责具体的分配算法,其代码如下:
def assignReplicasToBrokers(brokerList: Seq[Int],
nPartitions: Int,
replicationFactor: Int,
fixedStartIndex: Int = -1,
startPartitionId: Int = -1)
: Map[Int, Seq[Int]] = {
//nPartitions 参数必须大于0
if (nPartitions <= 0)
throw new AdminOperationException("number of partitions must be larger than 0")
//replicationFactor 参数必须大于0
if (replicationFactor <= 0)
throw new AdminOperationException("replication factor must be larger than 0")
/*replicationFactor 不能大于broker server的数量,否则同一个broker会分配到同一个partition的两个Replica.
*/
if (replicationFactor > brokerList.size)
throw new AdminOperationException("replication factor: " + replicationFactor +
" larger than available brokers: " + brokerList.size)
val ret = new mutable.HashMap[Int, List[Int]]()
//起始偏移量,如果小于0就取随机数,
val startIndex = if (fixedStartIndex >= 0) fixedStartIndex else rand.nextInt(brokerList.size)
//起始分配的Partition,默认从第一个Partition开始
var currentPartitionId = if (startPartitionId >= 0) startPartitionId else 0
//计算第二个Replica相比于第一个的偏移量。
var nextReplicaShift = if (fixedStartIndex >= 0) fixedStartIndex else rand.nextInt(brokerList.size)
for (i <- 0 until nPartitions) {
/*如果currentPartitionId 正好等于broker个数,则nextReplicaShift加1,
从第一个Broker开始
*/
if (currentPartitionId > 0 && (currentPartitionId % brokerList.size == 0))
nextReplicaShift += 1
//计算分区的第一个Replica索引
val firstReplicaIndex = (currentPartitionId + startIndex) % brokerList.size
//获取对应的Broker Id
var replicaList = List(brokerList(firstReplicaIndex))
for (j <- 0 until replicationFactor - 1)
//分配分区内其它的Replicas,距离第一个Replica的偏移量为nextReplicaShift
replicaList ::= brokerList(replicaIndex(firstReplicaIndex, nextReplicaShift, j, brokerList.size))
//分配结束,保存起来
ret.put(currentPartitionId, replicaList.reverse)
//分区索引+1,继续分配
currentPartitionId = currentPartitionId + 1
}
ret.toMap
}
当分配结束之后,会将Replica的分配情况写入到zookeeper的/brokers/topics/[topic]目录中,从而触发TopicChangeListener监听器,从而进行Topic的真正创建。