
Kafka权威指南 by Gwen Shapira & Todd Palino & Rajini Sivaram & Krit Petty
Kafka权威指南
目录
初识Kafka 初识Kafka


Kafka 自 4.0 版本起彻底移除对 ZooKeeper 的依赖,核心是 ZooKeeper 的局限性已成为 Kafka 规模化发展的瓶颈,替换为自研的 KRaft(Kafka Raft)架构:
- 可扩展性存在硬上限:ZooKeeper 自身仅支持 3-5 个节点,无法随 Kafka 集群线性扩容
- 性能与资源开销巨大:每个 Broker 需与 ZooKeeper 保持长连接和心跳,数千个 Broker 可压垮小型 ZooKeeper 集群
- 一致性模型不匹配,易出现状态异常:元数据同时存储在 ZooKeeper(持久化)和 Kafka Controller 内存(运行时),易出现两者不一致,引发脑裂、副本选主错误、消息丢失或重复等问题
- 运维复杂度高,云原生不友好:需同时部署、监控、升级 Kafka 和 ZooKeeper 两个独立系统,运维成本翻倍
- 架构适配性差:ZooKeeper 是通用分布式协调工具,而 Kafka 需要的是专用、高效的内嵌元数据引擎,两者设计范式不匹配
KRaft 架构以 “元数据即日志” 为核心,将元数据变更写入内部特殊主题 __cluster_metadata,通过 Raft 协议实现元数据管理,解决了上述所有痛点,使 Kafka 支持百万级分区,故障恢复时间缩短至秒级。
KRaft 仅改变元数据管理方式,Kafka 核心特性(消息存储、副本机制、ISR、生产者 / 消费者逻辑等)完全不变。
Kafka生产者——向Kafka写入数据 Kafka生产者——向Kafka写入数据
- 发送消息的基本流程是怎样的,经过了哪些组件?
- 3 种消息发送方式
- 发送并忘记:send 后就不管了,发送后的异常无感知
- 同步发送:send 会返回 Future,get()主线程会阻塞,生产环境极少用
- 异步发送:send 会指定回调,服务器响应时触发
- acks 配置影响消息持久性,0、1、all 分别代表了什么?


- 使用序列化器的意义?必须要有这一步吗?
- Kafka 的Broker 端、网络传输、磁盘存储只识别字节数组(byte []),不识别 Java/Python/Go 等语言的对象、字符串、实体类,序列化器就是完成 「业务对象 → 字节数组」 转换的核心组件
- 网络传输适配:网络协议只传输二进制字节流,Java 对象、POJO、字符串无法直接通过 Socket 发给 Kafka Broker,必须转字节。
- 存储格式统一:Kafka 持久化消息到磁盘、副本同步,均以字节形式存储,无序列化则无法落盘。
- 跨语言 / 跨平台兼容:生产者用 Java、消费者用 Go/Python 时,字节是通用格式,序列化能屏蔽语言差异。
- 体积优化与性能提升:二进制序列化(Avro/Protobuf)比纯文本更小,传输更快、吞吐量更高。
- 必须做序列化,没有例外。Kafka 生产者协议强制要求消息的键(Key)和值(Value)必须是字节数组,哪怕你只发简单字符串,也会通过 StringSerializer 转字节;如果不配置序列化器,直接抛 SerializationException 异常,消息根本无法发送。
- Kafka 的Broker 端、网络传输、磁盘存储只识别字节数组(byte []),不识别 Java/Python/Go 等语言的对象、字符串、实体类,序列化器就是完成 「业务对象 → 字节数组」 转换的核心组件
- Avro 序列化的特点
- Apache Avro 是 Kafka 生态最主流的序列化方案,核心特点:
- Schema(模式)驱动:用 JSON 定义数据结构,序列化后是二进制格式,体积远小于 JSON 文本。
- 极佳的兼容性:模式增删字段、修改类型时,新旧生产者 / 消费者无需修改代码即可兼容,不会出现解析异常。
- 语言无关:支持 Java/Go/Python 等所有主流语言,跨平台无压力。
- 支持默认值:字段缺失时自动使用默认值,不会丢失数据。
- 结合 Schema Registry(模式注册表):模式统一存储,消息只带模式 ID,大幅减少消息体积,避免每条消息内嵌完整模式。
- 动态类型:无需提前编译生成实体类,支持通用 GenericRecord 动态解析。
- Avro 与主流序列化框架对比:Kafka 生产环境优先选 Avro,追求极致性能选 Protobuf。
序列化框架 序列化形式 体积 兼容性 性能 依赖 Schema Kafka 生态适配 Avro 二进制 极小 最优(动态兼容) 高 强依赖 原生支持,官方推荐 Protobuf 二进制 小 好(需编译) 极高 强依赖 适配性好 Thrift 二进制 小 好 高 强依赖 通用,非 Kafka 首选 JSON 文本 大 差(字段变更易报错) 低 无 简单场景可用 Java 原生序列化 二进制 大 极差 低 无 严禁在 Kafka 使用
- Apache Avro 是 Kafka 生态最主流的序列化方案,核心特点:
- 代码中的 JSON 序列化 vs Kafka 消息序列化
- 代码中的 JSON 序列化(普通业务层)
- 本质:Java 对象 → JSON 字符串(文本格式)
- 目的:接口传输、日志打印、前端交互
- 特点:体积大、无 Schema、兼容性差、仅做文本转换
- 结果:最终还是要通过 Kafka 的 StringSerializer 再转字节数组才能发送
- Kafka 消息序列化(协议层)
- 本质:任意对象 → 字节数组(二进制 / 文本字节)
- 目的:满足 Kafka 网络传输、存储、跨语言的协议要求
- 特点:
- 最终输出一定是 byte[],而非字符串
- 支持二进制压缩(Avro/Protobuf),体积更小
- 配套反序列化器,严格保证消费端解析
- 可结合 Schema 做兼容、校验
- 层级关系:JSON 序列化是业务层转换,Kafka 序列化是协议层强制转换
- 代码中的 JSON 序列化(普通业务层)
- 消息投递语义是Kafka 生产者 / 消费者 与 Broker 之间,消息投递的可靠性承诺,描述「消息到底会被投递几次、会不会丢、会不会重复」,Kafka 有三大核心投递语义:
- 最多一次(At Most Once):消息最多被投递 1 次,可能丢失,绝不会重复
- 生产者配置:
acks=0+ 关闭重试(acks规定 Kafka 服务器需要完成多少份数据备份,才告诉生产者「消息发送成功了」) - 行为:生产者发完消息不等待 Broker 响应,网络异常、Broker 宕机都会丢消息
- 适用场景:日志采集、监控指标(允许少量丢失,追求极致吞吐量)
- 生产者配置:
- 至少一次(At Least Once):绝不丢消息,但可能重复投递(工程上的「绝不」不代表宇宙级的「绝不」,消息不会无故丢失,生产者会重试,只要 Broker 正常就能收到)
- 生产者配置:
acks=1/all+ 开启重试(默认开启),acks=1在极端极端故障下存在理论丢消息可能,而不是日常会丢,acks=all把「最后那点理论丢消息风险」也彻底堵死了。 - 行为:Broker 没响应时生产者自动重试,可能导致 Broker 已写入但响应丢失,重复发送
- 适用场景:绝大多数业务场景(可通过业务幂等解决重复问题)
- 生产者配置:
- 精确一次(Exactly Once):消息既不丢失,也不重复,精准投递 1 次
- 生产者配置:
enable.idempotence=true(幂等生产者)+ 事务生产者 - 行为:Broker 对消息做序列号去重,重试不会产生重复消息
- 适用场景:金融交易、支付、对账(强一致性要求)
- 生产者配置:
- 最多一次(At Most Once):消息最多被投递 1 次,可能丢失,绝不会重复
acks=0:不等待任何确认,发完就走,风险:网络断了、Broker 挂了,消息直接丢
acks=1:只等「首领副本」确认写入,Kafka 分区有 1 个 Leader(首领)+ 多个 Follower(副本),风险:Leader 刚写完就挂了,副本还没同步 → 消息丢失
acks=all/acks=-1:等「所有同步副本」都写入成功才确认,所有在同步列表里的副本(ISR)都写完,才算成功,Kafka 3.0+ 默认值就是acks=all,速度最慢、延迟最高
acks控制的是:消息的可靠性 ↔ 吞吐量 的平衡
Kafka消费者——从Kafka读取数据 Kafka消费者——从Kafka读取数据
轮询消费是 Kafka 消费者的核心工作机制,属于拉取模式(Pull) 的典型实现,指消费者通过主动、周期性调用
poll()方法向 Kafka Broker 请求数据的过程。其核心特点如下:- 消费者主导:完全由消费者控制请求时机、频率和数量,Broker 仅被动响应请求
- 无限循环:消费者需持续轮询(类似 “鲨鱼停止移动就会死掉”),否则会被判定为 “死亡”,分区被移交
- 多功能一体:
poll()不仅获取数据,还承担:- 首次调用:查找 GroupCoordinator、加入群组、分配分区
- 再均衡过程:执行再均衡协议、分区重分配
- 心跳维护:间接维持群组成员关系
- 阻塞控制:
poll(timeout)的超时参数控制无数据时的阻塞时长(0 表示立即返回) - 偏移量管理:通过提交偏移量记录消费进度,而非传统确认机制
RocketMQ 推模式是用长轮询模拟 Push:
- 伪 Push,底层基于长轮询 Pull,Broker 无消息时请求挂起 15 秒,有新消息立即响应
对比维度 Kafka 轮询消费(纯 Pull) RocketMQ 推模式(长轮询模拟 Push) 底层本质 纯拉取模式,消费者主动发起请求,Broker 无消息时立即返回 伪 Push,底层基于长轮询 Pull,Broker 无消息时请求挂起 15 秒,有新消息立即响应 控制主体 开发者完全掌控 poll()时机、频率和批处理逻辑SDK 封装长轮询 + 自动拉取,开发者只需注册消息监听器,感知为 “Broker 主动推送” 线程模型 单线程轮询 + 业务处理(或多线程需手动协调) 内部维护 PullMessageService拉取线程 + 回调线程池,解耦拉取与处理实时性 取决于轮询间隔,间隔越大延迟越高,可能有空轮询 接近 Push 的实时性,无消息时请求挂起,新消息到达立即推送,避免空轮询 负载适配 消费者自行控制批处理大小( max.poll.records),适应自身处理能力Broker 通过 PullRequestHoldService感知消费者负载,动态调整推送节奏偏移量管理 需手动控制提交(自动提交有风险),提交位置为 “下一条待处理消息的偏移量” 消费进度由 Broker 集中管理,自动提交,重试机制更完善 再均衡处理 完全在 poll()中执行,需开发者通过ConsumerRebalanceListener处理状态SDK 自动处理再均衡,开发者感知更弱 异常处理 需捕获 WakeupException优雅退出,手动调用close()封装更完善,异常处理更自动化 适用场景 高吞吐、可预测负载、需要精准控制消费节奏的场景 实时性要求高、快速开发、希望降低客户端复杂度的场景
- 伪 Push,底层基于长轮询 Pull,Broker 无消息时请求挂起 15 秒,有新消息立即响应
商用消息队列(RocketMQ/Kafka/RabbitMQ)没有绝对的纯 Push / 纯 Pull,都是基于基础模式的优化变种,整体分为 5 类:
纯 Push 模式(理论模型,工业界弃用)
- Broker 收到消息后,主动、立即向消费者发送数据,消费者被动接收,无法拒绝。
- 致命缺点(为什么不用?)
- 消费者过载崩溃:Broker 流量洪峰时,强行推送会压垮消费者(像快递员强行塞 1000 个快递,你接不住)
- 无法控速:消费者处理慢,Broker 还在推,导致消息堆积、丢消息
- 负载均衡极差:无法根据消费者能力分配消息
- 连接资源浪费:长连接维持成本高
纯 Pull 模式(基础模型,低效)
- 消费者固定间隔(比如 1 秒)主动调用接口问 Broker:「有消息吗?」,有消息:返回数据;无消息:立即返回空
- 优点
- 消费者完全掌控节奏,想拉就拉,绝不会过载
- 实现最简单
- 缺点
- 延迟高:间隔越大,消息消费越慢
- 空轮询浪费资源:99% 的请求都是「无消息」,白白消耗 CPU / 网络
- 体验极差,几乎不用
长轮询 Pull(伪 Push) → RocketMQ 默认推模式(最常用)
- RocketMQ 默认的「推模式」,也是业界标准的 Push 实现,底层还是 Pull,只是封装成对用户无感的 Push。
- 核心原理:
- 消费者主动向 Broker 发送拉取请求;
- Broker 没有消息时,不立即返回,挂起请求(默认 15 秒);
- Broker 有新消息时,立刻返回数据给消费者;
- 挂起超时 / 收到消息后,消费者立即重新发起请求,循环往复。
- 优点:
- 实时性 = 纯 Push:有消息秒回,无延迟
- 消费者可控:避免被 Broker 压垮(消费者不发请求,Broker 就不推)
- 无空轮询:Broker 挂起请求,不浪费 CPU / 网络
- 开发简单:用户只需写监听器,不用管拉取逻辑
- 缺点:底层封装复杂,消费者自定义拉取策略的灵活性低
智能轮询 Pull → Kafka 默认轮询消费(最常用)
- Kafka 的核心消费模式,优化了纯 Pull 的所有缺点
- 核心原理:
- 消费者主动调用
poll(timeout)方法拉取消息; - 无消息时消费者客户端线程阻塞(服务端按配置短暂等待消息,到期无论有无消息都返回,客户端需主动循环调用
poll()),直到超时 / 有消息; - 批量拉取:一次拉取多条消息,提升吞吐;
- 消费者自主控制拉取频率、批量大小,完全适配自身处理能力。
- 消费者主动调用
- 优点:
- 极致高吞吐:批量拉取,适合大数据场景
- 消费者绝对可控:灵活性拉满,自定义消费逻辑
- 资源利用率高:无空轮询,无阻塞浪费
- 缺点:
- 开发复杂度高:需要手动管理偏移量、线程、异常
- 实时性略低于伪 Push(取决于阻塞超时时间)
增强模式(批量消费、推拉结合、流式消费)
- 批量消费模式:Pull/Push 都支持,一次处理多条消息,大幅提升吞吐,是性能优化首选。
- 推拉结合模式:RabbitMQ 常用:正常用 Push 保实时,消费者繁忙时自动切换为 Pull 控速。
- 流式消费模式:Kafka Streams / RocketMQ Stream:实时处理消息流,无中间存储。
| 对比维度 | 长轮询(RocketMQ Push) | 智能轮询(Kafka Poll) |
|---|---|---|
| 挂起 / 阻塞主体 | 服务端 Broker 挂起网络连接 | 消费者客户端线程阻塞 |
| 请求发起方式 | 客户端发 1 次请求,等待服务端触发返回 | 客户端循环主动调用 poll() 发起请求 |
| 服务端行为 | 无消息→长期挂起连接(默认 15s),不返回;有消息→立即返回 | 无消息→按 fetch.max.wait.ms 短暂等待,到期返回空列表 |
| 客户端行为 | 等待服务端响应,无需主动重复发请求 | poll() 方法阻塞,直到超时 / 收到消息,结束后立即发起下一次轮询 |
| 实时性 | 接近纯推送,消息到达立即返回 | 高实时,依赖服务端等待时长,略逊于长轮询 |
| 控制权 | 服务端主导消息推送节奏 | 客户端完全主导拉取节奏、批量大小 |
| 连接资源 | 服务端长期持有大量连接,开销稍高 | 服务端无长期挂起,连接用完即释放 |
深入Kafka 深入Kafka
Kafka通过改变主题的保留策略来满足这些应用场景。如果保留策略是delete,那么早于保留时间的旧事件将被删除;如果保留策略是compact(压实),那么只为每个键保留最新的值。
主题的数据保留策略也可以被设置成delete.and.compact,也就是以上两种策略的组合。超过保留时间的消息将被删除,即使它们的键对应的值是最新的。组合策略可以防止压实主题变得太大,同时也可以满足业务需要在一段时间后删除数据的要求。
Kafka使用零复制技术向客户端发送消息,也就是说,Kafka会直接把消息从文件(或者更确切地说是Linux文件系统缓存)里发送到网络通道,不需要经过任何中间缓冲区。这是Kafka与其他大部分数据库系统不一样的地方,其他数据库在将数据发送给客户端之前会先把它们保存在本地缓存中。这项技术避免了字节复制,也不需要管理内存缓冲区,从而能够获得更好的性能。
在现有架构中,ZooKeeper起到了两个重要作用:一是用于选举控制器,二是用于保存集群元数据(broker、配置、主题、分区和副本)。此外,控制器本身也需要管理元数据——用于选举首领、创建和删除主题,以及重新分配副本。所有这些功能在新控制器中都将被替换掉。
新控制器背后的核心设计思想是:Kafka本身有一个基于日志的架构,其中用户会将状态的变化表示成一个事件流。在新架构中,控制器节点形成了一个Raft仲裁,管理着元数据事件日志。这个日志中包含了集群元数据的每一个变更。原先保存在ZooKeeper中的所有东西(比如主题、分区、ISR、配置等)都将被保存在这个日志中。因为使用了Raft算法,所以控制器节点可以在不依赖外部系统的情况下选举首领。
- ZooKeeper核心角色
ZooKeeper在传统Kafka架构中承担集群协调与元数据管理中心的关键职责:
核心功能 具体实现 集群成员管理 Broker启动时创建临时节点注册至 /brokers/ids,通过监听机制感知Broker上下线控制器选举 首个启动Broker创建 /controller临时节点成为控制器,保障集群单一控制器元数据存储 保存主题配置、分区副本分配、ISR集合、偏移量等核心集群元数据 分布式锁 提供分布式一致性保障,防止Broker ID冲突、重复选举等问题 配置同步 协调集群配置变更,确保所有Broker状态一致 - KRaft替代方案(Kafka Raft Metadata)
- KRaft是Kafka原生实现的基于Raft协议的元数据管理系统,完全替代ZooKeeper,从Kafka 2.8预览,3.0正式发布:
- 核心改进:
- 架构简化:移除外部依赖,元数据管理内生化,降低运维复杂度
- 性能提升:
- 控制器故障转移更快(状态本地持久化,无需从ZK全量加载)
- 元数据更新通过内部RPC而非ZK事务,吞吐量显著提升
- 扩展性增强:支持百万级分区,突破ZK在大规模集群中的性能瓶颈
- 一致性优化:基于Raft协议,确保元数据变更顺序一致性,防御脑裂
- 核心架构:
- 控制器仲裁:一组专用Broker组成,包含主控制器和跟随者控制器
- 元数据事件日志:替代ZK存储,记录所有元数据变更,支持状态重放
- MetadataFetch API:Broker主动拉取元数据,而非被动接收推送,减少网络开销
- 核心定义 Kafka的零复制(Zero-Copy)并非完全无拷贝,而是消除用户态与内核态之间的CPU拷贝,数据全程在内核态流转,大幅提升IO效率。
- 实现原理(基于Linux系统)
依赖两个核心系统调用,适配不同读写场景:
系统调用 适用场景 工作流程 拷贝次数 sendfile() 顺序读(消费者拉取消息) 磁盘→内核页缓存→网卡缓冲区,全程内核态,无用户态参与 2次DMA拷贝(磁盘→页缓存→网卡) mmap() 随机读(索引文件访问) 将文件映射到内存地址空间,直接操作内核缓冲区 1次DMA拷贝(磁盘→页缓存)+ 内存映射 - 与传统IO对比
- 传统IO流程:磁盘→内核缓存→用户缓存→内核Socket缓存→网卡,共4次拷贝+2次上下文切换
- 零复制流程:磁盘→内核缓存→网卡,仅2次拷贝+1次上下文切换,消除用户态参与
- 关键价值
- 性能提升:减少CPU拷贝和上下文切换,提升吞吐量30%-50%
- 内存优化:避免数据在用户态和内核态间重复存储,降低内存占用
- 延迟降低:减少数据流转环节,提升消息传输实时性
- 核心相似点
特性 具体表现 追加写(Append-only) 均采用顺序写入而非原地更新,规避磁盘随机写性能瓶颈 分层存储 Kafka日志分段、LSM树SSTable分层,实现冷热数据分离管理 后台合并 Kafka日志压实、LSM树Compaction,均通过后台进程清理无效数据 高吞吐设计 优先保障写性能,通过批量操作和顺序IO实现高吞吐量 墓碑机制 均使用特殊标记(Kafka墓碑消息、LSM树删除标记)处理键删除 - 核心差异点
对比维度 Kafka事件日志+压实 LSM树(如RocksDB) 设计目标 分布式消息队列,强调高吞吐、低延迟、多订阅者 嵌入式键值存储,强调读写平衡、随机访问性能 数据模型 基于偏移量的日志流,消息有序,支持任意位置读取 键值对模型,支持点查询、范围查询等复杂操作 合并策略 日志压实:保留每个键最新值,基于浑浊率触发,单键去重 多版本合并:合并多个SSTable,解决版本冲突,支持快照隔离 索引结构 偏移量索引+时间戳索引,简单高效,仅支持偏移量/时间戳查询 多级索引(MemTable+SSTable+布隆过滤器),支持高效随机查找 一致性模型 基于ISR的副本同步,消息提交后才对消费者可见 支持事务、快照等强一致性特性,满足数据库场景需求 适用场景 流式数据处理、日志收集、事件驱动架构 数据库存储引擎、状态存储、KV缓存 - 本质区别 Kafka日志压实是面向消息流的轻量级合并策略,专注于保留键的最新值,不支持复杂查询;而LSM树是完整的存储引擎架构,通过多级合并和复杂索引,兼顾写性能与查询灵活性。
- 核心角色与功能
角色 核心功能 关键特性 Broker(服务节点) 集群核心单元,接收/存储/转发消息,管理分区副本 可配置为纯Broker或控制器+Broker混合模式(KRaft) Controller(控制器) 特殊Broker,负责分区首领选举、元数据管理、集群协调 传统ZK模式由Broker兼任,KRaft模式由专用控制器节点担任 Producer(生产者) 消息发送方,决定消息分区策略,支持批量/异步发送 支持 acks参数控制可靠性,幂等性和事务保障精确一次语义Consumer(消费者) 消息接收方,通过拉取模式获取消息,支持消费组负载均衡 可配置从首领或跟随者读取(KIP-392),支持偏移量手动/自动提交 Consumer Group(消费者组) 一组消费者协同消费,每个分区仅被组内一个消费者消费 实现消息负载均衡和故障转移,支持水平扩展消费能力 Topic(主题) 消息分类容器,逻辑概念,物理上分为多个分区 支持多生产者多消费者,可配置保留策略和分区数 Partition(分区) 主题的物理分片,消息顺序写入,副本分布在不同Broker 每个分区有唯一首领副本,处理读写请求,跟随者副本同步数据 Replica(副本) 分区的备份,分为首领副本和跟随者副本,保障高可用 仅同步副本(ISR)有资格参与首领选举,通过 replica.lag.time.max.ms配置ISR(同步副本集) 与首领保持同步的副本集合,保障消息可靠性 首领故障时从ISR中选举新首领,避免数据丢失 - 整体架构
- 传统ZK模式架构
[Producer] → [Broker集群] ← [Consumer/Consumer Group] ↑ ↓ [ZooKeeper] (协调中心:控制器选举、成员管理、元数据存储) - KRaft模式架构(去ZK化)
[Producer] → [Broker集群] ← [Consumer/Consumer Group] ↑ ↓ [Controller集群] (Raft仲裁:主控制器+跟随者,元数据事件日志存储) - 数据流转核心流程
- 生产阶段:生产者→元数据请求→获取分区首领→发送生产请求→首领写入→同步至ISR→响应客户端
- 消费阶段:消费者→元数据请求→获取分区首领→发送获取请求→首领读取(零复制)→返回消息→提交偏移量
- 故障转移:Broker下线→控制器感知→ISR选举新首领→元数据更新→客户端刷新缓存→请求重定向
- 关键设计原则
- 分布式架构:多Broker集群,分区副本跨节点,保障高可用和水平扩展
- 顺序IO优先:消息顺序写入日志片段,最大化磁盘吞吐量
- 零复制优化:内核态直接传输数据,减少CPU拷贝和上下文切换
- 副本机制:基于ISR的同步策略,平衡可靠性与性能
- 元数据驱动:客户端通过元数据请求动态感知集群状态,实现灵活路由
- 传统ZK模式架构
不是必须。 Kafka 消费者默认必须从分区首领(Leader)读取数据,但从 Kafka 2.4 版本开始(通过 KIP-392),支持配置为从同步的跟随者副本(Follower)读取数据,不再强制只读 Leader。
- 默认行为:只能从 Leader 读
- 在未做任何特殊配置时,消费者只能、也必须从分区 Leader 拉取消息,原因:
- 一致性保证 Kafka 只保证已同步到所有 ISR 副本的消息对消费者可见,Leader 维护着高水位(High Watermark),知道哪些消息是已提交、可安全读取的。
- 客户端路由机制
消费者通过元数据请求获取集群拓扑,默认会把获取请求直接发给 Leader 所在 Broker;
如果发给非 Leader,Broker 会直接返回
NotLeaderForPartition错误,让客户端重试。 - 避免脏读与数据不一致 Follower 只是异步复制,可能存在滞后,Kafka 设计上默认不让消费者读可能未最终确认的消息。
- 在未做任何特殊配置时,消费者只能、也必须从分区 Leader 拉取消息,原因:
- 可选行为:可以从 Follower 读(KIP-392)
- Kafka 2.4+ 开放了从同步副本就近读取的功能,目的是:
- 降低跨机架、跨可用区的网络流量成本
- 减轻 Leader 节点的读取压力
- 开启条件与配置
- 消费者端:配置
client.rack,标识消费者所在机架/可用区。 - Broker 端:配置
replica.selector.class- 默认:
org.apache.kafka.common.replica.LeaderSelector(只读 Leader) - 开启就近读:
org.apache.kafka.common.replica.RackAwareReplicaSelector
- 默认:
- 限制:只能读处于 ISR 中的同步 Follower,不同步的副本依然不能读。
- 消费者端:配置
- trade-off(代价)
- 读取延迟略高:Follower 复制时会携带高水位标记,消费者从 Follower 读到消息的时间,会略晚于从 Leader 读。
- 可靠性不变:依然只能读到已提交消息,不会出现脏读。
- 适用场景:多机房/多机架部署、对读取延迟不敏感、希望降低公网/跨区带宽成本的场景。
- Kafka 2.4+ 开放了从同步副本就近读取的功能,目的是:
在 Kafka 里,“提交”分两个完全不同的层面,判断逻辑完全不一样:
- Broker 端:消息被认定为「已提交」(可被消费者读取):满足
acks+ 所有 ISR 同步完成 → 标记为 HW → 判定已提交,消费者可见。 - 消费者端:消费位移「提交成功」(标记我已经消费到哪条):自动按时间提交 / 手动调用提交 → 记录消费位置 → 判定提交成功。
- Broker 端:一条消息算不算「已提交」?怎么判断?:这是 Kafka 可靠性的核心,最终判断标准只有一个:这条消息是否被「所有同步副本 ISR」确认写入,由高水位线 HW(High Watermark) 标记。
- Kafka 只会把高水位线(HW)之前的消息定义为已提交消息,消费者只能读到已提交消息。
- 消息提交 = 满足 acks 要求 + 进入高水位线 HW,只有这样的消息,消费者才能读到。
- 高水位 HW 怎么来的?
- Leader 会跟踪所有 ISR 里的 Follower 复制到了哪个偏移量
- HW = 所有 ISR 副本都同步完成的最新偏移量
- 只有 ≤ HW 的消息,才是已提交、安全、不会丢的消息
- 由生产者
acks参数直接决定提交判定- 发消息时配的
acks,就是在定义 “我认为什么程度才算提交成功”:acks配置 提交判定规则 可靠性 acks=0生产者发出去就算提交,不等待任何Broker响应 最低,可能丢消息 acks=1Leader 写入本地日志 → 就算提交 中等,Leader挂了可能丢 acks=all / -1Leader + 所有ISR副本都写入完成 → 才算提交 最高,不丢消息
- 发消息时配的
- 兜底保障:
min.insync.replicas- 当
acks=all时,不是真的要等所有副本,而是至少等 ISR 里存活的副本数 ≥ 这个配置(默认 1),就判定提交完成。 - 如果 ISR 副本数不够,Broker 直接拒绝写入,抛异常。
- 当
- 消费者端:消费位移(offset)怎么算「提交成功」?
- 消费者的“提交”,不是提交消息,而是提交「我消费到第几条了」,位移提交成功=Broker记录下你的消费位置。
- 自动提交(默认)
- 配置:
enable.auto.commit=true - 判定规则:每过
auto.commit.interval.ms(默认5秒),消费者自动把本次 poll() 回来的最大偏移量提交给 Broker。 - 关键点:时间到就提交,不管你业务有没有处理完,容易丢数据/重复消费。
- 配置:
- 手动提交(开发常用)
- 调用
commitSync()/commitAsync() - 判定规则:只有你显式调用提交方法,才会把指定 offset 提交到
__consumer_offsets内部主题。 - 精确可控:业务处理成功 → 再提交,保证不丢不重复。
- 调用
- 消费提交的本质:只是在 Kafka 内部主题里记一个数字:
消费者组A + 主题T + 分区0 → 偏移量1000。不删除消息,不影响其他消费者组。
- 从 Follower 读数据时:Follower 只会返回自己同步到 HW 之前的消息,也就是说:从 Follower 读到的,一定是 Broker 已经判定为「已提交」的消息,不会出现脏读、未提交消息。