从生产到消费:Kafka 核心原理与实战指南

**摘要:**本文详解 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 发送消息详细流程


  1. 生产者主线程处理阶段

  2. 初始化发送 :生产者调用 send(ProducerRecord) 方法,发起消息发送请求。

  3. 拦截器处理 :消息首先经过 Interceptors,可以在此处对消息进行自定义的预处理或监控。

  4. 序列化Serializer(序列化器)将消息键和值从对象转换为字节数组,方便网络传输和存储。

  5. 分区路由Partitioner根据消息的键或默认规则,决定该消息要发送到哪个主题的哪个分区。


  1. 消息累积与批量处理阶段

  2. 消息入队 :经过分区后的消息,被放入 RecordAccumulator(消息累加器)中对应分区的 Deque(双端队列)里。

  3. 批量触发条件

    • 按大小触发 :当单个 ProducerBatch(消息批次)的大小达到 batch.size(默认 16KB)时,触发发送。
    • 按时间触发 :如果消息迟迟未达到 batch.size,则等待 linger.ms(默认 0ms,即无延迟)后,无论大小都会触发发送。
  4. 清理机制:累加器会定期清理超时的消息批次,避免内存占用过高。


  1. Sender 线程发送阶段

  2. 读取批量数据Sender 线程从 RecordAccumulator 中取出准备好的 ProducerBatch

  3. 构建网络请求NetworkClient 将消息批次封装成 Request 请求,并维护 InFlightRequests(默认每个 Broker 最多缓存 5 个请求)来跟踪未完成的请求。

  4. 发送与选择器轮询Selector(选择器)负责网络 I/O 的多路复用,将请求发送到 Kafka 集群的 Broker 节点,并等待响应。


  1. Broker 集群接收与应答阶段

  2. Broker 接收消息:消息发送到目标分区的 Leader 副本所在的 Broker 节点(如 Broker1 或 Broker2)。

  3. 重试机制 :如果发送失败(如网络异常、Broker 无响应),会根据 retries 配置进行重试。

  4. 应答确认(acks)

    • acks=0:生产者无需等待 Broker 应答,直接认为发送成功,性能最高但可能丢数据。
    • acks=1:仅等待 Leader 副本接收并写入消息后应答,可靠性中等。
    • acks=-1(all):等待 Leader 副本和所有 ISR(同步副本)队列中的节点都接收并写入消息后才应答,可靠性最高。

  1. 发送完成与后续处理

当收到 Broker 的确认应答后,Sender 线程会更新 RecordAccumulator 中的批次状态,释放对应的内存,并可以通过回调函数通知生产者发送结果。

异步发送 API

普通异步发送

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

带回调函数的异步发送

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

同步发送 API

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

生产者分区

分区好处
生产者发送消息的分区策略

默认的分区器 DefaultPartitioner

消息分区策略


  1. 指定分区(partition)时

当发送消息时显式指定了 partition 参数:

  • 消息会被直接发送到指定的分区
  • 例如:若指定 partition=0,所有数据都会写入分区 0

  1. 未指定分区,但有 key 时

当发送消息时没有指定 partition,但提供了 key

  • Kafka 会计算 key 的哈希值
  • 将哈希值与主题的分区数进行取余运算
  • 取余结果即为消息要发送到的分区编号
  • 例如:
    • key1 的哈希值为 5,key2 的哈希值为 6
    • 主题分区数为 2
    • key1 对应 5 % 2 = 1,写入分区 1
    • key2 对应 6 % 2 = 0,写入分区 0

  1. 既无分区也无 key 时

当发送消息时既没有指定 partition,也没有提供 key

  • Kafka 使用 Sticky Partition(黏性分区器)
  • 首先随机选择一个分区,并尽可能持续使用该分区
  • 直到该分区的 batch 已满(默认 16KB)或 linger.ms 超时
  • 再随机选择一个新的分区(与上一次不同)
  • 例如:第一次随机选择分区 0,等该批次满了或超时后,再随机切换到另一个分区
自定义分区器

如果研发人员可以根据企业需求,自己重新实现分区器。例如我们实现一个分区器实现,发送过来的数据中如果包含 atguigu,就发往 0 号分区,不包含 atguigu,就发往 1 号分区。

实现步骤:定义类实现Partitioner 接口,并重写partition()方法。

自定义分区器

关联自定义分区器

生产者如何提高吞吐量

  1. 增大批次大小(batch.size
  • 默认值为 16KB
  • 增大该值可以让生产者在一个批次中积累更多消息,减少网络请求次数
  • 提高传输效率,从而提升吞吐量

  1. 设置适当的等待时间(linger.ms
  • 默认值为 0ms(即消息立即发送)
  • 建议调整为 5--100ms
  • 让生产者在发送前等待一小段时间,积累更多消息后再批量发送
  • 这可以显著减少网络 I/O 次数,提升吞吐量

  1. 启用消息压缩(compression.type
  • 推荐使用 snappy 压缩算法
  • 压缩可以减小消息体积,降低网络传输带宽和 Broker 存储压力
  • 虽然会增加 CPU 开销,但整体吞吐量通常会得到提升

  1. 增大缓冲区大小(RecordAccumulator
  • 默认值为 32MB
  • 建议调整为 64MB
  • 更大的缓冲区可以让生产者在高并发场景下缓存更多消息

数据可靠性

ACK 应答级别


  1. acks=0
  • 行为:生产者发送数据后,不需要等待 Broker 的任何应答,直接认为发送成功。
  • 特点:性能最高,但数据可靠性最低,可能出现数据丢失。
  • 适用场景:对数据可靠性要求极低,但追求极致性能的场景。

  1. acks=1
  • 行为:生产者发送数据后,仅等待分区的 Leader 副本成功接收并写入消息,就会收到应答。
  • 特点:性能与可靠性中等。如果 Leader 接收后还未同步给 Follower 就宕机,会导致数据丢失。
  • 适用场景:对性能有一定要求,且能接受少量数据丢失的场景。

  1. 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 启动与注册

  1. Broker 注册 :Broker 启动后,会在 Zookeeper 的 /brokers/ids 节点下注册自己的 Broker ID(如 [0,2])。
  2. Controller 选举 :所有 Broker 会向 Zookeeper 的 /controller 节点竞争注册,谁先注册成功谁就成为集群的 Controller(如 brokerid:0)。

Controller 核心职责

  1. Broker 节点监听 :Controller 会持续监听 Zookeeper 上 /brokers/ids 节点的变化,感知集群中 Broker 的上下线。
  2. Leader 副本选举:当需要为分区选举 Leader 时,Controller 会以 ISR(同步副本)中的节点为存活前提,按照 AR(所有副本)列表的顺序,选择排在最前面的节点作为 Leader。
  3. 信息同步到 ZK :Controller 会将分区的 Leader 和 ISR 信息(如 leader:0, isr:[0,2])更新到 Zookeeper 的 /brokers/topics/{topic}/partitions/{partition}/state 节点。
  4. 其他 Controller 同步:集群中其他 Broker 会监听 Zookeeper 上的节点变化,同步最新的 Leader 和 ISR 信息。

Broker 故障与恢复

  1. Broker 故障触发:假设 Broker1 宕机,其负责的分区 Leader(如 TopicA-Partition0 的 Leader)随之失效。
  2. Controller 感知变化:Controller 通过 Zookeeper 监听到 Broker1 下线的事件。
  3. 获取当前 ISR:Controller 从 Zookeeper 中获取该分区当前的 ISR 列表。
  4. 选举新 Leader :在 ISR 列表中,按照 AR 顺序选择新的 Leader(例如从 ISR [0,2] 中选择 Broker0)。
  5. 更新 Leader 与 ISR:Controller 将新的 Leader 和 ISR 信息更新到 Zookeeper,其他 Broker 同步该信息,完成故障转移。

正常消息收发

  • 生产者发送消息:生产者将消息发送到目标分区的 Leader 副本所在的 Broker。
  • Broker 持久化与应答 :Leader 副本将消息写入本地的 .log.index 文件,并在满足 ACK 条件后向生产者返回应答。
  • Follower 副本同步:Follower 副本会从 Leader 副本拉取消息并同步,保持与 Leader 的数据一致,维护在 ISR 列表中。

节点服役和退役

服役新节点

1. 新节点准备

  1. 关闭hadoop104,并右键执行克隆操作。
  2. 开启hadoop105,并修改 IP 地址。
  3. 在 hadoop105 上,修改主机名称为hadoop105。
  4. 重新启动hadoop104、hadoop105。
  5. 修改haodoop105 中 kafka 的 broker.id 为 3。
  6. 删除hadoop105 中 kafka 下的 datas 和 logs。
  7. 启动hadoop102、hadoop103、hadoop104 上的 kafka 集群。
  8. 单独启动hadoop105 中的 kafka。

2. 执行负载均衡操作

  1. 创建一个要均衡的主题。
  2. 生成一个负载均衡的计划。
  3. 创建副本存储计划(所有副本存储在 broker0、broker1、broker2、broker3 中)。
  4. 执行副本存储计划。
  5. 验证副本存储计划。
退役旧节点

1. 旧节点准备

  1. 关闭待退役节点(如 hadoop104,对应 broker.id=3)的 kafka 服务。
  2. 查看集群当前所有主题的分区副本分布,确认待退役节点承载的副本信息。
  3. 确保集群其余节点(hadoop102、103、105)kafka 服务正常运行,状态健康。
  4. 备份待退役节点 kafka 的 datas 和 logs 目录,防止数据丢失。
  5. 确认集群副本配置满足退役后可用性(如副本数≥2,剩余节点可承接副本)。

2. 执行副本迁移操作

  1. 生成旧节点副本迁移计划(将 broker.id=3 上的所有副本迁移至其余可用节点)。
  2. 创建副本重分配计划(指定副本仅存储在剩余可用 broker 中,如 broker0、1、2)。
  3. 执行副本重分配计划,等待集群完成数据同步。
  4. 验证副本迁移结果,确认待退役节点无任何分区副本。

3. 旧节点下线操作

  1. 再次确认待退役节点无集群元数据和数据副本,无业务请求接入。
  2. 彻底关闭待退役节点的 kafka 服务,禁止其重新启动接入集群。
  3. 删除集群中待退役节点的相关配置(如主机映射、zk 中 broker 注册信息)。
  4. 验证集群整体状态,所有主题分区 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 的消息只保留最新版本,适合需要保留最新状态的场景。

高效读写数据

  1. Kafka 本身是分布式集群,可以采用分区技术,并行度高
  2. 读数据采用稀疏索引,可以快速定位要消费的数据
  3. 顺序写磁盘

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


4. 页缓存 + 零拷贝技术

Kafka 的零拷贝技术是和 PageCache(页缓存) 配合实现的,核心是跳过应用层(用户态)的缓存拷贝,让数据直接在内核态的 PageCache 和网卡之间传输,从而减少 CPU 开销和内存占用。


非零拷贝与零拷贝的流程对比

传统非零拷贝流程之所以会经历多次拷贝,本质是由操作系统的安全隔离设计早期应用的功能需求共同决定的,我们可以从这几个角度来理解:

用户态与内核态的安全隔离

  • 操作系统将内存分为用户态 (应用程序可直接访问)和内核态(仅操作系统可访问),这是为了防止应用程序直接操作硬件或破坏系统数据。
  • 数据在用户态和内核态之间无法直接传输,必须通过 CPU 拷贝作为 "中转",这就导致了 PageCache → Application Cache → Socket Cache 的两次 CPU 拷贝。

1. 非零拷贝流程(性能瓶颈)

  1. 数据从磁盘读取到内核态的 PageCache
  2. 数据从 PageCache 拷贝到 Kafka 应用层的 Application Cache(用户态)
  3. 数据从 Application Cache 拷贝到内核态的 Socket Cache
  4. 数据从 Socket Cache 拷贝到网卡(NIC)发送给消费者

过程产生 4 次数据拷贝多次上下文切换,其中第 2、3 步是 CPU 参与的拷贝,消耗大量资源。


2. 零拷贝流程(Kafka 实际使用)

  1. 数据从磁盘读取到内核态的 PageCache(DMA 拷贝,无 CPU 参与)
  2. 数据直接从 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):由组内选举产生,负责制定分区分配方案。

完整初始化流程

  1. 发送加入请求: 每个消费者向 Coordinator 发送 JoinGroup 请求,申请加入消费者组。
  2. **选举 Leader 消费者:**Coordinator 从组内选出一个消费者作为 Leader。
  3. **同步 Topic 信息:**所有消费者将自己要消费的 Topic 情况发送给 Leader 消费者。
  4. **制定消费方案:**Leader 消费者根据 Topic 分区数量和消费者数量,制定分区分配方案。
  5. **提交消费方案:**Leader 将分配方案发送给 Coordinator。
  6. **下发消费方案:**Coordinator 将最终的消费方案分发给组内所有消费者。
  7. 心跳与存活检测**:** 每个消费者会定期与 Coordinator 保持心跳(默认 3s),如果超时(session.timeout.ms=45s)或消费处理超时(max.poll.interval.ms=5分钟),该消费者会被移除并触发重平衡。
消费者组详细消费流程

一、发送消费请求

  1. 消费者通过 ConsumerNetworkClient 向目标分区的 Leader 副本发送拉取请求(sendFetches)。
  2. 拉取行为受以下参数控制:
    • fetch.min.bytes:每批次最小抓取大小,默认 1 字节。
    • fetch.max.wait.ms:若数据量未达到 fetch.min.bytes,最长等待时间,默认 500ms。
    • fetch.max.bytes:每批次最大抓取大小,默认 50MB。

二、接收并缓存拉取结果

  1. Broker 返回的消息数据会被存入消费者端的 completedFetches 队列
  2. 消费者会从该队列中获取 FetchedRecords 进行后续处理。

三、消息处理流程

  1. 反序列化(parseRecord:将 Broker 返回的二进制数据解析为业务可识别的消息格式。
  2. 拦截器处理(Interceptors:通过自定义拦截器对消息进行前置处理(如日志记录、数据过滤)。
  3. 业务处理:消费者对消息执行具体的业务逻辑。
  4. 单次拉取返回的最大消息条数由 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 提供了四种主流分区分配策略:RangeRoundRobinStickyCooperativeSticky
  • 默认策略是 Range + CooperativeSticky,可通过参数 partition.assignment.strategy 修改。

二、分区分配流程

  1. 发送加入请求 :每个消费者向 Coordinator 发送 JoinGroup 请求,申请加入消费者组。
  2. 选举 Leader 消费者:Coordinator 从组内选出一个消费者作为 Leader。
  3. 同步 Topic 信息:所有消费者将自己要消费的 Topic 情况发送给 Leader 消费者。
  4. 制定分配方案:Leader 根据选定的分配策略,制定分区与消费者的映射方案。
  5. 提交分配方案:Leader 将分配方案发送给 Coordinator。
  6. 下发分配方案:Coordinator 将最终的分配方案分发给组内所有消费者。

三、再平衡触发场景

再平衡是指消费者组内重新分配分区的过程,触发条件包括:

  • 消费者心跳超时 :消费者与 Coordinator 保持心跳(默认 3s),若超时(session.timeout.ms=45s),该消费者会被移除并触发再平衡。
  • 消费处理超时 :消费者处理消息的时间过长(max.poll.interval.ms=5分钟),会触发再平衡。
  • 消费者组成员变化:有新消费者加入或现有消费者下线。
  • 主题分区数量变化:主题的分区数增加。

四、主流分配策略简介

  1. Range:按主题分区序号范围分配,容易导致负载不均衡。
  2. RoundRobin:轮询分配所有主题的分区,负载更均衡。
  3. Sticky:优先保持现有分配,再平衡时尽量减少分区移动,减少重复消费。
  4. 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

优缺点

优点:逻辑简单,计算高效;对于单个 Topic,负载不均衡的影响相对较小。

缺点:多 Topic 场景易引发数据倾斜,如果消费多个 Topic,每个 Topic 都会按此规则分配,导致排序靠前的消费者(如 C0)在每个 Topic 中都多分配 1 个分区。Topic 数量越多,倾斜越严重。

RoundRobin 分区策略

核心规则

RoundRobin 是一种跨 Topic 的全局轮询分配策略,它针对集群中所有被消费的 Topic 统一计算分配。

  1. 全局收集与排序

    • 收集消费者组订阅的所有 Topic 的分区,以及组内所有消费者。
    • 对所有分区和消费者分别按 hashcode 进行排序。
  2. 轮询分配

    • 将排序后的分区列表,按顺序依次轮询分配给排序后的消费者列表。
    • 例如,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

  1. 简化开发:让开发者无需关心消费进度的持久化,专注于业务逻辑处理,降低代码复杂度。
  2. 保证消费连续性:当消费者重启或发生再平衡时,新的消费者可以从已提交的 Offset 位置继续消费,避免从头开始重复消费。
  3. 默认高可用:通过定期提交(默认 5 秒),在系统故障时能最大程度保留消费进度,减少数据重复处理的风险。

核心配置参数

  • enable.auto.commit :是否开启自动提交,默认值为 true
  • auto.commit.interval.ms :自动提交的时间间隔,默认值为 5000 毫秒(即 5 秒)。

工作流程

  1. 消费者持续从 Broker 拉取消息并进行业务处理。
  2. 每经过 auto.commit.interval.ms 配置的时间(默认 5 秒),消费者自动将当前的 Offset 提交到 __consumer_offsets 主题。
  3. 当消费者重启或再平衡时,新的消费者会从 __consumer_offsets 中读取已提交的 Offset,从该位置继续消费。
手动提交 offset

什么是手动提交 Offset

手动提交 Offset 是指开发者通过代码主动控制消费进度(Offset)的提交时机,而不是依赖系统自动定期提交。它提供了比自动提交更精细的控制,适合对数据一致性要求高的场景。


为什么需要手动提交

自动提交是基于时间的,开发者无法精确控制提交时机,可能会导致:

  • 数据丢失:如果自动提交后,消费者在处理消息时发生故障,已提交但未处理完成的消息会丢失。
  • 重复消费:如果在处理完成前就自动提交,重启后会从已提交的位置继续消费,导致部分消息被重复处理。

手动提交让开发者可以在确认消息处理完成后再提交 Offset,从而避免这些问题。


两种手动提交方式

1. commitSync(同步提交)

  • 核心逻辑:提交请求发出后,会阻塞当前线程,直到提交成功或失败(失败会自动重试)。
  • 特点:必须等待 Offset 提交完毕,才会继续消费下一批数据。
  • 适用场景:对数据一致性要求高的场景,如金融交易、支付等,确保 Offset 提交成功后再处理下一批。

2. commitAsync(异步提交)

  • 核心逻辑:提交请求发出后,立即返回,不会阻塞线程,也不会自动重试。
  • 特点:发送提交请求后,就开始消费下一批数据,性能更高,但存在提交失败的风险。
  • 适用场景:对性能要求高、能容忍少量重复消费的场景,如日志收集、实时监控等。

工作流程

  1. 消费者持续从 Broker 拉取消息并进行业务处理。
  2. 在确认消息处理完成后,开发者调用 commitSynccommitAsync 主动提交 Offset。
  3. Offset 被提交到内部主题 __consumer_offsets 中保存。
  4. 当消费者重启或再平衡时,新的消费者从 _consumer_offsets 读取已提交Offset,继续消费。
指定 Offset 消费

核心背景

当消费者组第一次消费 ,或者当前要使用的 Offset 在 Kafka 中已不存在(如数据已被清理)时,就需要通过 auto.offset.reset 参数来指定消费的起始位置。


配置参数与三种行为

该参数的配置值为 auto.offset.reset = earliest | latest | none,默认值为 latest

  1. earliest

    • 自动将 Offset 重置为最早的偏移量,即从 Topic 最开始的位置(偏移量 0)开始消费。
    • 类似命令行中的 --from-beginning 参数。
    • 适用场景:需要回溯消费历史数据的场景,如数据补录、重新计算等。
  2. latest(默认值)

    • 自动将 Offset 重置为最新的偏移量,即从消费者启动时 Topic 中最新的消息开始消费。
    • 适用场景:只关注新消息的场景,如实时监控、告警通知等。
  3. 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 引起)

  1. 消费者默认每 5 秒自动提交一次 Offset。
  2. 假设消费者提交 Offset 后 2 秒发生故障(如进程挂掉)。
  3. 当消费者重启时,会从上次提交的 Offset 位置继续消费,而该位置之后、已经拉取但还未处理完成的消息,会被重新消费,从而导致重复消费。

本质原因

自动提交是基于时间触发的,而非基于消息处理完成的状态,导致 Offset 提交与消息处理的状态不一致。


漏消费

触发场景(手动提交 Offset 引起)

  1. 若设置为手动提交 Offset,当开发者在消息处理完成前就提交了 Offset。
  2. 如果此时消费者进程被意外终止(如被 kill),已提交 Offset 对应的消息可能还在内存中、尚未完成业务处理或落盘。
  3. 重启后,消费者会从已提交的 Offset 继续消费,导致内存中未处理的消息丢失,即漏消费。

本质原因

手动提交的时机错误,先提交 Offset、后处理消息,导致 Offset 提交与消息处理的状态不一致。

核心目标

消费者事务的核心是实现精准一次性消费(Exactly-Once Delivery),确保每条消息只被处理一次,既不会重复消费,也不会漏消费。

要做到这一点,关键是将消费消息提交 Offset这两个操作绑定为一个原子事务,要么全部成功,要么全部失败。


实现原理

Kafka 默认的 Offset 提交机制(自动 / 手动)无法保证原子性,因此需要借助外部支持事务的存储介质(如 MySQL),将 Offset 与业务数据保存在同一个事务中:

  1. 消费消息:消费者从 Kafka 拉取消息并进行业务处理。
  2. 原子性保存:将业务数据和当前消费的 Offset 一起写入支持事务的自定义介质(如 MySQL),通过数据库事务保证两者要么同时成功,要么同时回滚。
  3. 恢复时定位:当消费者重启或发生故障时,从自定义介质中读取最新的 Offset,确保从正确的位置继续消费。
数据积压

场景 1:Kafka 消费能力不足

原因:Topic 分区数不足,导致消费者数量无法扩容,消费速度跟不上生产速度。

解决方案

  1. 增加 Topic 分区数:这是提升消费并行度的基础,因为每个分区只能被一个消费者消费。
  2. 同步提升消费者数量:让消费者数量与分区数保持一致(消费者数 = 分区数),实现最大并行消费。
  3. 两者缺一不可,仅增加消费者而不增加分区,多余的消费者会处于空闲状态。

场景 2:下游数据处理不及时

原因:每批次拉取的数据量过少,导致处理速度跟不上生产速度。

解决方案

  1. 提高每批次拉取数量 :例如,将 max.poll.records 从默认的 500 条调整为 1000 条,减少网络请求次数,提升处理效率。
  2. 同时可以调整 fetch.min.bytesfetch.max.wait.ms,让消费者在拉取到足够数据后再返回,减少频繁的小批量拉取。

恭喜你掌握 kafka 核心内容!✿

相关推荐
廋到被风吹走1 小时前
持续学习方向:云原生深度(Kubernetes Operator、Service Mesh、Dapr)
java·开发语言·学习
HDXxiazai1 小时前
idea JDK17 spring boot+nacos搭建 图文教程
java·spring boot·spring cloud·intellij-idea
yzp-1 小时前
Kafka 原子更新,精确一次消费 Exactly-Once --------- 学习笔记
分布式·学习·kafka
urkay-1 小时前
Android 当前Activity内显示的浮窗
android·java·iphone·androidx
刘 大 望1 小时前
使用AI IDE从0到1开发五子棋对战项目(vibe coding)
java·人工智能·spring boot·redis·ai·java-rabbitmq·ai编程
液态不合群1 小时前
AI赋能下的中国低代码市场:从工具革新到产业数字化核心引擎
java·人工智能·低代码·架构
零雲1 小时前
java面试:有了解过springboot的自动装配流程吗?
java·spring boot·面试
sanshizhang1 小时前
设计模式-责任链模式
java·设计模式·责任链模式
请叫我大虾1 小时前
数据结构与算法-分裂问题,将数字分成0或1,求l到r之间有多少个1.
java·算法·r语言