Kafka | Broker 工作原理

前言

上一篇文章 Kafka | 集群部署和项目接入集群部署 讲述了 Kafka 集群的搭建以及在项目的接入步骤,本文将更进一步介绍 Kafka Broker 的设计及原理。

节点角色及职责

在 Kafka 集群中,节点的核心角色有两种,分别是 Broker 和 Controller,节点可设置角色为其中之一,也可以同时设置为 Broker 和 Controller。

Broker 职责

Broker 主要负责数据的存储和读写操作,是消息处理的 "执行者"。

  • 消息存储

    以分区 Partition 为单位持久化消息数据,每个 Partition 在 Broker 上以 Segment 文件形式存储,并维护索引文件以实现高效读写。

  • 请求处理

    • 作为 Partition Leader 时:接收生产者的消息写入请求,并响应消费者的拉取请求
    • 作为 Partition Follower 时:从 Leader 副本同步数据,保证副本一致性
  • 集群协作

    与其他 Broker 和 Controller 通信,同步分区状态(如 Leader 切换、副本分配等)

Controller 职责

Controller 是集群的"管理者",通常由某个 Broker 节点兼任(KRaft 模式下可独立部署),负责元数据协调和全局决策。

  • 元数据管理

    维护集群全局状态,包括 Topic 配置、分区分配、副本位置(ISR 列表)等,并将变更同步至其他节点。

  • Leader 选举

    当分区 Leader 故障时,根据 ISR 规则选举新的 Leader,并通知相关 Broker 更新状态

  • Broker 生命周期管理

    监听 Broker 的上线/下线事件,触发副本重新分配或分区迁移

  • 故障恢复

    在 KRaft 模式下,通过 Raft 协议快速选举新的 Controller(传统模式依赖 Zookeeper),确保高可用

集群启动流程

Controller 仲裁组部分会先启动,完成选举就绪后,Broker 部分再开始自动注册到 Controller 集群并加载分区数据。

Controller 仲裁组启动流程

  1. Raft 选举阶段

    每个 Controller 节点启动时,会初始化 Raft 客户端(RaftManager)和元数据日志(_cluster_metadata 主题)。开始以 Follower 状态启动,等待随机超时(默认 2 s) 后若没收到心跳,转为 Candidate 发起选举请求。其他 Controller 节点根据日志一致性 (Raft 的 Log Matching 属性)投票,获得多数票的节点成为 Active Controller(Leader),其余为 Follower。

  2. 元数据加载与恢复

    • 日志回放:Active Controller 从 __cluster_metadata 主题的日志文件中回放元数据变更记录(如何 Topic 创建、分区分配等),重建内存状态
    • 快照加速:若存在快照文件(如 00000000000000000000.checkpoint),则直接加载快照中的集群状态,避免全量日志回放,提高效率。
  3. 仲裁组形成与服务就绪

    Active Controller 需确保多数(N/2 + 1)Controller 节点在线并完成日志同步,才允许处理元数据变更请求(如分区 Leader 选举)。Active Controller 会定期向 Follower 发送心跳(包含 term 和 commitIndex),维持领导权并同步日志提交进度。

​脑裂防护​ ​:通过 Raft 的 termepoch机制避免多主问题,旧 Controller 恢复后因 term较小会被拒绝

​动态仲裁扩展​ ​:Kafka 3.4+ 支持通过 controller.quorum.bootstrap.servers动态添加 Controller 节点,无需重启集群

Broker 节点启动流程

  1. 注册到 Controller 仲裁组

    Broker 启动后会向 controller.quorum.bootstrap.servers 列表中的任一 Controller 发送 BrokerRegistrationRequest,包含自身 node.id 和监听地址。 Active Controller 将该 Broker 加入集群元数据,并分配分区副本(根据 auto.leader.rebalance.enable 配置)。

  2. 元数据同步和初始化

    Broker 通过 MetadataFetch 请求从 Controller 获取最新集群状态(如 Topic 分区分布、Leader 副本分配等),根据元数据加载本地分配的 Partition 日志文件(如 topic-0/00000000000000000000.log),并初始化副本状态(Leader/Follower)

  3. 加入集群服务

    Broker 定期向 Controller 上报心跳,同步副本同步进度(如 highWatermark),若超时未上报会被标记为离线。

    完成初始化后,Broker 开始接收生产者写入请求(仅 Leader 副本)和消费者拉取请求,Follower 副本从 Leader 同步数据。

整体流程图

Kafka 主题分区副本

副本基本信息

  1. 副本:在 Kafka 中,分区可以配置多个副本,起到数据备份的作用,提高数据可靠性。

  2. Kafka 默认副本 1 个(也就是只有 Leader),生产环境一般配置为 2 个,保证数据可靠性;太多副本会

    增加磁盘存储空间,增加网络上数据传输,降低效率。

  3. Kafka 中副本分为:Leader 和 Follower。Kafka 生产者只会把数据发往 Leader,

    然后 Follower 找 Leader 进行同步数据。

  4. Kafka 分区中的所有副本统称为 AR(Assigned Repllicas)。

    AR = ISR + OSR

    ISR:表示和 Leader 保持同步的副本集合。如果 Follower 长时间未向 Leader 发送通信或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由 replica.lag.time.max.ms 参数设定,默认 30 s。Leader 发生故障后,就会从 ISR 中选举出新的 Leader。

    OSR: 表示 Follower 和 Leader 副本同步时,延时过多的副本。

Leader 选举流程

当分区 Leader 出现故障时,KRaft 控制器(Active Controller)会检测到分区 Leader 失效事件并触发选举流程:

  1. 候选副本筛选

    KRaft 模式下的 Leader 选举优先从 ISR (In-Sync Replicas) 列表中选择新 Leader:

    • ISR 非空时的选举

      Controller 从 ISR 列表中选择一个副本作为新 Leader,选择标准通过基于副本的同步进度,即选择 LEO (日志末端偏移量)较大的,因为这样的副本数据最接近原 Leader 的,以最大程度保证数据完整和一致性。

    • ISR 为空时的选举

      如果 unclean.leader.election.enable=true(默认 false),控制器会从所有可用副本(包括非 ISR 的 OSR 副本中选择 LEO 最大的副本作为新 Leader)。这种选举可能导致数据丢失,因为新 Leader 可能不包含所有已提交的信息。

  2. Leader 确认与元数据更新

    当选的副本需要通过 Raft 协议获得多数控制器的确认。然后由 Active Controller 将 Leader 信息写入 __cluster_metadata 元数据主题,所有 Broker 通过定期拉取元数据更新(MetadataFetch)获取新 Leader 信息。后续生产者和消费者在下次请求时会自动连接到新 Leader。

  3. 副本同步恢复

新 Leader 接管,新 Leader 开始处理读写请求并维护 ISR 列表,其他副本则从新 Leader 同步数据,追赶落后的日志。

分区 Leader 选举基于 Raft 协议的核心机制说明

​任期(Term)机制​ ​:每个选举周期有唯一递增的任期号,防止脑裂

​日志完整性检查​ ​:候选副本必须包含所有已提交的日志条目

​多数派原则​​:新 Leader 必须获得多数控制器的确认

Leader 和 Follower 故障处理细节

基础概念

LEO(Log End Offset):表示每个副本下次要写入数据的位置,即最新数据的位置 + 1

HW (High Watemark): 所有副本中最小的 LEO。

Follower 故障处理

  1. Follower 发生故障后会被踢出 ISR

  2. 这个期间 Leader 和其他 Follower 会继续接收数据

  3. 待该 Follower 恢复后,会读取本地磁盘上次记录的 HW,并且把 log 文件中高于 HW 的部分截取掉,从 HW 开始向 Leader 进行同步数据。

  4. 等该 Follower 的 LEO 大于等于该分片的 HW 后,即 Follower 追上 Leader 之后,即可重新加入 ISR 了。

Leader 故障处理

  1. Leader 发生故障后,会根据前面说的选举规则从 ISR 中选出一个新的 Leader
  2. 为了保证数据一致性,其他 Follower 会把自己高于的 HW 的数据截掉,然后再从新 Leader 同步数据。

这只能保证副本之间的数据一致性,但是不能保证数据不丢失或者不重复。

Leader Partition 自动平衡

正常情况下,Kafka 会把在各分片的 Leader 分片均匀地分布到各个节点,以实现负载均衡。但是若有 Broker 出现故障,Leader 就会被分布到剩下的 Broker 下,后续 Broker 重新上线后,也只是作为 Follower 节点,所以会导致负载倾斜,部分 Broker 需要承受大部分的请求负载。

如果每个分片的 Leader 都是对应 ISR 中的优先副本(第一个副本),也就是最初的状态,这样则是均匀的。所以当出现不均匀时,只需把失衡分片的 Leader 归还为 ISR 中的优先副本即可。Kafka 就自带这样的重平衡机制,通过 auto.leader.rebalance.enable 开关,默认为 true。通过 leader.imbalance.per.broker.percentage 参数设置 broker 允许的不平衡的 Leader 的最大比例(当前 Broker 上不平衡的分片 / 当前 topic 的总分片数),默认为 10 %,若超过该比例,控制器则会触发 leader 再平衡。

关键配置参数

自动平衡的行为主要由以下三个参数控制:

参数名 默认值 作用
​auto.leader.rebalance.enable​ true 总开关,决定是否启用自动平衡功能 。
​leader.imbalance.per.broker.percentage​ 10 每个Broker允许的不平衡率阈值,单位是百分比 。
​leader.imbalance.check.interval.seconds​ 300 控制器检查不平衡的时间间隔,单位是秒 。
  • 性能影响​:Leader切换是一项高成本操作。在切换期间,分区会有短暂的不可用窗口,并且客户端需要感知到元数据变更并重新连接新的Leader,可能导致请求延迟增加或短暂阻塞 。
  • ​建议配置​ :正因为其执行时机不可控,为了避免在业务高峰期(如大促时段)触发自动平衡对服务造成影响,​通常建议在生产环境中将此参数 auto.leader.rebalance.enable设置为 false
  • ​替代方案​ :关闭自动平衡后,运维人员可以选择在业务低峰期,通过Kafka提供的命令行工具(如 kafka-leader-election.sh​手动执行优先副本选举​,这样可以更精准地控制维护窗口,降低风险

增加副本

生产环境中,随着某些主题的重要等级提升,我们可能会需要给主题添加副本。具体操作如下:

  1. 创建主题(如果需要) 指定 3 个分片,各 1 个副本,也就是只有 Leader。

    bash 复制代码
    kafka-topics.sh --bootstrap-server 172.20.0.11:9092 --create --partitions 3 --replication-factor 1 --topic four

    查看分布情况:

  2. 创建副本存储计划

    创建副本计划 increase-replication-factor.json,为每个分片指定 3 个副本。

    json 复制代码
    {
      "version": 1,
      "partitions": [
        {
          "topic": "four",
          "partition": 0,
          "replicas": [1, 2, 3]
        },
        {
          "topic": "four",
          "partition": 1,
          "replicas": [1, 2, 3]
        },
        {
          "topic": "four",
          "partition": 2,
          "replicas": [1, 2, 3]
        }
      ]
    }
  3. 执行分配计划

    bash 复制代码
    kafka-reassign-partitions.sh --bootstrap-server 127.0.0.1:9092 --reassignment-json-file increase-replication-factor.json --execute

    执行后再次查看分片副本分布情况:

Topic 数据存储机制

一个 Topic 下可以有多个 partition,具体到 broker 存储的是 partition 数据。而 partition 并不是直接使用单个文件存储,因为这样当 log 过大时会导致数据定位效率太低,因此 Kafka 进一步对 partition 进行分片和索引,分为了多个 segement,一个 segement 主要包含 .index、.log 以及 .timeindex 等文件。

同一个 topic 的所有 segement 文件都放在同一个文件夹下,文件夹命名为 <主题名>-<分区号>,例如 hello-0,则表示 hello 主题的第一个分区。

  • .log 文件: 存储实际的消息数据
  • .index 文件:存储消息的偏移量索引
  • .timeindex 文件:存储时间戳索引

查看 .log 中数据

  1. 先往主题 four 发送数据

    bash 复制代码
    kafka-console-producer.sh bootstrap-server 127.0.0.1:9092 --topic four

    发送消息内容 helloworld

  2. 查看 log 内容

    bash 复制代码
    kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.log --print-data-log

需要注意的是,.log 中的一条数据记录不一定是对应一条消息的,可能会包含多条消息,这是因为 Kafka 为了提高效率,生产者写入消息是以批次写入的,所以 .log 的每个记录对应着一个批次。所以也就能理解了为什么记录中包含了 baseOffset、lastOffset、count 这些字段。

.index 索引原理

.index 为稀疏索引,一般大约每往 log 中写入 4KB 数据就会向 index 中写入一条索引,这个大小由 log.index.interval.bytes 配置决定,默认为 4KB。.index 中存储的相对 offset(相对当前 segement 起始 offset),而不是绝对 offset,从而可以确保索引数据的大小不会太大,可以控制其中的 offset 值在固定大小范围内。

文件清理策略

Kafka 的文件清理策略主要有两种,分别是 日志删除日志压缩

特性 日志删除 日志压缩
​策略​ 根据条件(时间/大小)直接删除旧数据 按消息的 Key 保留最新版本,清理旧版本
​配置参数​ log.cleanup.policy=delete log.cleanup.policy=compact
​主要目标​ 控制数据存储的生命周期,释放磁盘空间 确保每个 Key 都有最新值,用于状态恢复
​默认策略​ Kafka 中用户创建 Topic 的​​默认策略​ 系统 Topic __consumer_offsets的默认策略
​适用场景​ 流式处理、日志收集等大多数场景 记录最新状态,如用户资料、数据库变更日志

日志删除

日志删除策略主要有两个触发维度:基于时间基于大小

两者并不是互斥的,可以同时打开,只要满足其中之一条件文件就会被删除。

基于时间

默认是打开的,以 segement 中所有记录中的最大时间戳作为该文件时间戳进行判断。

日志的默认保存时间是 7 天,也可以自行配置,相关配置如下:

配置 说明
log.retention.hours 小时,默认 7 天,优先级最低
log.retention.minutes 分钟
log.retention.ms 毫秒,优先级最高
log.retention.check.interval.ms 过期检查周期,默认 5 分钟

基于大小

默认是关闭的,通过 log.retention.bytes参数(默认-1,无限制)设置每个分区所能保留的最大数据量,如果总大小超过此值,最早的日志 segement 会被删除。

日志压缩

日志压缩不会直接删除数据,而是像字典一样只会保留最新的值,例如有多个相同 key 的值,被压缩后只会保留最新的一个值,offset 也是取较大值,因此需要注意一个关键点,压缩后会使得分区内偏移量不连续,当尝试消费一个已被清除的偏移量,会消费到比它大的下一个可用偏移量的消息。

压缩过程是异步的,而且只会作用于已提交的非活跃日志分段,不会影响正在写入的活跃分段。

数据的高效读写

Kafka 可以高效地读写数据,主要得益于以下几点:

  1. Kafka 本身是分布式集群,采用了分区技术,提高了读写并行度

  2. 读数据采用稀疏索引,可以快速定位要消费的数据

  3. 顺序写磁盘

    Kafka 往 log 文件写数据是一直追加到文件末端,采用顺序写的方式,因此速度非常快。

  4. 页缓存 + 零拷贝技术

    • PageCache 页缓存

      主要依赖底层操作系统提供的 PageCache 功能,也就是磁盘的缓存。当上层写入数据时,仅写入到 PageCache 中,当读数据时,先从 PageCache 中找,找不到再到磁盘中读取。而 PageCache 和磁盘间的数据交互,由操作系统管理。

    • 零拷贝

      Kafka 的数据加工处理由生产者和消费者端完成,Kafka Broker 只需负责读写数据,所以数据无需经过应用层,减少数据拷贝次数,提高效率。

      传统I/O流程的瓶颈

      在传统的数据传输流程中,数据从磁盘到网络需要经历:

      磁盘 → 内核缓冲区(DMA拷贝)

      内核缓冲区 → 用户缓冲区(CPU拷贝)

      用户缓冲区 → Socket缓冲区(CPU拷贝)

      Socket缓冲区 → 网卡(DMA拷贝)

      这个过程涉及 4 次数据拷贝和 4 次上下文切换,其中两次 CPU 拷贝是冗余的 。

      Kafka的零拷贝实现

      Kafka 通过 Linux 的 sendfile() 系统调用(在 Java 中通过 FileChannel.transferTo() 实现)来优化这一流程:

      sendfile()系统调用:Kafka直接调用sendfile()将数据从页缓存传输到Socket缓冲区,完全跳过用户空间 。

      数据流向简化:

      数据直接从磁盘 → 页缓存(DMA) → Socket缓冲区(内核直接拷贝) → 网卡(DMA)

      消除了两次CPU拷贝 。

      SG-DMA技术:在支持分散-聚集DMA技术的系统上,数据可以直接从页缓存传输到网卡,连Socket缓冲区的拷贝都消除了,实现真正的零CPU拷贝

相关推荐
苏三的开发日记4 小时前
Zookeeper实现分布式锁的原理
后端
王景程4 小时前
让IOT版说话
后端·python·flask
苏三的开发日记4 小时前
Redis实现分布式锁的原理
后端
阿豪啊4 小时前
Prisma ORM 入门指南:从零开始的全栈技能学习之旅
javascript·后端·node.js
optimistic_chen5 小时前
【Java EE进阶 --- SpringBoot】统一功能处理(拦截器)
spring boot·后端·java-ee·log4j·拦截器
苏三的开发日记5 小时前
什么是幂等,幂等如何实现
后端
zyh200504305 小时前
RabbitMQ概述
分布式·消息队列·rabbitmq·消息中间件·amqp
逻极6 小时前
变量与可变性:Rust中的数据绑定
开发语言·后端·rust
一缕茶香思绪万堵6 小时前
028.爬虫专用浏览器-抓取#shadowRoot(closed)下
java·后端