在kafka中增加新的节点后,数据是不会自动迁移到新的节点上的,需要我们手动将数据迁移(或者成为打散)到新的节点上

1 迁移方法

kafka为我们提供了用于数据迁移的脚本。我们可以用这些脚本完成数据的迁移。

1.1 生成partiton分配表

1.1.1 创建json文件topic-to-move.json

{
"topics": [{"topic": "testTopic"}],
"version": 1
}

1.1.2 生成partiton分配表

运行

$ ./kafka-reassign-partitions --zookeeper ${zk_address} --topics-to-move-json-file  topic-to-move.json --broker-list "140,141" --generate

其中${zk_address}是kafka所连接zk地址,"140,141"为该topic将要迁移到的目标节点。

生成结果如下:

Current partition replica assignment

{"version":1,"partitions":[{"topic":"testTopic","partition":1,"replicas":[61,62]},{"topic":"testTopic","partition":0,"replicas":[62,61]}]}

Proposed partition reassignment configuration

{"version":1,"partitions":[{"topic":"testTopic","partition":1,"replicas":[140,141]},{"topic":"testTopic","partition":0,"replicas":[141,140]}]}

其中上半部分是当前的partiton分布情况,下半部分是迁移成功后的partion分布情况。那么我们就用下部分的json进行迁移。另外,也可以自己构造个类似的json文件,同样可以进行迁移。这里我们使用脚本为我们生成的json文件,将下半部分的json保存为expand-cluster-reassignment.json

1.2 执行迁移

$ ./kafka-reassign-partitions --zookeeper ${zk_address}  --reassignment-json-file expand-cluster-reassignment.json --execute

1.3 查看迁移进度

$ ./kafka-reassign-partitions --zookeeper ${zk_address} --reassignment-json-file expand-cluster-reassignment.json --verify

2 源码分析

2.1 脚本调用

kafka-reassign-partitions.sh会调用kafka.admin.ReassignPartitionsCommand.scala,在代码运行过程中抛出的任何异常都会通过标准输出打印出来,所以如果执行该脚本报错,可以看下这块代码来定位问题。

def main(args: Array[String]): Unit = {
// 略
try {
if(opts.options.has(opts.verifyOpt)) // 校验
verifyAssignment(zkUtils, opts)
else if(opts.options.has(opts.generateOpt)) // 生成json
generateAssignment(zkUtils, opts)
else if (opts.options.has(opts.executeOpt)) // 执行迁移
executeAssignment(zkUtils, opts)
} catch {
case e: Throwable =>
println("Partitions reassignment failed due to " + e.getMessage)
println(Utils.stackTrace(e))
} finally {
val zkClient = zkUtils.zkClient
if (zkClient != null)
zkClient.close()
}

2.1.1 executeAssignment

executeAssignment 用于执行迁移。

  def executeAssignment(zkUtils: ZkUtils,reassignmentJsonString: String){
// 略,做一些校验和去重等工作 // 获取当前的partition分布情况
zkUtils.getReplicaAssignmentForTopics(partitionsToBeReassigned.map(_._1.topic))
println("Current partition replica assignment\n\n%s\n\nSave this to use as the --reassignment-json-file option during rollback"
.format(zkUtils.formatAsReassignmentJson(currentPartitionReplicaAssignment))) // 重点,执行迁移,z即将json写到zk上,准确的说是写到"/admin/reassign_partitions"下
// start the reassignment
if(reassignPartitionsCommand.reassignPartitions())
println("Successfully started reassignment of partitions %s".format(zkUtils.formatAsReassignmentJson(partitionsToBeReassigned.toMap)))
else
println("Failed to reassign partitions %s".format(partitionsToBeReassigned))
}

executeAssignment将json写到zk上后,brokerwatch到节点数据变化就开始进行迁移了

2.1.2 verifyAssignment

verifyAssignment用于校验迁移进度

def verifyAssignment(zkUtils: ZkUtils, opts: ReassignPartitionsCommandOptions) {
// 略
println("Status of partition reassignment:")
val reassignedPartitionsStatus = checkIfReassignmentSucceeded(zkUtils, partitionsToBeReassigned) // 重点
reassignedPartitionsStatus.foreach { partition =>
partition._2 match {
case ReassignmentCompleted =>
println("Reassignment of partition %s completed successfully".format(partition._1))
case ReassignmentFailed =>
println("Reassignment of partition %s failed".format(partition._1))
case ReassignmentInProgress =>
println("Reassignment of partition %s is still in progress".format(partition._1))
}
}
} private def checkIfReassignmentSucceeded(zkUtils: ZkUtils, partitionsToBeReassigned: Map[TopicAndPartition, Seq[Int]])
:Map[TopicAndPartition, ReassignmentStatus] = {
val partitionsBeingReassigned = zkUtils.getPartitionsBeingReassigned().mapValues(_.newReplicas) // 从zk节点"/admin/reassign_partitions"读取迁移信息
partitionsToBeReassigned.map { topicAndPartition =>
(topicAndPartition._1, checkIfPartitionReassignmentSucceeded(zkUtils,topicAndPartition._1,
topicAndPartition._2, partitionsToBeReassigned, partitionsBeingReassigned))
}
} def checkIfPartitionReassignmentSucceeded(zkUtils: ZkUtils, topicAndPartition: TopicAndPartition,
reassignedReplicas: Seq[Int],
partitionsToBeReassigned: Map[TopicAndPartition, Seq[Int]],
partitionsBeingReassigned: Map[TopicAndPartition, Seq[Int]]): ReassignmentStatus = {
val newReplicas = partitionsToBeReassigned(topicAndPartition)
partitionsBeingReassigned.get(topicAndPartition) match {
case Some(partition) => ReassignmentInProgress // 如果tp对应的数据存在则说明还在迁移
case None => // 否则可能是成功了
// check if the current replica assignment matches the expected one after reassignment
val assignedReplicas = zkUtils.getReplicasForPartition(topicAndPartition.topic, topicAndPartition.partition)
if(assignedReplicas == newReplicas) // 重点,如果节点不存在了,但是迁移后的replica列表和预期不一致,则报错
ReassignmentCompleted
else { // 经常遇到的报错
println(("ERROR: Assigned replicas (%s) don't match the list of replicas for reassignment (%s)" +
" for partition %s").format(assignedReplicas.mkString(","), newReplicas.mkString(","), topicAndPartition))
ReassignmentFailed
}
}
}

从源码中可以看出判断迁移是否完成是根据"/admin/reassign_partitions"是否存在来判断。如果节点不存在了,并且迁移后的AR和预期一致,则才算成功。

注意:在实际迁移中遇到过好几次报错类似如下,即上面代码的打印的日志

don't match the list of replicas for reassignment

从代码中可以看到出现这个错误的原因是"/admin/reassign_partitions"不存在了,但是当前topic的AR和预期的不一致。这个原因一般是由于迁移的时候broker那边报错了,然后将节点删除了,并没有进行迁移。具体原因需要看下broker的controller的日志。

2.2 broker如何进行迁移

2.2.1 入口

broker的controller节点负责partiton的迁移工作,在broker被选为controller节点的时候会watch "/admin/reassign_partitions" 节点的变化。

private def registerReassignedPartitionsListener() = {
zkUtils.zkClient.subscribeDataChanges(ZkUtils.ReassignPartitionsPath, partitionReassignedListener)
}

所以迁移的工作主要在partitionReassignedListener中,controller watch到"/admin/reassign_partitions"节点数据变化后,会读取该数据内容,并跳过正在删除的partiton,进行迁移工作。

class PartitionsReassignedListener(controller: KafkaController) extends IZkDataListener with Logging {
this.logIdent = "[PartitionsReassignedListener on " + controller.config.brokerId + "]: "
val zkUtils = controller.controllerContext.zkUtils
val controllerContext = controller.controllerContext @throws(classOf[Exception])
def handleDataChange(dataPath: String, data: Object) {
val partitionsReassignmentData = zkUtils.parsePartitionReassignmentData(data.toString) // 读取"/admin/reassign_partitions"节点内的数据,封装成[TopicAndPartition, relipcs] 的形式
val partitionsToBeReassigned = inLock(controllerContext.controllerLock) {
partitionsReassignmentData.filterNot(p => controllerContext.partitionsBeingReassigned.contains(p._1))
}
partitionsToBeReassigned.foreach { partitionToBeReassigned => // 迁移每一个partiton
inLock(controllerContext.controllerLock) {
if(controller.deleteTopicManager.isTopicQueuedUpForDeletion(partitionToBeReassigned._1.topic)) { // 正在删除的则跳过
error("Skipping reassignment of partition %s for topic %s since it is currently being deleted"
.format(partitionToBeReassigned._1, partitionToBeReassigned._1.topic))
controller.removePartitionFromReassignedPartitions(partitionToBeReassigned._1)
} else {
val context = new ReassignedPartitionsContext(partitionToBeReassigned._2) // 将目标replica列表封装成ReassignedPartitionsContext
controller.initiateReassignReplicasForTopicPartition(partitionToBeReassigned._1, context) // 重点,以上都是读取,这里才是真正的迁移工作
}
}
}
}

2.2.2 KafkaController#initiateReassignReplicasForTopicPartition()

initiateReassignReplicasForTopicPartition进行迁移工作。但是他主要做一些校验工作,该方法中会watch该partiton的ISR变化情况,即监听“/brokers/topics/{topic}/partitions/{partiton}/state” 节点的变化, 这和迁移的原理有关系。

def initiateReassignReplicasForTopicPartition(topicAndPartition: TopicAndPartition,
reassignedPartitionContext: ReassignedPartitionsContext) {
val newReplicas = reassignedPartitionContext.newReplicas
val topic = topicAndPartition.topic
val partition = topicAndPartition.partition
val aliveNewReplicas = newReplicas.filter(r => controllerContext.liveBrokerIds.contains(r)) // 根据broker的存活进行过滤
try {
// 重点, 从controllerContext中读取partition的AR
val assignedReplicasOpt = controllerContext.partitionReplicaAssignment.get(topicAndPartition)
assignedReplicasOpt match {
case Some(assignedReplicas) =>
// 如果ontrollerContext中AR和目标迁移列表相同,则抛异常。注意他们都是Seq类型,相同是指顺序也相同。
if(assignedReplicas == newReplicas) {
throw new KafkaException("Partition %s to be reassigned is already assigned to replicas".format(topicAndPartition) +
" %s. Ignoring request for partition reassignment".format(newReplicas.mkString(",")))
} else {
if(aliveNewReplicas == newReplicas) { // 目标列表里的replic的broker都存活才能进行迁移 watchIsrChangesForReassignedPartition(topic, partition, reassignedPartitionContext) // 重点,后面会分析
controllerContext.partitionsBeingReassigned.put(topicAndPartition, reassignedPartitionContext) deleteTopicManager.markTopicIneligibleForDeletion(Set(topic))
onPartitionReassignment(topicAndPartition, reassignedPartitionContext) // 重点,真正干活的,做迁移工作的
} else { // 有不存活的,则抛出异常。
// some replica in RAR is not alive. Fail partition reassignment
throw new KafkaException("Only %s replicas out of the new set of replicas".format(aliveNewReplicas.mkString(",")) +
" %s for partition %s to be reassigned are alive. ".format(newReplicas.mkString(","), topicAndPartition) +
"Failing partition reassignment")
}
}
case None => throw new KafkaException("Attempt to reassign partition %s that doesn't exist"
.format(topicAndPartition))
}
} catch {
case e: Throwable => error("Error completing reassignment of partition %s".format(topicAndPartition), e)
// remove the partition from the admin path to unblock the admin client
removePartitionFromReassignedPartitions(topicAndPartition) // 重点,一点迁移出问题,抛出异常,则会将"/admin/reassign_partitions"里的相应信息清空或者删除节点
}
}

2.2.3 KafkaController#onPartitionReassignment

真正的迁移步骤是在onPartitionReassignment完成的

def onPartitionReassignment(topicAndPartition: TopicAndPartition, reassignedPartitionContext: ReassignedPartitionsContext) {
val reassignedReplicas = reassignedPartitionContext.newReplicas
areReplicasInIsr(topicAndPartition.topic, topicAndPartition.partition, reassignedReplicas) match { // 判断目标迁移的replic是不是都在ISR中,都在的意思是目标迁移的replic列表是ISR的一个子集。例如ISR列表是[1,2,3],目标迁移列表是[2,3],则为true。 // 如果是false,则会将AR和目标replic列表做个并集,类似于增加该partiton的副本数。例如AR是[1,2,3], 目标是[2,3,4],则会将partiton的AR设置为[1, 2, 3, 4], 相当于partiton增加了一个新的replic。
case false =>
val newReplicasNotInOldReplicaList = reassignedReplicas.toSet -- controllerContext.partitionReplicaAssignment(topicAndPartition).toSet
val newAndOldReplicas = (reassignedPartitionContext.newReplicas ++ controllerContext.partitionReplicaAssignment(topicAndPartition)).toSet // AR和目标replica列表求并集
// 更新AR到zk和缓存
updateAssignedReplicasForPartition(topicAndPartition, newAndOldReplicas.toSeq)
// 发送LeaderAndIsr
updateLeaderEpochAndSendRequest(topicAndPartition, controllerContext.partitionReplicaAssignment(topicAndPartition),
newAndOldReplicas.toSeq)
// 让新增加的replic上线,使其开始从leader同步数据。
startNewReplicasForReassignedPartition(topicAndPartition, reassignedPartitionContext, newReplicasNotInOldReplicaList)
info("Waiting for new replicas %s for partition %s being ".format(reassignedReplicas.mkString(","), topicAndPartition) +
"reassigned to catch up with the leader") // 如果是true,则将不再目标列表中的AR中的replic去掉,例如目标迁移是[2,3],AR是[1,2,3], 则将1下线
case true =>
//4. Wait until all replicas in RAR are in sync with the leader.
val oldReplicas = controllerContext.partitionReplicaAssignment(topicAndPartition).toSet -- reassignedReplicas.toSet
//5. replicas in RAR -> OnlineReplica
reassignedReplicas.foreach { replica =>
replicaStateMachine.handleStateChanges(Set(new PartitionAndReplica(topicAndPartition.topic, topicAndPartition.partition,
replica)), OnlineReplica)
} // 目标列表中没有leader则需要重新选下leader
moveReassignedPartitionLeaderIfRequired(topicAndPartition, reassignedPartitionContext) // 以下是停用掉下掉的replica的一些工作,例如更新AR,更新zk,发送meta请求等。另外删除"/admin/reassign_partitions"节点数据
stopOldReplicasOfReassignedPartition(topicAndPartition, reassignedPartitionContext, oldReplicas) updateAssignedReplicasForPartition(topicAndPartition, reassignedReplicas) removePartitionFromReassignedPartitions(topicAndPartition)
info("Removed partition %s from the list of reassigned partitions in zookeeper".format(topicAndPartition))
controllerContext.partitionsBeingReassigned.remove(topicAndPartition) sendUpdateMetadataRequest(controllerContext.liveOrShuttingDownBrokerIds.toSeq, Set(topicAndPartition)) deleteTopicManager.resumeDeletionForTopics(Set(topicAndPartition.topic))
}
}

可以到这里会有个疑问,目标迁移列表不是ISR的子集,就只是增加了replic,并没有去掉replic的步骤啊。这里的关键是在initiateReassignReplicasForTopicPartition中watch了partiton的ISR情况,即调用了watchIsrChangesForReassignedPartition方法。

2.2.4 watchIsrChangesForReassignedPartition

该方法监听/brokers/topics/{topic}/partitions/{partiton}/state” 节点的变化。如果目标迁移列表已经跟上leader了,那么就会将不在目标迁移列表里的replic下线,完成迁移

def handleDataChange(dataPath: String, data: Object) {
inLock(controllerContext.controllerLock) {
debug("Reassigned partitions isr change listener fired for path %s with children %s".format(dataPath, data))
val topicAndPartition = TopicAndPartition(topic, partition)
try {
controllerContext.partitionsBeingReassigned.get(topicAndPartition) match {
case Some(reassignedPartitionContext) =>
val newLeaderAndIsrOpt = zkUtils.getLeaderAndIsrForPartition(topic, partition)
newLeaderAndIsrOpt match {
case Some(leaderAndIsr) => // check if new replicas have joined ISR
val caughtUpReplicas = reassignedReplicas & leaderAndIsr.isr.toSet // 求并集ISR和目标迁移列表的并集
if(caughtUpReplicas == reassignedReplicas) { // 目标迁移列表全部跟上了,则再次调用KafkaController#onPartitionReassignment,这次会走true那个判断分支了,会将不再目标replic列表中的replic下线。
// resume the partition reassignment process
info("%d/%d replicas have caught up with the leader for partition %s being reassigned."
.format(caughtUpReplicas.size, reassignedReplicas.size, topicAndPartition) +
"Resuming partition reassignment")
controller.onPartitionReassignment(topicAndPartition, reassignedPartitionContext)
}
else {
info("%d/%d replicas have caught up with the leader for partition %s being reassigned."
.format(caughtUpReplicas.size, reassignedReplicas.size, topicAndPartition) +
"Replica(s) %s still need to catch up".format((reassignedReplicas -- leaderAndIsr.isr.toSet).mkString(",")))
}
case None => error("Error handling reassignment of partition %s to replicas %s as it was never created"
.format(topicAndPartition, reassignedReplicas.mkString(",")))
}
case None =>
}
} catch {
case e: Throwable => error("Error while handling partition reassignment", e)
}
}
}

3 broker 处理迁移的思路总结

从以上分析我们可以看出,broker会watch "/admin/reassign_partitions"节点。当发现有迁移任务的时候,会将partiton的AR进行扩展,例如原先partiton的AR是[1, 2], 现在要迁移到[2, 3],那么partiton会先将AR扩展到[1, 2, 3],并监控ISR的变化。

当replica-2和replica-3都跟上后,即在ISR中的时候,表明新的repica-3已经和leader数据同步了。这个时候就可以将replica-1剔除了,最后得到迁移结果是[2, 3]。即迁移是一个先增加再减少的过程。

4 可能遇到的问题

4.1 报错

Assigned replicas (0,1) don't match the list of replicas for reassignment (1,0) for partition [testTopic,0] Reassignment of partition [testTopic,0] failed

在2.1.2中已经说明了,该错误是由于"/admin/reassign_partitions"节点已经被删除了,但是AR和目标迁移列表不相同报的错,一般需要看下controller的日志,看下controller在迁移过程中是不是抛出了异常。

4.2 迁移一直在进行中,不能完成

迁移需要等目标迁移列表中的replic都跟上了leader才能完成,目前迁移列表一直跟不上,那么就不会完成。可以看下zk中“/brokers/topics/{topic}/partitions/{partiton}/state”,注意下目标迁移列表是不是在isr中,如果不在说明要迁移的replic还没有完成从leader拉取数据。具体为甚么没有拉取成功,可能是数据量比较大,拉取需要一定的时间;也可能是其他原因比如集群宕机了等,需要具体分析下

kafka partiton迁移方法与原理的更多相关文章

  1. 字体属性设置(一):谷歌浏览器12px以下字体的显示;方法和原理

    前言: chrome 谷歌浏览器默认的字体大小为16px:可以通过设置font-size来设置字体大小但是当设置到12px以下的时候字体大小不再改变:对于想设置其他大小的字体就很头疼,本文参考网上的方 ...

  2. 数据库 SQL Server 到 MySQL 迁移方法总结

    最近接手一起老项目数据库 SQL Server 到 MySQL 的迁移.因此迁移前进行了一些调查和总结.下面是一些 SQL Server 到 MySQL 的迁移方法. 1. 使用 SQLyog 迁移 ...

  3. Android ListView实现不同item的方法和原理分析

    ListView实现不同item的方法和原理分析 一问题抛出Listview是android里面的重要组件,用来显示一个竖向列表,这个没有什么问题:但是有个时候列表里面的item不是一样的,如下图,列 ...

  4. oracle 表迁移方法 (二) 约束不失效

    DB:11.2.0.3.0 在oracle 表迁移方法 (一)中,只是move了一张普通的表,如果表的字段带有主键约束呢 ? [oracle@db01 ~]$ sqlplus / as sysdba ...

  5. JS跨域方法及原理

        JS跨域分析判断 JS跨域:在不同域之间,JS进行数据传输或通信.比如ajax向不同的域请求数据.JS获取iframe中的页面中的值(iframe内外不同域) 只要协议.端口.域名有一个不同则 ...

  6. Ubuntu Server搭建svn服务以及迁移方法【转】

    转自:http://www.linuxidc.com/Linux/2013-05/84693.htm Ubuntu Server搭建svn服务以及迁移方法 采用apache+svn,http访问方式. ...

  7. android中获取root权限的方法以及原理(转)

    一. 概述 本文介绍了android中获取root权限的方法以及原理,让大家对android 玩家中常说的“越狱”有一个更深层次的认识. 二. Root 的介绍 1. Root 的目的 可以让我们拥有 ...

  8. jquery ready方法实现原理 内部原理

    jquery ready方法实现原理 内部原理 今天闲来无事研究研究jquery.ready()的内部实现,看JQ的源码一头雾水,由于自己很菜了,于是翻了翻牛人的播客,讲述详细,收获颇多. 先普及一下 ...

  9. jQuery插件实现的方法和原理简单说明

    下文来自 http://www.itzhai.com/jquery-plug-ins-to-achieve-the-methods-and-principles-of-simple-instructi ...

随机推荐

  1. vscode分栏显示快捷键

    vscode没有默认的分栏快捷键,我们可以自定义,步骤如下: 1.Crtl + k,再Ctrl + s,调出快捷键设置面板 2.在搜索栏输入"视图:",在未定义快捷键的区域找到&q ...

  2. leetcode算法13.罗马数字转整数

    哈喽!大家好,我是[学无止境小奇],一位热爱分享各种技术的博主! [学无止境小奇]的创作宗旨:每一条命令都亲自执行过,每一行代码都实际运行过,每一种方法都真实实践过,每一篇文章都良心制作过. [学无止 ...

  3. Java的诞生历史

    Java帝国的诞生 1.1972年C诞生 贴近硬件,运行极快,效率极高 操作系统,编译器,数据库,网络系统等都采用C语言 但是,它的指针和内存管理给程序员安上了"枷锁".它的指针没 ...

  4. 【Oracle】PLSQL如何更新、提交数据库中数据

    转至:https://blog.csdn.net/Bee_AI/article/details/89279612 Oracle更新单表数据时,一般使用SELECT--FOR UPDATE语句,这条语句 ...

  5. 点击弹出div内容包括遮罩层

    效果: HTML: <!-- 添加分组按钮 -->             <div class="group_add" onclick = "docu ...

  6. MySQL 学习笔记(二)MVCC 机制

    之前在讲 MySQL 事务隔离性提到过,对于写操作给读操作的影响这种情形下发生的脏读.不可重复读.虚读问题.是通过MVCC 机制来进行解决的,那么MVCC到底是如何实现的,其内部原理是怎样的呢?我们要 ...

  7. Python:用正则表达式,提取字符串中的所有中文

    import re def clean(line): pattern = re.compile(u'[^\u4e00-\u9fa5]') #中文的范围为\u4e00-\u9fa5 line = re. ...

  8. C# 反编译工具之ILSpy

    下载地址:http://ilspy.net/ 中文版下载地址:http://www.fishlee.net/soft/ilspy_chs 对dll和exe文件反编译:

  9. Python推导式

    Python推导式 推导式:是Python中提供的一个非常方便的功能,可以通过一行代码实现创建 list.dict.tuple.set的同时初始化一些值. 1.列表推到式 # -*- coding: ...

  10. if 、whitch、for、while

    if 用于过程的判断  whitch用于值得判断 if { } else if { } else if //当此前的过程满足条件时,程序便不再判断其他过程,但是if{}if{}这样的写法程序会将每个过 ...