kafka的一个有趣问题(BUG)

news2025/2/23 4:10:11

这是我的第104篇原创文章

问题由来

在使用kafka时,创建topic,对某个topic进行扩分区的操作,想必大家肯定都使用过。尤其是集群进行扩容时,对流量较大的topic进行扩分区操作。一般而言,期望的效果是:新扩的分区分布到新的broker节点上,这样才能达到均衡流量分摊broker压力。

然而,我们在实际使用过程中发现,在一种场景下,原生的代码逻辑中,对topic分区进行扩容后,新增的分区并没有分配到新的节点上,即新增的分区和老的分区仍旧位于扩容前的broker节点上,导致这个节点压力反而变大,引发生产者发送超时等问题,本文就这个问题进行分析。

问题产生的步骤

我们先来看下问题出现的操作步骤与现象,在具有3个broker的集群环境中,分别通过自带脚本(kafka-topics.sh)和adminClient的接口创建一个分区数为2、副本因子为1的topic,然后再扩容到3分区,此后查看topic分区的分布情况。

通过接口操作的情况如下所示:

d8780c79b0bfb24f3e7291d59a422c2d.jpeg

通过脚本操作的情况如下所示:

eed3a58c66ef978db672d56d153d9548.jpeg

从图中可以直观的看到通过接口创建的topic,在扩容分区后,新增的2号分区的leader与老的1号分区的leader位于同一个broker节点上,而不是像脚本操作的一样,3个分区的leader分别位于3个broker节点上。

两种操作方式的区别

由于扩分区的操作都是一样的,那为什么不同的创建的方式,在进行扩容后就有了不同的结果呢?我们还是先来看下两种创建topic方式的逻辑。

对于通过脚本创建topic的处理逻辑为:

  • 通过zk客户端获取当前集群的所有broker

  • 根据topic创建的参数,进行topic分区副本的分配

  • 通过zk客户端,将topic分区副本信息写入zookeeper

  • kafka broker的controller监听zk并感知其变化,然后根据分区副本的分布情况向对应broker发送请求完成topic分区的创建

关键代码如下所示:

8dc71473249a094612bb320c8f4b03b0.jpeg

对于通过接口创建topic的处理逻辑为:

  • 客户端向broker发送CREATE_TOPIC的请求

  • broker接收到请求后,从元数据缓存中获取当前集群的所有broker

  • 根据topic创建的参数,进行topic分区副本的分配

  • 通过zk客户端,将topic分区副本信息写入zookeeper

  • kafka broker的controller监听zk并感知其变化,然后根据分区副本的分布情况向对应broker发送请求完成topic分区的创建

两种方式本质上的处理都是一样的,即进行topic分区副本的分配,并将这个信息写入zookeeper,然后由controller完成真正的创建逻辑。只不过一个是在客户端侧完成,一个是向broker发送请求,在broker侧完成。

分区副本分配逻辑

既然两种创建topic的方式,其处理逻辑是一样的,那怎么进行扩分区操作后,就不一样了呢?我们再来看下扩分区的处理逻辑。其流程其实和创建topic是一样的,先获取集群所有broker的信息,然后对新增的分区进行broker的分配,最后将完整信息写入zookeeper,broker的controller监听感知信息的变化后,向对应的broker节点发送请求完成分区的新增动作。

值得注意的是:这里对新增分区进行broker分配时,与创建topic时分区的broker分配,调用的是同一个方法:AdminUtils的assignReplicasToBrokers方法,我们来仔细分析下这个函数。

private def assignReplicasToBrokersRackUnaware(
    // 需要分配的分区个数
    nPartitions: Int,
    // 副本因子
    replicationFactor: Int,
    // 集群broker列表
    brokerList: Seq[Int],
    // 分配的起始ID
    fixedStartIndex: Int,
    // 起始分区ID
    startPartitionId: Int): Map[Int, Seq[Int]] = {
    val ret = mutable.Map[Int, Seq[Int]]()
    val brokerArray = brokerList.toArray
    // 如果起始ID非0, 则以传入的为准, 否则从broker集群中随机挑选一个作为起始分配
    val startIndex = 
        if (fixedStartIndex >= 0) 
            fixedStartIndex 
        else 
            rand.nextInt(brokerArray.length)
    // 起始分配的分区ID
    // 对于创建topic而言, startPartitionId的值为-1, 因此分区是从0开始分配
    // 而对于新增分区而言, startPartitionId为当前topic实际分区的个数, 因此已存在的分区是不会再次分配的
    var currentPartitionId = math.max(0, startPartitionId)
    var nextReplicaShift = 
        if (fixedStartIndex >= 0) 
            fixedStartIndex 
        else 
            rand.nextInt(brokerArray.length)
    // 分区副本分配
    for (_ <- 0 until nPartitions) {
        if (currentPartitionId > 0 && (currentPartitionId % brokerArray.length == 0))
            nextReplicaShift += 1
        // 分区首个副本的 brokerID
        val firstReplicaIndex = (currentPartitionId + startIndex) % brokerArray.length
        val replicaBuffer = mutable.ArrayBuffer(brokerArray(firstReplicaIndex))
        for (j <- 0 until replicationFactor - 1)
            replicaBuffer += brokerArray(replicaIndex(firstReplicaIndex, nextReplicaShift, j, brokerArray.length))
        // 新分区的副本情况
        ret.put(currentPartitionId, replicaBuffer)
        currentPartitionId += 1
    }
    ret
}

这个函数为了能同时适用于创建topic和新增topic分区,巧妙的利用了fixedStartIndexstartPartitionId两个参数。

创建topic调用时,fixedStartIndex的值为 -1,即随机挑选一个broker作为起始分配,startPartitionId的值也为-1,因此分区副本是从0开始分配。在分配的时候,随分区的递增,brokerID也进行循环递增,这样可以保证分区副本是尽可能均匀分布在broker中的。

而扩分区调用时,fixedStartIndex为当前topic首个分区副本的brokerID,startPartitionId为当前topic实际分区的个数,这样一来,已经存在的分区不会再次进行分配,即仅对新增的分区进行分配;同时结合fixedStartIndex字段,保证分区分配逻辑是延续创建topic时的分配逻辑,达到分区副本的在broker集群中均衡分布的效果。

举个例子来说明下:在有5个节点的集群中,创建分区数为2、副本因子为1的topic,假设首个分区随机挑选的brokerID为3,那么分区副本的分配结果为:"{0,[3]},{1,[4]}";此后将分区数扩到5个,根据上面的代码分析,fixedStartIndex参数的值为3、startPartitionId的值为2,这样,这个函数得到的结果就是"{2,[0]},{3,[1]},{4,[2]}"。

罪魁祸首

从上面的分析来看,逻辑上都没有问题,但为什么就出现了不符合预期的现象呢?通过断点调试分析,我们发现了一个细节。

在用脚本创建topic和扩分区时,assignReplicasToBrokers函数的入参brokerList都是有序排列的

4d0620c3e6d77fada42b026dc02b57bf.jpeg

从代码中也可以证实这一点。

98c4086223fd692a02a6f0979081d963.jpeg

然而,通过接口创建topic时,入参的brokerList是无序的

6dc2a401f4f5e771ef24ae9b4a040f8c.jpeg

这样一来,前后两个操作的顺序不一致也就导致出现了这个问题。

回头再看文章开头的那个图,其实可以看出端倪,通过接口创建的topic的两个分区分布为:

分区号副本所在的brokerID
01
10

从代码可以反推出,这里传入的broker列表是无序的:[1,0,2],扩分区时,fixedStartIndex的值为1,startPartitionId为2,同时这里传入的broker列表又是有序的,即[0,1,2],根据分配逻辑取broker数组的第 (1+2)%3=0位,即0,这样分区2就又分配到0号broker上了。

从上面断点的图中可以看到,broker侧在处理创建topic的请求时,brokerList是直接从元数据缓存中获取的,而这个元数据缓存是根据controller来更新的。

从代码逻辑可以分析出:controller被选举出来后首次初始化时触发的元数据更新,无法保证broker的有序,同样,随着broker可能的上下线引起的元数据更新也无法保证broker的有序(这里就不再贴相关的代码了,感兴趣的可以自行看下源码)。

小结

本文通过分析集群扩容,同时对topic进行扩分区操作,新扩的分区没有分布到新的broker节点上这一问题现象进行分析,最后发现是由于创建topic时broker列表没有按ID排序,而扩分区操作时broker列表又是按ID排序,两次操作时的broker列表顺序不一致导致出现该问题。

另外,在分析过程中,我们发现kafka-topics.sh脚本可以是指定--zookeeper参数,或者是--bootstrap-server参数。如果指定--bootstrap-server参数的话,等同于通过调用接口完成相关逻辑,也就是会遇到上面提到的问题。

好了,这就是本文的全部内容,如果觉得本文对您有帮助,请点赞+转发,如果觉得有不正确的地方,欢迎留言交流~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2065242.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI在医学领域:HYDEN一种针对医学图像和报告的跨模态表示学习方法

近年来&#xff0c;跨模态文本-图像表示学习在诸多领域取得了显著的突破&#xff0c;尤其是在零样本学习和图像-文本检索等任务上。这一成果的取得很大程度上归功于大量弱监督的图像-文本配对数据的利用&#xff0c;这些数据有效地增强了视觉-语言表示学习的能力。在医学成像领…

如何保证每次生成的都同一张人脸?AI绘画Stable Diffusion的Reference only插件人物一致性教程

Ai绘画有一个很现实的问题&#xff0c;要保证每次画出的都是同一个人物的话&#xff0c;很费劲。 Midjourney就不必说了&#xff0c;人物的高度一致性一直得不到很好的解决。而在Stable Diffusion&#xff08;SD&#xff09;中&#xff0c;常用办法是通过同一个Seed值&#xf…

Linux宝塔面板使用教程 - Centos/Alibaba Cloud Linux,解放命令实现可视化

使用前注意事项&#xff1a;为了您的正常使用&#xff0c;请确保使用全新或纯净的系统安装宝塔面板&#xff0c;不支持已部署项目/环境的系统安装 1.安装命令 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh &&…

Godot《躲避小兵》实战之创建游戏主场景

游戏主场景 现在是时候将我们所做的一切整合到一个可玩的游戏场景中了。 创建新场景并添加一个 Node节点&#xff0c;命名为 Main。&#xff08;我们之所以使用 Node 而不是 Node2D&#xff0c;是因为这个节点会作为处理游戏逻辑的容器使用。本身是不需要 2D 功能的。&#x…

ZooKeeper 的3种部署模式

ZooKeeper 的3种部署模式 1. 单机模式&#xff08;Standalone Mode&#xff09;2. 伪集群模式&#xff08;Pseudo-Cluster Mode&#xff09;3. 集群模式&#xff08;Cluster Mode&#xff09; &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496;…

[000-01-011].第2节:持久层方案的对比

我的后端学习大纲 MyBatis学习大纲 1.持久层解决方案&#xff1a; 1.1.面试1&#xff1a;请说一说持久层解决方案有哪些&#xff1f;&#xff1f;&#xff1f; 1.jdbc JDBC为访问不同的数据库提供了一种统一的途径&#xff0c;为开发者屏蔽了一些细节问题。Java程序员使用JDB…

Vodafone 推出了与 Wi-Fi 竞争的基于树莓派私人5G技术

随着全球5G网络的逐步推出&#xff0c;在其过程中遇到了可预见的起起伏伏&#xff0c;并且蜂窝技术也开始进入另一个无线技术 Wi-Fi &#xff0c;并且已经占据的市场。私有5G网络&#xff08;即个人或公司建立自己的全设施蜂窝网络&#xff09;如今正在寻找曾经属于Wi-Fi的唯一…

Unity低延迟播放RTSP视频流

Unity播放RTSP视频流这个功能在好几个项目中用到&#xff0c;虽然有一些现成的插件&#xff08;VLC for unity、UMP&#xff09;可以使用&#xff0c;但是延迟高&#xff08;300毫秒以上&#xff09;的问题一直没法解决。 最近终于下定决心来解决这个问题&#xff0c;经过几天…

基于 Jenkins、Gitlab、Harbor、Helm 和 Kubernetes 的 CI/CD

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

【微服务部署】Linux部署微服务启动报ORA-01005

问题背景&#xff1a; Linux机器部署springboot微服务&#xff0c;部署完成后发现无法启动&#xff0c;后台报ORA-01005错误。 解决方案&#xff1a; 1.检查当前服务器是否已安装oracle客户端 命令行执行sqlplus username/passwd实例名&#xff0c;如果执行成功&#xff0c;说…

【Canvas与艺术】十边曲线形光阑

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>十边曲线型光阑</title><style type"text/css"&g…

机器学习-识别手写数字

机器学习可以首先构建一个神经网络&#xff0c;用于识别手写数字。通过训练数据&#xff0c;优化神经网络的参数。再利用测试数据来测试训练完成后的神经网络的准确度。本次需要下载的库有tensorflow和matplotlib&#xff0c;keras和mnist数据集一般都被集成在tensorflow中了。…

C/C++语言基础--指针三大专题详解3,完结篇(包括指针做函数参数,函数指针,回调函数,左右法则分析复杂指针等)

本专栏目的 更新C/C的基础语法&#xff0c;包括C的一些新特性 前言 指针是C/C的灵魂&#xff0c;和内存地址相关联&#xff0c;运行的时候速度快&#xff0c;但是同时也有很多细节和规范要注意的&#xff0c;毕竟内存泄漏是很恐怖的指针打算分三篇文章进行讲解&#xff0c;本…

LabVIEW多显示器环境下主显示器识别与管理

该程序使用 LabVIEW 图形化编程语言&#xff0c;涉及多显示器环境中主显示器的识别与信息提取。图像显示了两个不同的方法来获取主显示器的信息。 第一部分&#xff1a;方法一——基于显示器位置的主显示器识别 1. 当前监视器识别&#xff1a; 使用“FP.Monitor”属性节点获取…

在AES加密中,设主密钥为“2B 7E 15 16 28 AE D2 A6 AB F7 15 88 09 CF 4F 3C”,试计算迭代第1轮使用的轮密钥。

题解: 1.分析: 第一轮使用的轮密钥是W[4]、W[5]、W[6]、W[7] w[i](4≤i≤43)求法: (1)i不为4的倍数 w[i] = w[i-1] ⊕w[i-4] (2)为4的倍数 w[i]=SubWord(RotWord(w[i-1]))⊕w[i-4]⊕Rcon[i/4] 解释: ①RotWord:将w[i-1]的4个字节循环上移一个字节 ②SubWo…

JVM的类是如何运行的

本文就是讲解 如何将.class文件转换为机器码

U盘中毒文件被隐藏怎么恢复文件?

很多用户都曾经遇到过U盘文件被病毒隐藏的问题&#xff0c;U盘作为拷贝、存储文件最主要的移动存储设备&#xff0c;里面经常存储了重要的文件&#xff0c;如果文件被隐藏了会给用户带来很多麻烦。那么U盘文件被病毒隐藏&#xff0c;应该怎么解决呢&#xff1f;本文列举了有效的…

QT中使用QAxObject类读取xlsx文件内容并显示在ui界面

一、源码 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow(QWidget *parent nullptr);~MainWindow();pr…

Andrid异步更新UI:Handler(二)深入了解:Message你真的会创建?它是如何子线程和主线程通知?

目录 为什么会有HandlerHandler的原理&#xff0c;以及对象讲解主线程的loop在哪里&#xff0c;为什么主线程loop没有阻塞呢&#xff1f;Looper如何保证唯一Handler为什么会引发内存泄漏呢&#xff1f;Message应该如何创建它&#xff1f; 一、为什么会有Handler 线程分为主线…

软件工程造价师习题练习 18

1.在软件估算过程中&#xff0c;我们主要对项目的规模&#xff0c;质量&#xff0c;进度和成本进行估算 错误 正确 软件估算不对质量进行估算&#xff0c;只对项目的规模&#xff0c;进度&#xff0c;成本进行估算。 答案&#xff1a;错误 2.在使用类比法进行估算时&#xf…