**摘要:**本文详解 Kafka 核心架构、生产消费全流程,剖析分区、副本、Offset 管理等核心机制,及高性能、高可用的优化策略与实践方案。


Kafka 基础架构

Kafka 核心组件围绕消息的生产、存储、消费全链路设计,各组件分工明确、层级关联,共同实现高可用、高可扩展的分布式消息传递,核心组件及关联关系如下:
Producer(生产者):消息发送的客户端,是 Kafka 消息链路的起点,负责将业务消息发送至 Kafka 集群的指定 Topic。
Topic(消息主题):消息的逻辑存储容器,是生产者发送、消费者消费的统一操作对象,可理解为 "消息队列的逻辑标识"。
Partition(分区):Topic 的物理拆分单元,是 Kafka可扩展和并行消费的核心。每个 Topic 包含一个或多个 Partition,每个 Partition 是独立的有序队列,消息一旦写入便会分配唯一偏移量(Offset)且不可修改;Partition 分布在不同的 Broker 上,实现 Topic 的分布式存储。
Replica(副本):Partition 的高可用保障单元,每个 Partition 对应若干个 Replica,副本分布在不同 Broker 上,避免单服务器故障导致的消息丢失。
Leader(主副本):每个 Partition 所有副本中的 "主节点",是生产和消费的唯一交互对象, 生产者消息仅发至Leader,消费者仅从Leader读消息,Follower 不参与生产消费流程。
Follower(从副本):每个 Partition 所有副本中的 "从节点",核心职责是实时从 Leader 同步数据,保持与 Leader 的数据一致性;当 Leader 发生故障时,Kafka 集群会从 Follower 中选举新的 Leader,保障 Partition 的服务连续性。
Broker(服务节点):Kafka 集群的独立服务器节点,一个 Kafka 集群由多个 Broker 组成;每个 Broker 可存储多个 Topic 的 Partition 及副本,是消息存储和集群调度的物理载体。
Consumer(消息消费者):消息读取的客户端,是 Kafka 消息链路的终点,负责从 Kafka 集群的指定 Topic 读取消息并处理,仅与 Leader 副本交互。
Consumer Group(消费者组):消费者的逻辑组织单元,所有 Consumer 必须归属某个消费者组,一个消费者组可包含多个 Consumer,是 Kafka并行消费的核心。其核心消费规则为:组内消费者分工消费同一个 Topic 的不同 Partition,一个 Partition 只能被一个组内 Consumer 消费;消费者组之间相互独立,互不影响,可同时消费同一个 Topic 的消息。
Kafka生产者
生产者消息发送流程

在消息发送的过程中,涉及到了两个线程------main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator, Sender 线程不断从RecordAccumulator 中拉取消息发送到Kafka Broker。
Kafka 发送消息详细流程
-
生产者主线程处理阶段
-
初始化发送 :生产者调用
send(ProducerRecord)方法,发起消息发送请求。 -
拦截器处理 :消息首先经过
Interceptors,可以在此处对消息进行自定义的预处理或监控。 -
序列化 :
Serializer(序列化器)将消息键和值从对象转换为字节数组,方便网络传输和存储。 -
分区路由 :
Partitioner根据消息的键或默认规则,决定该消息要发送到哪个主题的哪个分区。
-
消息累积与批量处理阶段
-
消息入队 :经过分区后的消息,被放入
RecordAccumulator(消息累加器)中对应分区的Deque(双端队列)里。 -
批量触发条件:
- 按大小触发 :当单个
ProducerBatch(消息批次)的大小达到batch.size(默认 16KB)时,触发发送。 - 按时间触发 :如果消息迟迟未达到
batch.size,则等待linger.ms(默认 0ms,即无延迟)后,无论大小都会触发发送。
- 按大小触发 :当单个
-
清理机制:累加器会定期清理超时的消息批次,避免内存占用过高。
-
Sender 线程发送阶段
-
读取批量数据 :
Sender线程从RecordAccumulator中取出准备好的ProducerBatch。 -
构建网络请求 :
NetworkClient将消息批次封装成Request请求,并维护InFlightRequests(默认每个 Broker 最多缓存 5 个请求)来跟踪未完成的请求。 -
发送与选择器轮询 :
Selector(选择器)负责网络 I/O 的多路复用,将请求发送到 Kafka 集群的 Broker 节点,并等待响应。
-
Broker 集群接收与应答阶段
-
Broker 接收消息:消息发送到目标分区的 Leader 副本所在的 Broker 节点(如 Broker1 或 Broker2)。
-
重试机制 :如果发送失败(如网络异常、Broker 无响应),会根据
retries配置进行重试。 -
应答确认(acks):
- acks=0:生产者无需等待 Broker 应答,直接认为发送成功,性能最高但可能丢数据。
- acks=1:仅等待 Leader 副本接收并写入消息后应答,可靠性中等。
- acks=-1(all):等待 Leader 副本和所有 ISR(同步副本)队列中的节点都接收并写入消息后才应答,可靠性最高。
- 发送完成与后续处理
当收到 Broker 的确认应答后,Sender 线程会更新 RecordAccumulator 中的批次状态,释放对应的内存,并可以通过回调函数通知生产者发送结果。
异步发送 API

普通异步发送
需求:创建Kafka 生产者,采用异步的方式发送到 Kafka Broker

带回调函数的异步发送
回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元数据信息(RecordMetadata)和异常信息(Exception),如果 Exception 为 null,说明消息发送成功,如果Exception 不为 null,说明消息发送失败。

同步发送 API
只需在异步发送的基础上,再调用一下 get()方法即可。

生产者分区
分区好处

生产者发送消息的分区策略
默认的分区器 DefaultPartitioner


消息分区策略
- 指定分区(partition)时
当发送消息时显式指定了 partition 参数:
- 消息会被直接发送到指定的分区
- 例如:若指定
partition=0,所有数据都会写入分区 0
- 未指定分区,但有 key 时
当发送消息时没有指定 partition,但提供了 key:
- Kafka 会计算
key的哈希值 - 将哈希值与主题的分区数进行取余运算
- 取余结果即为消息要发送到的分区编号
- 例如:
key1的哈希值为 5,key2的哈希值为 6- 主题分区数为 2
key1对应5 % 2 = 1,写入分区 1key2对应6 % 2 = 0,写入分区 0
- 既无分区也无 key 时
当发送消息时既没有指定 partition,也没有提供 key:
- Kafka 使用 Sticky Partition(黏性分区器)
- 首先随机选择一个分区,并尽可能持续使用该分区
- 直到该分区的
batch已满(默认 16KB)或linger.ms超时 - 再随机选择一个新的分区(与上一次不同)
- 例如:第一次随机选择分区 0,等该批次满了或超时后,再随机切换到另一个分区
自定义分区器
如果研发人员可以根据企业需求,自己重新实现分区器。例如我们实现一个分区器实现,发送过来的数据中如果包含 atguigu,就发往 0 号分区,不包含 atguigu,就发往 1 号分区。
实现步骤:定义类实现Partitioner 接口,并重写partition()方法。
自定义分区器

关联自定义分区器

生产者如何提高吞吐量

- 增大批次大小(
batch.size)
- 默认值为 16KB
- 增大该值可以让生产者在一个批次中积累更多消息,减少网络请求次数
- 提高传输效率,从而提升吞吐量
- 设置适当的等待时间(
linger.ms)
- 默认值为 0ms(即消息立即发送)
- 建议调整为 5--100ms
- 让生产者在发送前等待一小段时间,积累更多消息后再批量发送
- 这可以显著减少网络 I/O 次数,提升吞吐量
- 启用消息压缩(
compression.type)
- 推荐使用 snappy 压缩算法
- 压缩可以减小消息体积,降低网络传输带宽和 Broker 存储压力
- 虽然会增加 CPU 开销,但整体吞吐量通常会得到提升
- 增大缓冲区大小(
RecordAccumulator)
- 默认值为 32MB
- 建议调整为 64MB
- 更大的缓冲区可以让生产者在高并发场景下缓存更多消息

数据可靠性

ACK 应答级别
- acks=0
- 行为:生产者发送数据后,不需要等待 Broker 的任何应答,直接认为发送成功。
- 特点:性能最高,但数据可靠性最低,可能出现数据丢失。
- 适用场景:对数据可靠性要求极低,但追求极致性能的场景。
- acks=1
- 行为:生产者发送数据后,仅等待分区的 Leader 副本成功接收并写入消息,就会收到应答。
- 特点:性能与可靠性中等。如果 Leader 接收后还未同步给 Follower 就宕机,会导致数据丢失。
- 适用场景:对性能有一定要求,且能接受少量数据丢失的场景。
- acks=-1(all)
- 行为:生产者发送数据后,需要等待 Leader 副本和 ISR(同步副本)队列中所有节点都成功接收并写入消息,才会收到应答。
- 特点:数据可靠性最高,但性能也相对最低。
- 思考点 :如果某个 Follower 因故障无法同步,可通过
min.insync.replicas配置确保 ISR 中至少有足够数量的副本正常,避免影响可用性。 - 适用场景:对数据可靠性要求极高,不允许任何数据丢失的场景。

数据完全可靠条件 = ACK 级别设置为 - 1 + 分区副本 ≥ 2 + ISR 里应答的最小副本数量 ≥ 2

数据去重

1. 至少一次(At Least Once)
- 配置条件 :
ACK级别设置为-1+分区副本数 ≥ 2+ISR里应答的最小副本数量 ≥ 2 - 特点:可以保证数据不丢失,但无法避免数据重复。
2. 最多一次(At Most Once)
- 配置条件 :
ACK级别设置为0 - 特点:可以保证数据不重复,但可能发生数据丢失。
3. 精确一次(Exactly Once)
- 适用场景:对数据完整性要求极高的场景(如金融交易),要求数据既不丢失也不重复。
- 实现方式 :Kafka 0.11 版本后,通过幂等性和事务特性来实现。
至少一次(保证数据不丢失) + 幂等性(保证不重复消费)= 保证数据不重复和不丢失

使用幂等性:开启参数 enable.idempotence 默认为 true,false 关闭。
生产者事务
Kafka 事务原理


java
//初始化事务
kafkaProducer.initTransactions();
//开启事务
kafkaProducer.beginTransaction();
//提交事务
kafkaProducer.commitTransaction();
//终止事务(回滚)
kafkaProducer.abortTransaction();
数据有序


1.x 版本之前
要保证单分区数据有序,必须将参数设置为:max.in.flight.requests.per.connection = 1
- 这个配置限制每个连接同时只能有 1 个请求在飞行中
- 无论是否开启幂等性,都需要设置该值
1.x 及以后版本
根据是否开启幂等性,有不同的配置方案:
1. 未开启幂等性
- 需要设置
max.in.flight.requests.per.connection = 1 - 原理和 1.x 版本之前一致,通过强制串行发送来保证顺序
2. 开启幂等性
- 需要设置
max.in.flight.requests.per.connection ≤ 5 - 原理:Kafka 服务端会缓存生产者发来的最近 5 个请求的元数据
- 即使出现请求乱序到达的情况,服务端也能对这 5 个请求重新排序,从而保证数据有序
Kafka Broker
Zookeeper 存储的 Kafka 信息

Kafka Broker 工作流程

Broker 启动与注册
- Broker 注册 :Broker 启动后,会在 Zookeeper 的
/brokers/ids节点下注册自己的 Broker ID(如[0,2])。 - Controller 选举 :所有 Broker 会向 Zookeeper 的
/controller节点竞争注册,谁先注册成功谁就成为集群的 Controller(如brokerid:0)。
Controller 核心职责
- Broker 节点监听 :Controller 会持续监听 Zookeeper 上
/brokers/ids节点的变化,感知集群中 Broker 的上下线。 - Leader 副本选举:当需要为分区选举 Leader 时,Controller 会以 ISR(同步副本)中的节点为存活前提,按照 AR(所有副本)列表的顺序,选择排在最前面的节点作为 Leader。
- 信息同步到 ZK :Controller 会将分区的 Leader 和 ISR 信息(如
leader:0, isr:[0,2])更新到 Zookeeper 的/brokers/topics/{topic}/partitions/{partition}/state节点。 - 其他 Controller 同步:集群中其他 Broker 会监听 Zookeeper 上的节点变化,同步最新的 Leader 和 ISR 信息。
Broker 故障与恢复
- Broker 故障触发:假设 Broker1 宕机,其负责的分区 Leader(如 TopicA-Partition0 的 Leader)随之失效。
- Controller 感知变化:Controller 通过 Zookeeper 监听到 Broker1 下线的事件。
- 获取当前 ISR:Controller 从 Zookeeper 中获取该分区当前的 ISR 列表。
- 选举新 Leader :在 ISR 列表中,按照 AR 顺序选择新的 Leader(例如从 ISR
[0,2]中选择 Broker0)。 - 更新 Leader 与 ISR:Controller 将新的 Leader 和 ISR 信息更新到 Zookeeper,其他 Broker 同步该信息,完成故障转移。
正常消息收发
- 生产者发送消息:生产者将消息发送到目标分区的 Leader 副本所在的 Broker。
- Broker 持久化与应答 :Leader 副本将消息写入本地的
.log和.index文件,并在满足 ACK 条件后向生产者返回应答。 - Follower 副本同步:Follower 副本会从 Leader 副本拉取消息并同步,保持与 Leader 的数据一致,维护在 ISR 列表中。
节点服役和退役
服役新节点
1. 新节点准备
- 关闭hadoop104,并右键执行克隆操作。
- 开启hadoop105,并修改 IP 地址。
- 在 hadoop105 上,修改主机名称为hadoop105。
- 重新启动hadoop104、hadoop105。
- 修改haodoop105 中 kafka 的 broker.id 为 3。
- 删除hadoop105 中 kafka 下的 datas 和 logs。
- 启动hadoop102、hadoop103、hadoop104 上的 kafka 集群。
- 单独启动hadoop105 中的 kafka。
2. 执行负载均衡操作
- 创建一个要均衡的主题。
- 生成一个负载均衡的计划。
- 创建副本存储计划(所有副本存储在 broker0、broker1、broker2、broker3 中)。
- 执行副本存储计划。
- 验证副本存储计划。
退役旧节点
1. 旧节点准备
- 关闭待退役节点(如 hadoop104,对应 broker.id=3)的 kafka 服务。
- 查看集群当前所有主题的分区副本分布,确认待退役节点承载的副本信息。
- 确保集群其余节点(hadoop102、103、105)kafka 服务正常运行,状态健康。
- 备份待退役节点 kafka 的 datas 和 logs 目录,防止数据丢失。
- 确认集群副本配置满足退役后可用性(如副本数≥2,剩余节点可承接副本)。
2. 执行副本迁移操作
- 生成旧节点副本迁移计划(将 broker.id=3 上的所有副本迁移至其余可用节点)。
- 创建副本重分配计划(指定副本仅存储在剩余可用 broker 中,如 broker0、1、2)。
- 执行副本重分配计划,等待集群完成数据同步。
- 验证副本迁移结果,确认待退役节点无任何分区副本。
3. 旧节点下线操作
- 再次确认待退役节点无集群元数据和数据副本,无业务请求接入。
- 彻底关闭待退役节点的 kafka 服务,禁止其重新启动接入集群。
- 删除集群中待退役节点的相关配置(如主机映射、zk 中 broker 注册信息)。
- 验证集群整体状态,所有主题分区 Leader 正常、ISR 列表完整,业务读写无异常。
Kafka 副本
副本基本信息
1)Kafka 副本作用:提高数据可靠性。
2)Kafka 默认副本 1 个,生产环境一般配置为 2 个,保证数据可靠性;太多副本会增加磁盘存储空间,增加网络上数据传输,降低效率。
3)Kafka 中副本分为:Leader 和 Follower。Kafka 生产者只会把数据发往 Leader,然后 Follower 找Leader 进行同步数据。
4)Kafka 分区中的所有副本统称为 AR(Assigned Repllicas)。 AR = ISR + OSRISR,表示和 Leader 保持同步的 Follower 集合。如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由 replica.lag.time.max.ms参数设定,默认 30s。Leader 发生故障之后,就会从ISR 中选举新的Leader。
**OSR:**表示Follower 与Leader 副本同步时,延迟过多的副本。
Leader 选举流程

Follower 故障处理细节

Kafka Follower 故障处理流程
1. 故障触发与临时踢除
- 当某个 Follower 副本发生故障时,它会被临时踢出 ISR(同步副本列表)。
- 此时,集群会基于剩余的同步副本继续提供服务。
2. 故障期间的集群行为
- Leader 副本会继续接收并处理生产者的消息,正常写入新数据,LEO(日志末端偏移量)持续增长。
- 故障的 Follower 无法再从 Leader 同步数据,它的 LEO 会停留在故障前的位置。
3. Follower 恢复与数据截断
- Follower 节点恢复后,会读取本地磁盘记录的上次 HW(高水位)。
- 它会将本地日志文件中高于 HW 的部分数据截断,以保证自己的数据与集群的已提交数据一致。
- 之后,该 Follower 从 HW 位置开始,向 Leader 拉取并同步后续的消息。
4. 重新加入 ISR
- 当该 Follower 的 LEO(日志末端偏移量)大于等于该分区的 HW,即完全追上 Leader 的数据后,就会被重新加入 ISR 列表。
- 此时,该 Follower 恢复为正常的同步副本,重新具备参与 Leader 选举的资格。
Leader故障处理细节

Kafka Leader 故障处理流程
1. 故障触发与新 Leader 选举
- 当 Leader 发生故障时,Controller 会从该分区的 ISR(同步副本列表) 中选出一个新的 Leader。
- 选举规则是在 ISR 存活节点中,按照 AR(所有副本)列表的顺序选择最靠前的节点。
2. 数据一致性保障:截断与同步
- 为了保证多个副本之间的数据一致性,其余 Follower 会先将各自日志文件中 高于 HW的部分截断。
- 截断完成后,这些 Follower 会从新的 Leader 同步后续数据,确保所有副本的数据与新 Leader 保持一致。
3. 注意事项
- 这个流程只能保证副本之间的数据一致性,并不能保证数据不丢失或不重复。
- 如果原 Leader 在故障前已写入但未同步到 Follower 的数据(即高于 HW 的部分),会被截断,从而导致这部分数据丢失。
分区副本分配
当你的 Kafka 集群有 4 个节点,且分区数大于节点数时,副本会遵循特定的分配策略来保证高可用和负载均衡。
1. 核心分配原则
Kafka 会在集群节点间尽可能均匀地分配副本,以实现负载均衡和高可用性。
- 副本分散存放:同一个分区的多个副本不会放在同一个 Broker 上,避免单点故障导致数据完全丢失。
- 均匀分布:集群会将所有分区的 Leader 和 Follower 副本均匀分布在不同 Broker 上,防止个别节点压力过大。
2. 当分区数大于 Broker 数时的分配逻辑
以 4 个 Broker(0,1,2,3)、主题副本因子为 3、分区数为 6 为例:
| 分区号 | Leader 副本 | Follower 副本 1 | Follower 副本 2 |
|---|---|---|---|
| 0 | 0 | 1 | 2 |
| 1 | 1 | 2 | 3 |
| 2 | 2 | 3 | 0 |
| 3 | 3 | 0 | 1 |
| 4 | 0 | 1 | 2 |
| 5 | 1 | 2 | 3 |
分配特点:
- 循环分配 Leader:分区 0 的 Leader 在 Broker 0,分区 1 在 Broker 1,依此类推,循环分配以平衡 Leader 负载。
- 副本依次后移:每个分区的 Follower 副本会按 Broker ID 顺序向后选取,确保同一个分区的副本分散在不同节点上。
- 超出 Broker 数时复用节点:当分区数超过 Broker 数时,分配逻辑会从第一个 Broker 开始循环复用节点,保证所有节点的负载尽可能均匀。
3. 额外说明
- 副本因子的限制:副本因子不能超过集群的 Broker 数量,否则会因无法满足副本分散存放的要求而报错。
- 自动与手动分配 :默认情况下,Kafka 会自动执行上述分配策略。你也可以通过
kafka-reassign-partitions.sh工具手动调整副本分布。 - 优先副本选举:Kafka 会定期执行 "优先副本选举",让 AR 列表中排在第一位的副本成为 Leader,以长期维持集群的负载均衡。
手动调整分区副本存储


Leader Partition 负载平衡

增加副本因子

文件存储
文件存储机制
1. Topic 数据的存储机制

2. index 文件和 log 文件详解

文件清理策略


日志保存时间配置
Kafka 默认日志保存时间为 7 天,可通过以下参数调整,优先级从高到低为:
log.retention.ms:最高优先级,单位为毫秒。log.retention.minutes:单位为分钟。log.retention.hours:最低优先级,默认值为 168(即 7 天)。log.retention.check.interval.ms:设置检查周期,默认 5 分钟。
日志清理策略
Kafka 提供两种核心清理策略:
1. delete(删除策略)
配置方式 :log.cleanup.policy = delete(默认策略)
- 核心逻辑:直接删除过期或超出大小限制的数据。
- 触发条件:
- 基于时间 (默认开启):以
segment文件中所有记录的最大时间戳作为该文件的时间戳,当超过配置的保存时间时删除。 - 基于大小 (默认关闭):当所有日志总大小超过
log.retention.bytes(默认值为-1,表示无限制)时,删除最早的segment。
- 基于时间 (默认开启):以
2. compact(压缩策略)
- 配置方式 :
log.cleanup.policy = compact - 核心逻辑 :对相同
key的消息只保留最新版本,适合需要保留最新状态的场景。
高效读写数据
- Kafka 本身是分布式集群,可以采用分区技术,并行度高
- 读数据采用稀疏索引,可以快速定位要消费的数据
- 顺序写磁盘

Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
4. 页缓存 + 零拷贝技术

Kafka 的零拷贝技术是和 PageCache(页缓存) 配合实现的,核心是跳过应用层(用户态)的缓存拷贝,让数据直接在内核态的 PageCache 和网卡之间传输,从而减少 CPU 开销和内存占用。
非零拷贝与零拷贝的流程对比
传统非零拷贝流程之所以会经历多次拷贝,本质是由操作系统的安全隔离设计 和早期应用的功能需求共同决定的,我们可以从这几个角度来理解:
用户态与内核态的安全隔离
- 操作系统将内存分为用户态 (应用程序可直接访问)和内核态(仅操作系统可访问),这是为了防止应用程序直接操作硬件或破坏系统数据。
- 数据在用户态和内核态之间无法直接传输,必须通过 CPU 拷贝作为 "中转",这就导致了 PageCache → Application Cache → Socket Cache 的两次 CPU 拷贝。
1. 非零拷贝流程(性能瓶颈)
- 数据从磁盘读取到内核态的 PageCache
- 数据从 PageCache 拷贝到 Kafka 应用层的 Application Cache(用户态)
- 数据从 Application Cache 拷贝到内核态的 Socket Cache
- 数据从 Socket Cache 拷贝到网卡(NIC)发送给消费者
过程产生 4 次数据拷贝 和多次上下文切换,其中第 2、3 步是 CPU 参与的拷贝,消耗大量资源。
2. 零拷贝流程(Kafka 实际使用)
- 数据从磁盘读取到内核态的 PageCache(DMA 拷贝,无 CPU 参与)
- 数据直接从 PageCache 通过 sendfile() 系统调用传递给网卡(仅拷贝数据描述符,无 CPU 拷贝)
过程仅需 2 次 DMA 拷贝 和2 次上下文切换,彻底跳过了用户态的缓存拷贝,大幅提升传输效率。
| 维度 | CPU 拷贝 | DMA 拷贝 |
|---|---|---|
| 资源占用 | 高,CPU 全程参与 | 低,仅 DMA 控制器参与,CPU 可并行处理其他任务 |
| 适用场景 | 需要对数据进行计算或修改的场景 | 纯数据转发、无需修改的场景(如 Kafka 日志传输) |
| 效率 | 较低,CPU 开销大 | 较高,CPU 资源得以释放 |
Kafka 消费者
Kafka 消费方式

Kafka 采用的消费方式:Pull(拉模式)
- 核心逻辑:消费者主动从 Broker 拉取数据,消费速度由消费者自身控制。
- 优势:
- 适配不同消费能力:消费者可以根据自身处理速度来决定拉取频率和数量,避免被 Broker 推送的速度压垮。
- 灵活的消费策略:支持按时间、偏移量等方式回溯消费,更适合 Kafka 这类高吞吐场景。
- 不足:如果 Broker 中没有新数据,消费者可能会陷入循环空轮询,持续返回空数据。
Kafka 不采用的消费方式:Push(推模式)
- 核心逻辑:Broker 主动将消息推送给消费者,推送速度由 Broker 决定。
- 劣势:
- 无法适配差异:Broker 无法感知每个消费者的处理能力,容易出现推送速度过快,导致消费者过载、消息堆积的问题。
- 缺乏灵活性:消费者无法自主控制消费节奏,难以应对突发流量或业务波动
Kafka 消费者工作流程
消费者总体工作流程

1. 分区与消费者的映射规则
- 每个分区的数据只能由消费者组中的一个消费者消费,避免重复消费。
- 一个消费者可以消费多个分区的数据,以实现负载均衡。
2. Offset 管理
- 每个消费者的消费进度(Offset)会被提交到 Kafka 内部主题
__consumer_offsets中保存。 - Offset 记录了消费者在每个分区上已经消费到的位置,重启后可以从上次的位置继续消费。
消费者组原理


消费者组初始化流程

核心角色
- Coordinator(协调器) :辅助实现消费者组初始化和分区分配,由
groupid的hashcode % 50计算得出,对应__consumer_offsets主题的一个分区所在的 Broker。 - Leader Consumer(消费者组 Leader):由组内选举产生,负责制定分区分配方案。
完整初始化流程
- 发送加入请求: 每个消费者向 Coordinator 发送
JoinGroup请求,申请加入消费者组。 - **选举 Leader 消费者:**Coordinator 从组内选出一个消费者作为 Leader。
- **同步 Topic 信息:**所有消费者将自己要消费的 Topic 情况发送给 Leader 消费者。
- **制定消费方案:**Leader 消费者根据 Topic 分区数量和消费者数量,制定分区分配方案。
- **提交消费方案:**Leader 将分配方案发送给 Coordinator。
- **下发消费方案:**Coordinator 将最终的消费方案分发给组内所有消费者。
- 心跳与存活检测**:** 每个消费者会定期与 Coordinator 保持心跳(默认 3s),如果超时(
session.timeout.ms=45s)或消费处理超时(max.poll.interval.ms=5分钟),该消费者会被移除并触发重平衡。
消费者组详细消费流程 
一、发送消费请求
- 消费者通过
ConsumerNetworkClient向目标分区的 Leader 副本发送拉取请求(sendFetches)。 - 拉取行为受以下参数控制:
fetch.min.bytes:每批次最小抓取大小,默认 1 字节。fetch.max.wait.ms:若数据量未达到fetch.min.bytes,最长等待时间,默认 500ms。fetch.max.bytes:每批次最大抓取大小,默认 50MB。
二、接收并缓存拉取结果
- Broker 返回的消息数据会被存入消费者端的
completedFetches队列。 - 消费者会从该队列中获取
FetchedRecords进行后续处理。
三、消息处理流程
- 反序列化(
parseRecord):将 Broker 返回的二进制数据解析为业务可识别的消息格式。 - 拦截器处理(
Interceptors):通过自定义拦截器对消息进行前置处理(如日志记录、数据过滤)。 - 业务处理:消费者对消息执行具体的业务逻辑。
- 单次拉取返回的最大消息条数由
max.poll.records控制,默认 500 条。
四、消费进度提交
在消息处理完成后,消费者会将当前分区的 Offset 提交到 __consumer_offsets 主题,以记录消费进度,确保重启后可以继续消费。
消费者 API
消费者案例(订阅主题)
java
public class CustomConsumer {
public static void main(String[] args) {
// 1.创建消费者的配置对象
Properties properties = new Properties();
// 2.给消费者配置对象添加参数 properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
"hadoop102:9092");
// 配置序列化 必须 properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 配置消费者组(组名任意起名) 必须
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// 创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 注册要消费的主题(可以消费多个主题) ArrayList<String> topics = new ArrayList<>(); topics.add("first"); kafkaConsumer.subscribe(topics);
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
// 打印消费到的数据
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
消费者案例(订阅分区)
java
public class CustomConsumerPartition {
public static void main(String[] args){
Properties properties = new Properties();
// 配置序列化(必须)
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 配置消费者组(必须),名字可以任意起
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
// 消费某个主题的某个分区数据
ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
topicPartitions.add(new TopicPartition("first", 0)); kafkaConsumer.assign(topicPartitions);
while (true){
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
分区的分配以及再平衡

一、核心背景
- 一个消费者组由多个消费者组成,一个主题包含多个分区,需要决定哪个消费者来消费哪个分区的数据。
- Kafka 提供了四种主流分区分配策略:
Range、RoundRobin、Sticky、CooperativeSticky。 - 默认策略是
Range + CooperativeSticky,可通过参数partition.assignment.strategy修改。
二、分区分配流程
- 发送加入请求 :每个消费者向 Coordinator 发送
JoinGroup请求,申请加入消费者组。 - 选举 Leader 消费者:Coordinator 从组内选出一个消费者作为 Leader。
- 同步 Topic 信息:所有消费者将自己要消费的 Topic 情况发送给 Leader 消费者。
- 制定分配方案:Leader 根据选定的分配策略,制定分区与消费者的映射方案。
- 提交分配方案:Leader 将分配方案发送给 Coordinator。
- 下发分配方案:Coordinator 将最终的分配方案分发给组内所有消费者。
三、再平衡触发场景
再平衡是指消费者组内重新分配分区的过程,触发条件包括:
- 消费者心跳超时 :消费者与 Coordinator 保持心跳(默认 3s),若超时(
session.timeout.ms=45s),该消费者会被移除并触发再平衡。 - 消费处理超时 :消费者处理消息的时间过长(
max.poll.interval.ms=5分钟),会触发再平衡。 - 消费者组成员变化:有新消费者加入或现有消费者下线。
- 主题分区数量变化:主题的分区数增加。
四、主流分配策略简介
- Range:按主题分区序号范围分配,容易导致负载不均衡。
- RoundRobin:轮询分配所有主题的分区,负载更均衡。
- Sticky:优先保持现有分配,再平衡时尽量减少分区移动,减少重复消费。
- CooperativeSticky:增量式再平衡,无需暂停所有消费者,减少业务中断。
Range分区策略

核心规则
Range 针对单个 Topic 独立计算分配,核心逻辑是按范围划分分区。
1. 排序准备
- 先将当前 Topic 的所有分区按分区序号升序排列(例如:0,1,2,3,4,5,6)。
- 再将消费者组内的消费者按消费者名称字母顺序排序(例如:C0,C1,C2)。
2. 计算分配数量
- 用
分区总数 ÷ 消费者数量计算每个消费者基础分配的分区数。 - 如果无法整除,排在前面的消费者会多分配 1 个分区。
- 例:7 个分区、3 个消费者 → 7 ÷ 3 = 2 .. 1 → C0 分配 3 个分区,C1、C2 各分配 2 个分区。
3. 范围分配
- 按计算结果,将连续的分区范围分配给对应消费者。
- 示例中:
- C0 消费分区
0,1,2 - C1 消费分区
3,4 - C2 消费分区
5,6
- C0 消费分区
优缺点
优点:逻辑简单,计算高效;对于单个 Topic,负载不均衡的影响相对较小。
缺点:多 Topic 场景易引发数据倾斜,如果消费多个 Topic,每个 Topic 都会按此规则分配,导致排序靠前的消费者(如 C0)在每个 Topic 中都多分配 1 个分区。Topic 数量越多,倾斜越严重。
RoundRobin 分区策略


核心规则
RoundRobin 是一种跨 Topic 的全局轮询分配策略,它针对集群中所有被消费的 Topic 统一计算分配。
-
全局收集与排序
- 收集消费者组订阅的所有 Topic 的分区,以及组内所有消费者。
- 对所有分区和消费者分别按
hashcode进行排序。
-
轮询分配
- 将排序后的分区列表,按顺序依次轮询分配给排序后的消费者列表。
- 例如,7 个分区、3 个消费者时,分配结果为:
- Consumer1 → Partition-0、Partition-3、Partition-6
- Consumer2 → Partition-1、Partition-4
- Consumer3 → Partition-2、Partition-5
优点
- 全局负载均衡:在多 Topic 场景下,能让所有消费者的分区数量尽可能均匀,避免了 Range 策略的多 Topic 数据倾斜问题。
- 跨 Topic 公平性:对于消费多个 Topic 的场景,分配结果更公平。
缺点
- 依赖全局排序:如果消费者订阅的 Topic 不一致,会导致部分分区无法分配,降低分配效率。
- 分区分散:轮询会让单个消费者的分区分散在不同 Broker 上,可能增加网络开销。
Sticky分区策略
Sticky(粘性)分区分配策略是 Kafka 为解决再平衡时重复消费问题而设计的,核心目标是在保证负载均衡的同时,尽可能保留原有分区分配关系,减少再平衡时的分区移动。
核心规则
首次分配:均衡优先
- 首次分配时,Sticky 会像 RoundRobin 一样,在消费者之间均匀分配所有分区,保证初始负载均衡。
再平衡时:粘性优先
- 当消费者组发生再平衡(如消费者上下线)时,Sticky 会优先保留现有消费者已分配的分区,仅对需要调整的部分进行最小化移动。
- 例如,若原有分配为:
C0: [P0,P1]、C1: [P2,P3],当 C0 下线时,不会将 P0、P1 全部分配给 C1,而是会优先分配给新加入或剩余的消费者,同时尽量保证负载均衡。
最小化重复消费
- 由于分区移动被最小化,消费者不需要重新消费已处理过的分区数据,大幅降低了再平衡期间的重复消费风险。
优点
- 减少重复消费:再平衡时仅调整必要的分区,避免了全量重新分配导致的重复消费。
- 负载均衡与粘性兼顾:在保证负载均衡的同时,尽可能保留原有分配关系,提升消费效率。
- 适合高吞吐场景:对于对重复消费敏感、要求高可用的业务场景非常友好。
缺点
- 算法复杂度高:需要跟踪历史分配关系,计算逻辑比 Range、RoundRobin 更复杂。
- 依赖全局状态:需要维护消费者与分区的映射历史,对 Coordinator 的性能有一定要求。
CooperativeSticky 分区策略
CooperativeSticky(协作粘性)是 Kafka 2.4+ 版本推出的分区分配策略,是 Sticky 策略的增强版,核心目标是实现增量式再平衡,避免全量分区重新分配,从而大幅降低业务中断时间。
核心规则
**首次分配,均衡优先:**首次分配时,与 Sticky 策略逻辑一致,会在消费者之间均匀分配所有分区,保证初始负载均衡。
再平衡时,增量协作调整: 再平衡触发时,不会让所有消费者暂停并全量重新分配,而是采用增量协作方式:
- 仅调整受影响的分区(如因消费者上下线而空闲的分区),并将其分配给需要补充分区的消费者。
- 未受影响的消费者可以继续消费原有分区,无需暂停,业务几乎无中断。
**粘性保留与负载均衡兼顾:**在增量调整的同时,尽可能保留消费者与原有分区的映射关系,最小化重复消费。最终分配结果既保证了负载均衡,又维持了粘性。
优点
- 无业务中断:再平衡期间,未受影响的消费者无需暂停,业务可继续消费。
- 最小化重复消费:仅移动必要的分区,大幅降低重复消费的范围和数据量。
- 支持弹性伸缩:非常适合云原生场景下消费者组的动态扩缩容。
缺点
- 算法复杂度更高:需要维护更精细的分区分配状态,对 Coordinator 性能要求较高。
- 兼容性限制:需要消费者客户端和 Broker 都升级到 Kafka 2.4+ 版本才能支持。
offset 位移
offset 的默认维护位置

自动提交 offset

什么是自动提交 Offset
自动提交 Offset 是 Kafka 提供的一个默认功能,指消费者会定期自动把当前消费到的分区位置(Offset)提交到 Kafka 内部主题 __consumer_offsets 中,无需业务代码手动干预。
为什么要做自动提交 Offset
- 简化开发:让开发者无需关心消费进度的持久化,专注于业务逻辑处理,降低代码复杂度。
- 保证消费连续性:当消费者重启或发生再平衡时,新的消费者可以从已提交的 Offset 位置继续消费,避免从头开始重复消费。
- 默认高可用:通过定期提交(默认 5 秒),在系统故障时能最大程度保留消费进度,减少数据重复处理的风险。
核心配置参数
enable.auto.commit:是否开启自动提交,默认值为true。auto.commit.interval.ms:自动提交的时间间隔,默认值为5000毫秒(即 5 秒)。
工作流程
- 消费者持续从 Broker 拉取消息并进行业务处理。
- 每经过
auto.commit.interval.ms配置的时间(默认 5 秒),消费者自动将当前的 Offset 提交到__consumer_offsets主题。 - 当消费者重启或再平衡时,新的消费者会从
__consumer_offsets中读取已提交的 Offset,从该位置继续消费。
手动提交 offset


什么是手动提交 Offset
手动提交 Offset 是指开发者通过代码主动控制消费进度(Offset)的提交时机,而不是依赖系统自动定期提交。它提供了比自动提交更精细的控制,适合对数据一致性要求高的场景。
为什么需要手动提交
自动提交是基于时间的,开发者无法精确控制提交时机,可能会导致:
- 数据丢失:如果自动提交后,消费者在处理消息时发生故障,已提交但未处理完成的消息会丢失。
- 重复消费:如果在处理完成前就自动提交,重启后会从已提交的位置继续消费,导致部分消息被重复处理。
手动提交让开发者可以在确认消息处理完成后再提交 Offset,从而避免这些问题。
两种手动提交方式
1. commitSync(同步提交)
- 核心逻辑:提交请求发出后,会阻塞当前线程,直到提交成功或失败(失败会自动重试)。
- 特点:必须等待 Offset 提交完毕,才会继续消费下一批数据。
- 适用场景:对数据一致性要求高的场景,如金融交易、支付等,确保 Offset 提交成功后再处理下一批。
2. commitAsync(异步提交)
- 核心逻辑:提交请求发出后,立即返回,不会阻塞线程,也不会自动重试。
- 特点:发送提交请求后,就开始消费下一批数据,性能更高,但存在提交失败的风险。
- 适用场景:对性能要求高、能容忍少量重复消费的场景,如日志收集、实时监控等。
工作流程
- 消费者持续从 Broker 拉取消息并进行业务处理。
- 在确认消息处理完成后,开发者调用
commitSync或commitAsync主动提交 Offset。 - Offset 被提交到内部主题
__consumer_offsets中保存。 - 当消费者重启或再平衡时,新的消费者从
_consumer_offsets读取已提交Offset,继续消费。
指定 Offset 消费


核心背景
当消费者组第一次消费 ,或者当前要使用的 Offset 在 Kafka 中已不存在(如数据已被清理)时,就需要通过 auto.offset.reset 参数来指定消费的起始位置。
配置参数与三种行为
该参数的配置值为 auto.offset.reset = earliest | latest | none,默认值为 latest。
-
earliest- 自动将 Offset 重置为最早的偏移量,即从 Topic 最开始的位置(偏移量 0)开始消费。
- 类似命令行中的
--from-beginning参数。 - 适用场景:需要回溯消费历史数据的场景,如数据补录、重新计算等。
-
latest(默认值)- 自动将 Offset 重置为最新的偏移量,即从消费者启动时 Topic 中最新的消息开始消费。
- 适用场景:只关注新消息的场景,如实时监控、告警通知等。
-
none- 如果未找到消费者组的历史 Offset,直接抛出异常,不自动重置。
- 适用场景:对消费起始位置有严格要求,不允许自动重置的场景,如金融交易等。
典型触发场景
- 首次消费:消费者组第一次订阅 Topic,没有历史 Offset 时。
- 数据过期:历史 Offset 对应的消息已被清理(如超过日志保留时间),导致 Offset 无效时。
指定时间消费
需求:在生产环境中,会遇到最近消费的几个小时数据异常,想重新按照时间消费。例如要求按照时间消费前一天的数据,怎么处理?
java
Set<TopicPartition> assignment = new HashSet<>();
while (assignment.size() == 0) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
HashMap<TopicPartition, Long> timestampToSearch = new HashMap<>();
// 封装集合存储,每个分区对应一天前的数据
for (TopicPartition topicPartition : assignment) {
timestampToSearch.put(topicPartition,System.currentTimeMillis() - 1 * 24 * 3600 * 1000);
}
// 获取从 1 天前开始消费的每个分区的 offset
Map<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(timestampToSearch);
// 遍历每个分区,对每个分区设置消费时间。
for (TopicPartition topicPartition : assignment) {
OffsetAndTimestamp offsetAndTimestamp= offsets.get(topicPartition);
// 根据时间指定开始消费的位置
if (offsetAndTimestamp != null){
kafkaConsumer.seek(topicPartition,offsetAndTimestamp.offset());
}
}
漏消费和重复消费

重复消费
触发场景(自动提交 Offset 引起)
- 消费者默认每 5 秒自动提交一次 Offset。
- 假设消费者提交 Offset 后 2 秒发生故障(如进程挂掉)。
- 当消费者重启时,会从上次提交的 Offset 位置继续消费,而该位置之后、已经拉取但还未处理完成的消息,会被重新消费,从而导致重复消费。
本质原因
自动提交是基于时间触发的,而非基于消息处理完成的状态,导致 Offset 提交与消息处理的状态不一致。
漏消费
触发场景(手动提交 Offset 引起)
- 若设置为手动提交 Offset,当开发者在消息处理完成前就提交了 Offset。
- 如果此时消费者进程被意外终止(如被 kill),已提交 Offset 对应的消息可能还在内存中、尚未完成业务处理或落盘。
- 重启后,消费者会从已提交的 Offset 继续消费,导致内存中未处理的消息丢失,即漏消费。
本质原因
手动提交的时机错误,先提交 Offset、后处理消息,导致 Offset 提交与消息处理的状态不一致。

核心目标
消费者事务的核心是实现精准一次性消费(Exactly-Once Delivery),确保每条消息只被处理一次,既不会重复消费,也不会漏消费。
要做到这一点,关键是将消费消息 和提交 Offset这两个操作绑定为一个原子事务,要么全部成功,要么全部失败。
实现原理
Kafka 默认的 Offset 提交机制(自动 / 手动)无法保证原子性,因此需要借助外部支持事务的存储介质(如 MySQL),将 Offset 与业务数据保存在同一个事务中:
- 消费消息:消费者从 Kafka 拉取消息并进行业务处理。
- 原子性保存:将业务数据和当前消费的 Offset 一起写入支持事务的自定义介质(如 MySQL),通过数据库事务保证两者要么同时成功,要么同时回滚。
- 恢复时定位:当消费者重启或发生故障时,从自定义介质中读取最新的 Offset,确保从正确的位置继续消费。
数据积压

场景 1:Kafka 消费能力不足
原因:Topic 分区数不足,导致消费者数量无法扩容,消费速度跟不上生产速度。
解决方案:
- 增加 Topic 分区数:这是提升消费并行度的基础,因为每个分区只能被一个消费者消费。
- 同步提升消费者数量:让消费者数量与分区数保持一致(消费者数 = 分区数),实现最大并行消费。
- 两者缺一不可,仅增加消费者而不增加分区,多余的消费者会处于空闲状态。
场景 2:下游数据处理不及时
原因:每批次拉取的数据量过少,导致处理速度跟不上生产速度。
解决方案:
- 提高每批次拉取数量 :例如,将
max.poll.records从默认的 500 条调整为 1000 条,减少网络请求次数,提升处理效率。 - 同时可以调整
fetch.min.bytes和fetch.max.wait.ms,让消费者在拉取到足够数据后再返回,减少频繁的小批量拉取。
恭喜你掌握 kafka 核心内容!✿