Kafka 深度原理分析:从消息流到分布式日志
版本基线:Apache Kafka 4.3.1。全文以当前 KRaft-only 架构为准,ZooKeeper 仅在升级与历史演进中出现。
本系列面向已经接触过 Kafka、希望系统理解其分布式原理的后端工程师和架构师。文章沿一条记录的生命周期展开:先建立实验环境和总体架构,再深入控制面、写入、存储、复制、消费、事务,最后收束到性能和生产治理。
目录
- 00|导读:用一条消息串起 Kafka 全貌
- 01|安装与使用:搭建 Kafka 4.3.1 实验环境
- 02|核心抽象与总体架构:Kafka 到底由什么组成
- 03|KRaft 控制面:元数据、选举与一致性
- 04|生产者与 Broker 写入链路:消息如何进入 Kafka
- 05|日志存储引擎:段、索引、清理与分层存储
- 06|副本与一致性:复制、提交和故障恢复
- 07|消费者与消费组:拉取、位点与重平衡
- 08|Share Groups:Kafka 的队列式消费模型
- 09|幂等与事务:从至少一次到精确一次
- 10|性能、容量与可观测性:Kafka 为什么快,边界在哪里
- 11|生产治理与技术演进:安全、容灾、升级及生态边界
推荐阅读方式
- 首次系统学习:按 00 → 11 顺序阅读。
- 排查写入与数据安全:重点阅读 04、05、06、09。
- 排查消费积压与重平衡:重点阅读 07、08、10。
- 规划生产集群:重点阅读 03、06、10、11。
写作约定
- 缩写首次出现时给出中英文全称。
- 优先解释问题、状态转换、关键不变量和故障恢复。
- 配置项只在能够说明因果关系时出现。
- 源码仅在能够明显帮助理解原理时作为少量实现锚点,不做逐类或逐行源码分析。
- 每篇末尾保留精简的 Apache Kafka 官方资料,便于进一步核对。
00|导读:用一条消息串起 Kafka 全貌
本篇结论 :Apache Kafka 不是简单的"消息转发器",而是一套围绕分区追加日志构建的分布式数据系统。理解 Kafka 的关键,不是背诵组件名,而是回答三个问题:消息写到哪里、什么时候算提交、由谁决定接下来怎么走。
1. 为什么从"一条消息"开始
假设订单服务发送一条"订单已创建"事件。生产者并不会把事件直接交给某个消费者,而是先确定目标主题和分区,将多条记录组成批次,发送给该分区的领导者副本。Broker 校验请求后把批次追加到分区日志,跟随者副本再从领导者拉取数据。当该批次满足复制与最小同步副本条件后,它才进入消费者可见的已提交范围。消费者拉取记录、处理业务,并把自己的消费进度提交到 Kafka。
这条链路可以压缩为:
Producer → Broker → Partition Log → Replica → Consumer Group → Offset Commit
每一个箭头背后都包含一个分布式系统问题:路由、批处理、持久化、复制、一致性、成员协调或故障恢复。全文将沿这条链路逐层展开。
2. 三个观察面
2.1 数据面
数据面负责真实记录的写入、复制和读取,核心对象是主题、分区、日志段、副本以及生产和拉取请求。它决定吞吐、延迟和数据可见性。
2.2 控制面
控制面负责"集群现在是什么状态":有哪些 Broker、主题有多少分区、每个分区的领导者是谁、副本放在哪里。Kafka 4.3.1 使用 Kafka Raft Metadata Mode(Kafka Raft 元数据模式,KRaft)维护这套元数据,不再依赖 ZooKeeper。
2.3 协调面
协调面处理跨请求的状态机,例如消费组成员关系、分区分配、事务状态以及共享消费记录锁。它们通常由 Coordinator(协调器)承载,并将关键状态写入内部主题。
一个常见误区是把 Controller 与 Coordinator 混为一谈。Controller 管集群和分区元数据;Coordinator 管某类客户端协议的状态。两者解决的问题完全不同。
3. Kafka 的四个核心不变量
不变量一:顺序只存在于分区内部
主题可以包含多个分区。Kafka 只保证同一分区内按位点递增观察记录,不提供跨分区的全局顺序。增加分区意味着提高并行度,同时也把顺序边界切得更细。
不变量二:位点是日志位置,不是消息身份
Offset(位点)是记录在某个分区日志中的逻辑位置。它只在"主题 + 分区"范围内有意义。消费者提交位点是在表达"下一步从哪里继续",不是在删除消息,也不是向 Broker 确认某条消息已被所有业务系统处理。
不变量三:成功响应不等于所有副本都已落盘
生产请求何时成功由 acks、In-Sync Replica(同步副本集合,ISR)和 min.insync.replicas 共同决定。Kafka 的耐久性来自协议约束、复制进度和故障模型的组合,而不是某个单独参数。
不变量四:Kafka 的一致性是按分区建立的
领导者、副本进度、高水位、事务可见性都以分区为单位推进。Kafka 能横向扩展,正是因为大量一致性问题被拆分到互相独立的分区上。
4. 一次正常写入究竟发生了什么
- 生产者序列化键和值,并根据键、显式分区或粘性策略选择分区。
- 记录进入本地累加器,与同分区记录组成 Record Batch(记录批次)。
- 客户端连接该分区的领导者 Broker,发送 Produce 请求。
- Broker 完成协议、权限、纪元、序列号和大小等校验。
- 批次被顺序追加到领导者日志;跟随者随后通过 Fetch 请求复制。
- 复制进度满足提交条件后,高水位推进,记录对普通消费者可见。
- 消费者拉取、处理记录,并根据业务语义决定何时提交消费位点。
异常并不是另一套系统:超时会触发重试,领导者切换会触发元数据刷新,副本落后会改变 ISR,消费者失联会触发组成员状态变化。理解 Kafka,就是理解这些状态如何在失败后重新收敛。
5. 4.3.1 时代需要更新的认知
- Kafka 已经是 KRaft-only 架构。ZooKeeper 只应作为迁移历史出现。
- 新一代消费者重平衡协议已经可用于生产。它把更多分配逻辑移到服务端,并取消全组同步屏障。
- Eligible Leader Replicas(合格领导者副本,ELR)扩展了传统 ISR 选主模型。
- Share Groups(共享消费组)让多个消费者可以并发处理同一分区中的不同记录。Kafka 因此具备更直接的队列式消费能力。
- Tiered Storage(分层存储)把本地热数据与远端历史数据分开,但仍有功能边界。
6. 阅读方法
先通过安装篇建立可运行环境,再用总体架构篇建立词汇表。随后依次理解控制面、写入、存储、复制、消费和事务。性能篇负责把前面的机制合并成容量与瓶颈模型,最后用生产治理篇回答安全、容灾和升级问题。
每篇都会优先解释:问题是什么、正常路径怎样运行、失败时如何恢复、系统必须保持什么不变量。只有当实现细节能够明显帮助理解这些问题时,才会提到少量源码结构。
7. 小结
Kafka 的主线可以概括为:生产者把记录追加到一个可复制的分区日志;控制面决定日志由谁领导;复制机制决定数据何时提交;消费者和协调器决定记录由谁处理以及从哪里继续。后续所有细节,都是这条主线在性能、故障或治理条件下的展开。
官方参考资料
- Apache Kafka 4.3 文档:kafka.apache.org/43/
- Apache Kafka 4.3.1 发布说明:kafka.apache.org/blog/2026/0...
01|安装与使用:搭建 Kafka 4.3.1 多节点实验集群
本篇目标:在一台开发机上运行 3 个独立 Controller 和 3 个独立 Broker,完成动态 KRaft 仲裁初始化、三副本主题、生产消费、消费组观测以及 Broker、Controller 故障演练。它保留了生产集群最关键的角色边界和容错语义,同时避免把安全、监控与操作系统调优过早塞进安装章节。
1. 为什么不再使用单节点
单节点只能验证命令能否执行,无法回答 Kafka 最重要的运行时问题:元数据仲裁如何维持多数派、分区副本如何复制、Leader 故障后谁接管、客户端如何发现新 Leader,以及"副本因子为 3、最小同步副本为 2"到底保护了什么。
实验拓扑
text
客户端(bootstrap)
└─ EXTERNAL → broker-1:19192 | broker-2:29192 | broker-3:39192
│
INTERNAL 副本复制:19092 | 29092 | 39092
│ 注册、心跳、元数据更新
▼
KRaft Controller 仲裁:101@19093 | 102@29093 | 103@39093
1 个 Leader + 2 个热备;2/3 多数派提交
本实验采用生产环境更推荐的角色分离部署:
- 3 个 Controller 组成 Kafka Raft 元数据模式(Kafka Raft Metadata Mode,KRaft)仲裁,负责元数据一致性和控制决策;多数派为 2,可容忍 1 个 Controller 故障。
- 3 个 Broker 承载分区日志、客户端读写和副本复制;Controller 与 Broker 可以独立扩缩容、滚动升级和故障演练。
- Broker 分离内部复制监听器与外部客户端监听器,帮助理解
listeners与advertised.listeners的真实边界。
边界说明 :六个进程仍运行在同一台机器上,因此能模拟"进程故障",不能模拟独立主机、机架、可用区(Availability Zone,AZ)、磁盘或网络分区的真实故障域。生产环境必须把节点放到独立故障域,并使用可解析的主机名替换
localhost。
2. 环境与节点规划
Kafka 4.3.1 服务端至少需要 Java Development Kit(Java 开发工具包,JDK)17。Kafka 4.3 官方还完整支持 Java 21 和 Java 25;实验优先选择组织已经验证过的长期支持版本。六个进程建议预留至少 6 GB 可用内存和 10 GB 磁盘空间。
bash
java -version
curl -O https://downloads.apache.org/kafka/4.3.1/kafka_2.13-4.3.1.tgz
tar -xzf kafka_2.13-4.3.1.tgz
cd kafka_2.13-4.3.1
mkdir -p config/lab data logs
节点、端口与目录统一规划如下。node.id 在集群内保持唯一;Controller 使用 101~103,Broker 使用 1~3,便于从日志中辨认角色。
- controller-101 :
node.id=101,Controller 端口19093,数据目录data/controller-101。 - controller-102 :
node.id=102,Controller 端口29093,数据目录data/controller-102。 - controller-103 :
node.id=103,Controller 端口39093,数据目录data/controller-103。 - broker-1 :
node.id=1,客户端端口19192,内部端口19092,数据目录data/broker-1。 - broker-2 :
node.id=2,客户端端口29192,内部端口29092,数据目录data/broker-2。 - broker-3 :
node.id=3,客户端端口39192,内部端口39092,数据目录data/broker-3。
3. 配置 3 个独立 Controller
创建 config/lab/controller-101.properties:
properties
process.roles=controller
node.id=101
controller.quorum.bootstrap.servers=localhost:19093,\
localhost:29093,localhost:39093
controller.listener.names=CONTROLLER
listeners=CONTROLLER://localhost:19093
listener.security.protocol.map=CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT
inter.broker.listener.name=INTERNAL
metadata.log.dir=data/controller-101
num.partitions=6
default.replication.factor=3
min.insync.replicas=2
再复制为 controller-102.properties 和 controller-103.properties,只修改三项:
controller-102.properties:node.id=102、listeners端口29093、metadata.log.dir=data/controller-102。controller-103.properties:node.id=103、listeners端口39093、metadata.log.dir=data/controller-103。
三个 Controller 的 controller.quorum.bootstrap.servers 必须一致。它只是发现仲裁的引导地址,不是固定成员清单;真正的投票成员会在格式化时写入元数据日志。Kafka 4.3.1 的新集群使用动态仲裁,不要再同时设置旧式 controller.quorum.voters。
4. 配置 3 个独立 Broker
创建 config/lab/broker-1.properties:
properties
process.roles=broker
node.id=1
controller.quorum.bootstrap.servers=localhost:19093,\
localhost:29093,localhost:39093
controller.listener.names=CONTROLLER
listeners=INTERNAL://localhost:19092,EXTERNAL://localhost:19192
advertised.listeners=INTERNAL://localhost:19092,EXTERNAL://localhost:19192
listener.security.protocol.map=CONTROLLER:PLAINTEXT,\
INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
inter.broker.listener.name=INTERNAL
log.dirs=data/broker-1
num.partitions=6
default.replication.factor=3
min.insync.replicas=2
offsets.topic.replication.factor=3
transaction.state.log.replication.factor=3
transaction.state.log.min.isr=2
auto.create.topics.enable=false
再复制为 broker-2.properties 和 broker-3.properties:
broker-2.properties:node.id=2,INTERNAL=29092,EXTERNAL=29192,log.dirs=data/broker-2。broker-3.properties:node.id=3,INTERNAL=39092,EXTERNAL=39192,log.dirs=data/broker-3。
这里把监听器拆成两类:
INTERNAL只用于 Broker 间的副本复制,由inter.broker.listener.name选定。EXTERNAL是客户端拿到元数据后真正回连的地址。bootstrap-server只负责第一次发现集群;后续连接目标来自advertised.listeners。PLAINTEXT(明文)仅为了让本地实验聚焦原理,绝不能直接暴露在不可信网络。
三副本配合 min.insync.replicas=2,表示生产者使用 acks=all 时,至少要有 2 个同步副本仍在同步副本集合(In-Sync Replica,ISR)中,否则写入失败。这比"故障时仍尽量写成功"更接近生产系统对数据耐久性的取舍。
5. 初始化动态 KRaft 仲裁
存储格式化会写入集群标识、节点目录标识和初始仲裁记录,不是普通的"创建空目录"。同一集群的六个节点必须使用同一个 CLUSTER_ID;三个初始 Controller 还必须使用完全相同的 INITIAL_CONTROLLERS。
bash
CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
C101_DIR_ID="$(bin/kafka-storage.sh random-uuid)"
C102_DIR_ID="$(bin/kafka-storage.sh random-uuid)"
C103_DIR_ID="$(bin/kafka-storage.sh random-uuid)"
INITIAL_CONTROLLERS="101@localhost:19093:${C101_DIR_ID},\
102@localhost:29093:${C102_DIR_ID},\
103@localhost:39093:${C103_DIR_ID}"
先格式化三个 Controller:
bash
bin/kafka-storage.sh format --cluster-id "$CLUSTER_ID" \
--initial-controllers "$INITIAL_CONTROLLERS" \
--config config/lab/controller-101.properties
bin/kafka-storage.sh format --cluster-id "$CLUSTER_ID" \
--initial-controllers "$INITIAL_CONTROLLERS" \
--config config/lab/controller-102.properties
bin/kafka-storage.sh format --cluster-id "$CLUSTER_ID" \
--initial-controllers "$INITIAL_CONTROLLERS" \
--config config/lab/controller-103.properties
再格式化三个 Broker。它们是仲裁观察者而不是投票者,因此使用 --no-initial-controllers:
bash
bin/kafka-storage.sh format --cluster-id "$CLUSTER_ID" \
--no-initial-controllers --config config/lab/broker-1.properties
bin/kafka-storage.sh format --cluster-id "$CLUSTER_ID" \
--no-initial-controllers --config config/lab/broker-2.properties
bin/kafka-storage.sh format --cluster-id "$CLUSTER_ID" \
--no-initial-controllers --config config/lab/broker-3.properties
重要 :不要对已有集群的数据目录重复执行格式化,也不要为不同节点重新生成
CLUSTER_ID。若只是本地首次配置写错,可在确认目录无业务数据后停止全部进程、清理本实验的data/,再从头初始化。
6. 按控制面到数据面的顺序启动
先在三个终端分别启动 Controller,确认其中一个成为 Leader,另外两个进入跟随状态:
bash
# 终端 1~3,每个终端执行一条
KAFKA_HEAP_OPTS="-Xms256m -Xmx512m" bin/kafka-server-start.sh config/lab/controller-101.properties
KAFKA_HEAP_OPTS="-Xms256m -Xmx512m" bin/kafka-server-start.sh config/lab/controller-102.properties
KAFKA_HEAP_OPTS="-Xms256m -Xmx512m" bin/kafka-server-start.sh config/lab/controller-103.properties
再在另外三个终端启动 Broker:
bash
# 终端 4~6,每个终端执行一条
KAFKA_HEAP_OPTS="-Xms256m -Xmx512m" bin/kafka-server-start.sh config/lab/broker-1.properties
KAFKA_HEAP_OPTS="-Xms256m -Xmx512m" bin/kafka-server-start.sh config/lab/broker-2.properties
KAFKA_HEAP_OPTS="-Xms256m -Xmx512m" bin/kafka-server-start.sh config/lab/broker-3.properties
客户端使用多个引导地址,避免恰好连接到已停止的 Broker:
bash
BOOTSTRAP=localhost:19192,localhost:29192,localhost:39192
bin/kafka-metadata-quorum.sh --bootstrap-server "$BOOTSTRAP" describe --status
bin/kafka-broker-api-versions.sh --bootstrap-server "$BOOTSTRAP"
bin/kafka-features.sh --bootstrap-server "$BOOTSTRAP" describe
仲裁状态中应看到 3 个 CurrentVoters、1 个 LeaderId,并且 MaxFollowerLag 很快回到较小值。Broker 应全部注册成功;此时控制面和数据面才算同时可用。
7. 创建三副本主题并理解结果
创建 6 分区、3 副本的 orders 主题,并在主题级显式固定最小同步副本数:
bash
bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
--create --topic orders \
--partitions 6 --replication-factor 3 \
--config min.insync.replicas=2
bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
--describe --topic orders
重点检查每个分区:
Leader:当前处理该分区读写的 Broker。Replicas:副本放置顺序,第一项通常是首选 Leader。Isr:当前跟得上 Leader、具备安全接管资格的副本集合;正常时应包含 3 个 Broker。
6 个分区让 Leader 有机会分散到 3 个 Broker;3 个副本让停止任意 1 个 Broker 后仍能保留 2 个副本。Kafka 的容错单位是"分区副本",不是抽象的整集群副本。
8. 生产、消费与消费组
启动生产者,并明确使用 acks=all:
bash
bin/kafka-console-producer.sh \
--bootstrap-server "$BOOTSTRAP" \
--topic orders \
--producer-property acks=all
输入若干订单,例如 order-1001、order-1002。另开终端启动带消费组的消费者:
bash
bin/kafka-console-consumer.sh \
--bootstrap-server "$BOOTSTRAP" \
--topic orders \
--group order-worker \
--from-beginning
查看消费位点:
bash
bin/kafka-consumer-groups.sh \
--bootstrap-server "$BOOTSTRAP" \
--describe --group order-worker
CURRENT-OFFSET 是消费组已提交的下一读取位置,LOG-END-OFFSET 是分区日志末端,LAG 是两者之差。它表示位置落后量,不直接等同于业务处理时延。
9. Broker 故障演练:观察数据面接管
先用主题描述找到某个分区的 Leader,然后在对应 Broker 终端按 Ctrl+C 停止进程:
bash
bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
--describe --topic orders
几秒后再次执行描述命令,预期看到:
- 原 Leader 所负责的分区切换到仍存活的 ISR 成员。
Isr暂时从 3 个成员缩小到 2 个。- 使用其余引导地址的生产者和消费者自动刷新元数据并继续工作。
- 再停止一个承载该分区副本的 Broker 后,ISR 可能降到 1;由于
min.insync.replicas=2,acks=all写入会失败,系统选择拒绝不够安全的写入。
只停止一个 Broker 完成观察后应立即重启它。它会从 Leader 拉取缺失日志,追赶到高水位后重新进入 ISR:
bash
KAFKA_HEAP_OPTS="-Xms256m -Xmx512m" bin/kafka-server-start.sh config/lab/broker-1.properties
实际重启时把配置文件替换为刚才停止的 Broker。不要删除其数据目录;"带原数据重启并追赶"才是正常恢复路径。
10. Controller 故障演练:观察控制面多数派
先记录当前 Controller Leader:
bash
bin/kafka-metadata-quorum.sh --bootstrap-server "$BOOTSTRAP" describe --status
停止一个 Controller。如果停止的是跟随者,Leader 不变;如果停止的是 Leader,剩余两个投票者会选出新 Leader。再次查看状态,应仍有多数派且集群可以创建主题、变更元数据和处理分区选举。
3 个 Controller 只能容忍 1 个同时故障。若再停止 1 个,仲裁失去多数派:已有数据路径在部分场景下可能短暂继续,但需要提交元数据的控制操作无法安全推进,分区故障后的恢复能力也会受限。不要把"Broker 进程还在"误判为"集群完整可用"。
恢复刚才停止的 Controller,并观察其追赶:
bash
bin/kafka-metadata-quorum.sh --bootstrap-server "$BOOTSTRAP" describe --replication
当其日志末端偏移逐步接近 Leader,说明元数据日志复制正在恢复。这里先观察结果,完整的选主、Epoch(任期)与提交规则将在 KRaft 专章展开。
11. 常见失败与排查顺序
- 端口已占用:先检查 19092/19192、29092/29192、39092/39192 和三个 Controller 端口。
- Broker 无法注册 :核对六份配置中的
controller.quorum.bootstrap.servers、集群标识和node.id。 - 客户端首次能连接、随后超时 :通常是
advertised.listeners发布了客户端不可达的地址。 - Controller 无法形成仲裁 :确认三个
--initial-controllers字符串完全一致,目录标识没有抄错。 - 主题创建一直等待:确认三个 Broker 均已注册;内部主题和三副本主题要求足够的 Broker 数量。
- 重启后出现集群标识不一致 :不要再次格式化或混用其他实验的数据目录,先检查各目录中的
meta.properties。
排错应先看进程日志和端口连通性,再看 KRaft 仲裁状态、Broker 注册状态、主题副本与 ISR,最后才调整超时参数。增加超时通常只会掩盖错误的地址或身份配置。
12. 与真实生产环境还差什么
这套实验已经能展示角色隔离、动态 KRaft 仲裁、三副本复制和两类故障切换,但生产部署还必须补齐:
- Controller 和 Broker 使用独立主机或容器实例,并跨机架或 AZ 放置;副本通过
broker.rack感知故障域。 - 客户端、Broker 间和 Controller 通信启用传输层安全协议(Transport Layer Security,TLS),并按需要配置简单认证与安全层(Simple Authentication and Security Layer,SASL)及访问控制列表(Access Control List,ACL)。
- 使用稳定磁盘、明确的容量与保留策略、主机名或服务发现,不依赖
localhost、相对数据目录和 512 MB 实验堆。 - 接入操作系统指标、Java 虚拟机、Broker、Controller、磁盘、请求延迟、Under-Replicated Partitions(副本不足分区)和消费积压监控,并准备滚动升级、再分配和备份恢复流程。
13. 停止与小结
按"生产者与消费者 → Broker → Controller"的顺序在各终端按 Ctrl+C 停止。保留 data/ 可以下次直接重启;只有确认不再需要实验数据时才删除本实验目录。
至此,我们不只是"启动了 Kafka",而是得到了一套可观察控制面多数派、数据面副本、客户端重路由和安全写入边界的多节点实验环境。下一篇将把这里出现的客户端、Broker、Controller、主题、分区、副本与消费组放进同一张总体架构图,建立后续章节共同使用的系统模型。
官方参考资料
- Kafka 4.3.1 Quick Start:kafka.apache.org/quickstart/
- Kafka 4.3.1 Downloads:kafka.apache.org/community/d...
- Kafka 4.3 KRaft:kafka.apache.org/43/operatio...
- Kafka 4.3 Broker Configs:kafka.apache.org/43/configur...
- Kafka 4.3 Topic Configs:kafka.apache.org/43/configur...
- Kafka 4.3 Java Version:kafka.apache.org/43/operatio...
02|核心抽象与总体架构:Kafka 到底由什么组成
本篇结论:Kafka 不是"一台消息服务器",而是三类分布式状态机的组合:数据面保存业务记录,KRaft 控制面维护集群事实,协调面维护消费与事务状态。三者都以分区化日志为基础,但复制规则、变化频率和故障边界不同。
1. 先看全图:三张互相衔接的网络

图中可以同时看到三条主线:
- 蓝色是客户端数据流。Producer(生产者)把记录发给目标分区的 Leader(领导者);Consumer(消费者)也从分区 Leader 拉取。客户端先获取元数据,再直连目标 Broker(代理节点),Kafka 不用一个中央代理转发所有数据。
- 绿色是普通分区的副本复制。Follower(跟随者)主动向 Leader 发送 Fetch(拉取)请求。每个分区独立决定 Leader、Follower、提交位置和故障恢复。
- 橙色是 KRaft 控制面 。Controller(控制器)仲裁组复制
__cluster_metadata元数据日志;活动 Controller 产生集群变更,其他 Controller 以多数派协议确认。Broker 的 Metadata Loader(元数据加载器)跟随已提交元数据,构造本地 Metadata Image(元数据镜像)。 - 紫色是协调状态。Group、Transaction、Share Coordinator(消费组、事务、共享消费协调器)运行在相应内部主题分区的 Leader Broker 上;它们把成员关系、位点、事务和确认状态写入内部日志。
这里最容易误解的一点是:Controller 决定"集群现在是什么样",但不转发正常的 Produce(生产)和 Fetch 请求;Coordinator 决定"客户端状态机如何推进",但业务记录仍由普通分区保存。
2. 从 Event 到 Record Batch
应用看到的是 Event(事件)或 Message(消息),Kafka 协议中的基本对象通常称为 Record(记录)。一条记录包含可选键、值、时间戳和若干 Header(头字段)。Kafka 不理解值的业务结构,它把键和值视为字节数组,序列化格式由应用和数据契约决定。
生产者通常把同一分区的多条记录组织为 Record Batch(记录批次)。批次是压缩、网络传输、磁盘追加以及幂等序列控制的重要单位。因此,很多性能参数和失败边界发生在"批次"而不是"单条消息"层面。
3. Topic、Partition 与 Offset
Topic(主题)是业务命名和配置边界。Partition(分区)是主题的并行、顺序、存储、复制和领导权边界。每个分区是一条只能在尾部追加的逻辑日志,记录按 Offset(位点)递增排列。
三个概念分别解决不同问题:
- 主题回答"这是什么数据";
- 分区回答"由谁承载、如何并行和在哪里保证顺序";
- 位点回答"记录在这个分区中的位置是什么"。
Kafka 的顺序保证是同一分区内的观察顺序,不是整个主题的全局顺序。如果同一业务实体必须有序,通常让它的键稳定映射到同一分区。
增加分区可以提高生产、复制和消费并行度,但也会增加文件、元数据、选举、协调和运维成本。分区数是容量模型中的长期结构决策,不是越多越好。
4. Broker、分区副本与数据面
Broker 承载分区副本并处理客户端请求。一个分区通常有多个 Replica(副本),分散在不同 Broker:一个为 Leader,其余为 Follower。
正常写入和读取由 Leader 处理。Follower 不是等待 Leader 推送,而是主动 Fetch 新日志。Replication Factor(副本因子)表示副本总数;In-Sync Replicas(同步副本集合,ISR)表示当前仍满足同步条件的副本。副本因子是静态布局,ISR 是随运行状态变化的安全集合,二者不能混为一谈。
图中 orders-0 和 orders-1 的 Leader 分散在不同 Broker,说明领导权以分区为单位:一个 Broker 可以同时领导一些分区、跟随另一些分区。数据面因此能够水平扩展,也可能产生热点分区。
5. Controller 与 KRaft 控制面
Controller 维护必须由全体节点得到唯一答案的集群事实,包括:
- Broker 注册、心跳、隔离和关闭状态;
- 主题、分区、副本分配和配置;
- 分区 Leader、Leader Epoch(领导任期)、ISR 与 Eligible Leader Replicas(合格领导者副本,ELR);
- 集群功能和元数据版本。
Kafka 4.3.1 以 Kafka Raft Metadata Mode(Kafka Raft 元数据模式,KRaft)复制这些事实。任一时刻只有一个 Active Controller(活动控制器)可以追加新的元数据记录,其他 Controller 是投票跟随者。记录获得多数派复制后才提交,各节点再按同一顺序应用。
Broker 不在每个请求上远程查询 Controller,而是持续跟随已提交元数据并维护本地镜像。于是,高频的数据请求只读取本地状态;控制面只传播变化。这是控制面与数据面解耦的关键。
6. Coordinator:分布式客户端状态机
Coordinator(协调器)不是一个固定全局进程,而是一组按键分片的协议状态机:
- Group Coordinator(消费组协调器)维护成员、订阅、分配和已提交位点;
- Transaction Coordinator(事务协调器)维护事务标识、生产者任期、参与分区和事务状态;
- Share Coordinator(共享消费协调器)维护记录级获取、确认和重新投递状态。
协调状态写入 __consumer_offsets、__transaction_state 等内部主题。某个键由哪个协调器实例负责,最终取决于它映射到哪个内部主题分区,以及该分区当前由哪个 Broker 领导。因此协调面也能随分区扩展,并沿用 Kafka 自身的日志复制能力。
7. 一条记录如何穿过整套架构
以"生产后由消费组读取"为例:
- Producer 连接
bootstrap.servers中任一可达节点,获取主题分区及 Leader 元数据。 - Producer 根据键或分区器选择分区,把同一分区的记录合并为批次,直发该分区 Leader。
- Leader 追加本地日志;Follower 主动 Fetch 并复制。满足确认条件后,Producer 收到成功响应。
- Consumer Group(消费组)先与 Group Coordinator 完成成员管理和分区分配,再向自己拥有的分区 Leader 发 Fetch。
- Consumer 处理记录后,把消费位点提交给 Coordinator;位点进入内部主题,而不会删除业务记录。
- 如果 Broker 故障,Controller 先在控制面提交新的分区领导关系;Broker 与客户端更新本地元数据后重新路由。
这条路径揭示了三种"提交"并不是一回事:数据副本提交决定记录是否可安全观察;元数据多数派提交决定集群事实何时生效;消费位点提交只记录某个消费组处理到了哪里。
8. 分区为什么是核心故障边界
分区同时是:
- 单调位点空间;
- 领导权与副本复制单元;
- 消费分配和协调分片单元;
- 事务参与单元;
- 吞吐、积压与热点分析单元。
因此,排查 Kafka 问题通常要最终落到具体 Topic-Partition(主题---分区)。主题级平均值可能掩盖单个热点;Broker 故障也只直接影响其承载的 Leader 和 Replica 集合。
9. 三个面的速度与故障影响
- 数据面持续高速流动。某个分区 Leader 不可用,只阻塞该分区,其他分区仍可工作。
- 协调面在成员变化、事务推进或确认状态变化时更新。协调器切换会短暂影响相关组或事务,不等于所有业务分区不可用。
- 控制面在拓扑、配置、Broker 或分区状态变化时更新。失去 Controller 多数派后,已有数据路径可能短期继续,但无法安全提交新的集群事实和完成故障切换。
这种边界划分解释了 Kafka 为什么能在大吞吐下保持可管理性,也解释了为什么"Broker 还活着"并不代表"集群可以完成所有操作"。
10. 常见误区
误区一:Topic 就是一条队列。底层是多分区日志;多个消费组可以独立读取同一份数据,Share Group 才提供更接近记录级共享队列的获取模型。
误区二:副本因子为 3 就一定不丢数据。安全性还取决于 ISR、最小同步副本、生产确认、ELR、是否允许非同步副本选主以及故障时序。
误区三:提交消费位点就是确认并删除消息。位点属于消费组;日志是否保留由时间、大小或压缩策略决定。
误区四:Controller 或 Coordinator 转发所有业务流量。Controller 管集群事实,Coordinator 管客户端状态;普通记录直接流向目标分区 Leader。
11. 小结
Kafka 可以抽象为大量独立分区日志组成的数据面、一条 KRaft 元数据日志组成的控制面,以及一组由内部主题分片的协调状态机。下一篇将深入控制面,并严格区分 Controller Leader 选举与普通分区 Leader 选举。
官方参考资料
- Kafka 4.3 Documentation:kafka.apache.org/43/
- Kafka 4.3 Implementation:kafka.apache.org/43/implemen...
- Kafka 4.3 Protocol:kafka.apache.org/43/design/p...
03|KRaft 控制面:元数据、选举与一致性
本篇结论:Kafka Raft(Kafka 的 Raft 共识协议,KRaft)把集群状态组织成一条可复制、可提交、可重放的元数据日志。
Controller 选举 解决"哪个 Controller 取得控制权";普通分区的 Leader 变更解决"哪个 Broker 副本接管读写"。
前者由 Controller 投票者以多数派投票达成共识,后者由 Active Controller(活动控制器)依据 In-Sync Replicas(同步副本集合,ISR)、Eligible Leader Replicas(合格领导者副本集合,ELR)和安全配置集中决定。两者都涉及 Leader,但协议、参与者和安全边界完全不同。
1. 先分清两种 Leader
Kafka 的故障恢复可能连续出现两个层级的 Leader 变更:
- Controller Quorum Leader(控制器仲裁组领导者)选举 。KRaft 投票者选出元数据分区的 Leader;该节点同时承担 Active Controller。它解决的是:谁有权追加集群元数据并执行控制决策。
- Partition Leader(分区领导者)重新选择 。这不是普通 Broker 之间投票,而是 Active Controller 从符合条件的副本中选出新 Leader,并把结果写入元数据日志。它解决的是:某个普通主题分区由哪个副本继续提供读写服务。
因此,第一层是分布式共识建立控制权,第二层是控制权建立后的集中式状态决策。普通 Broker 会上报状态、复制数据并应用新的元数据,但不投票选普通分区 Leader;ISR 和 ELR 是候选资格集合,也不是 Controller 仲裁组的投票集合。
Active Controller 故障:先恢复控制权
text
Controller Follower 在 controller.quorum.fetch.timeout.ms 内
未能从现任 Leader 成功 Fetch
→ 进入 Prospective(预候选)状态,以未提升的 Epoch 发起 Pre-Vote(预投票)
→ 获得多数预投票后进入 Candidate(候选者),提升 Epoch 并正式请求投票
→ 日志足够新的 Candidate 获得 Controller 多数票
→ 成为新的 Controller Quorum Leader,并写入当前 Epoch 的领导变更记录
→ Voter 复制该记录;当前 Epoch 的记录到达多数派后,提交边界重新推进
→ 新 Active Controller 恢复元数据写入、Broker 管理与后续分区故障处理
如果旧 Active Controller 只是被隔离,它会在 Check Quorum(多数派连通性检查)失败后放弃领导权,或在看到更高 Epoch(任期)时退回非 Leader 状态。
Controller 切换不会无条件更换普通分区的 Leader:只要 Broker 与既有分区 Leader 仍然健康,已有数据读写通常可以继续;短暂停顿的是需要控制面提交的新变更以及新的故障处置。
Broker 故障:由 Active Controller 完成分区接管
text
Broker 不再向 Active Controller 发送心跳
→ broker.session.timeout.ms 对应的租约到期
→ Active Controller 生成 Broker 隔离及相关分区变更
→ 按 ISR、ELR、Last Known Leader(最后已知领导者)和安全配置选择候选副本
→ 隔离状态与分区 Leader 变化写入 KRaft 元数据日志
→ Controller 多数派复制并提交这些记录
→ Broker 应用新的元数据镜像,客户端刷新元数据并重新路由
这两条链不能合并成一次"选主":Active Controller 故障恢复的是控制面的决策权;Broker 故障恢复的是数据面的分区服务。如果二者同时发生,必须先产生新的 Active Controller,再由它根据已提交的集群状态和后续心跳处理 Broker 隔离与分区 Leader 变更。
2. 为什么元数据必须是一致状态机
分区由谁领导、Broker 是否被隔离、一次主题创建是否生效、集群启用了哪个功能版本,都必须有唯一答案。两个节点如果同时认可不同的分区 Leader,客户端可能把请求写向两条历史;两个 Controller 如果可以各自提交变更,集群拓扑就会发生不可安全合并的分叉。
KRaft 把每次集群状态变化编码为 Metadata Record(元数据记录),追加到内部 __cluster_metadata 主题的单一分区。记录由 Offset(位点)和写入它的 Leader Epoch 建立唯一顺序;Controller 与 Broker 按顺序重放已提交记录,构建 Metadata Image(元数据镜像)。
这个模型把权威性分成三层:
- 单一 Leader 决定追加顺序:同一 Epoch 只有有效的 KRaft Leader 可以接受新记录;
- 多数派与当前任期门槛决定提交:写入 Leader 本地不等于生效,新 Leader 还必须先让本 Epoch 的记录到达多数派,才能安全推进提交边界;
- 确定性状态机决定语义:节点按同一顺序应用同一批记录,得到相同的集群状态。
KRaft 复制的是集群元数据,不是普通主题的数据。普通数据分区仍采用 Leader---Follower、ISR 和 High Watermark(高水位,HW)机制;不要把数据分区的 ISR 提交语义与 Controller 仲裁组的多数派提交语义混为一谈。
3. 仲裁成员、日志位置与状态
KRaft 元数据分区中的副本分为:
- Voter(投票者):可以投票并参与 Controller 选举,其复制位置用于计算多数派提交边界;
- Observer(观察者):复制元数据日志但不投票,也不能成为 KRaft Leader。普通 Broker 以观察者身份获取元数据。
三个 Voter 的多数派是 2,五个 Voter 的多数派是 3,因此分别可容忍 1 个和 2 个 Controller 故障。增加 Voter 会提高并发故障容忍度,也会扩大复制路径、故障域协调和运维成本;Controller 数量不是越多越好。
理解日志进度时要区分三个位置:
- Log End Offset(日志末端位点,LEO):本地已经写到哪里,可能包含未提交尾部;
- High Watermark(高水位,HW):仲裁组已经提交到哪里,是不会因正常换主而回滚的边界;
- Last Applied Offset(最后应用位点):状态机已经重放到哪里。节点即使拿到记录,也可能尚未应用完成。
一个 Voter 的状态转换可简化为:
text
Follower --Fetch 超时--> Prospective --预投票过半--> Candidate
Candidate --正式投票过半--> Leader
Candidate --失败或超时--> Prospective
非 Leader --发现有效 Leader--> Follower
任意状态 --发现更高 Epoch--> 按新 Epoch 转入 Unattached / Follower
Leader --优雅辞任或 Check Quorum 失败--> Resigned / Unattached
这是便于理解的主路径;精确落点还取决于节点是否知道 Leader 端点、是否仍是 Voter,以及是否处于动态成员变更中。Epoch 是单调递增的领导周期编号,不是时间戳。节点一旦接受更高 Epoch,就不能继续用旧 Epoch 执行领导者动作。
4. 正常运行:Fetch 同时承担复制与存活证明
KRaft 使用拉取式复制。Follower 周期性向 Leader 发送 Fetch 请求,携带本地日志位置和末端 Epoch;Leader 返回后续记录、当前 Leader 信息和 HW。一次 Fetch 循环同时完成:
- 复制元数据日志;
- 汇报 Follower 的复制进度;
- 证明 Leader 与 Follower 仍能双向通信。
Leader 根据 Voter 的复制位置计算多数派可见位置。记录只有越过 HW 才成为已提交事实,随后 Controller 状态机和 Broker 元数据加载器才能把它稳定应用。
例如,三个 Voter 中,Leader 在当前 Epoch 追加一条记录,一个 Follower 已复制该记录,另一个仍落后。该记录已存在于两个 Voter,满足多数派条件;如果本 Epoch 的提交门槛已经建立,HW 就能覆盖它。反之,若记录只留在 Leader 本地便发生换主,它属于未提交尾部,可能被截断。
5. 什么会触发 Controller 选举
对 Follower 而言,主要的领导故障检测窗口是 controller.quorum.fetch.timeout.ms:在该时间内没有从现任 Leader 成功 Fetch,就进入 Prospective。进程退出、网络隔离、磁盘停顿或线程长时间暂停都可能触发它,因此"超时"只代表本节点无法继续确认现任 Leader,并不等于已经证明对方进程死亡。
Leader 侧使用同一个超时尺度执行 Check Quorum:如果持续收不到多数 Voter 的有效 Fetch 或 FetchSnapshot 请求,就放弃领导权。这样,被隔离在少数派的旧 Leader 不会无限期维持有效控制权。
此外,优雅关闭或成员变更可以通过 EndQuorumEpoch(结束仲裁任期)等机制加速交接。controller.quorum.election.timeout.ms 主要约束选举轮次无法形成多数票时何时重试;它不应与基于 Fetch 的领导存活检测混成一个概念。无论由哪种事件触发,超时都只是启动选举,只有多数票才能建立新 Leader。
6. 第一阶段:Pre-Vote 防止孤立节点扰动集群
没有 Pre-Vote 时,一个隔离很久的节点恢复后可能先提升 Epoch,再请求投票。即使它日志很旧、无法胜选,较高 Epoch 也可能迫使健康 Leader 退位,形成无意义的抖动。
Kafka 4.3.1 的 Pre-Vote 把"确认自己有机会获胜"和"正式提高 Epoch"分开:
- Follower 的 Fetch 超时后进入 Prospective;
- 它发送带
PreVote=true的 Vote 请求,请求中的 Replica Epoch 仍是尚未提升的当前 Epoch; - Leader 会拒绝预投票;已经成功跟随过现任 Leader 的 Follower 也会拒绝;
- Unattached(未依附)、Prospective 等没有有效 Leader 约束的状态,会在申请者日志至少同样新时考虑同意;预投票不持久化,也不构成正式的一任期一票;
- Prospective 获得多数预投票后,才进入 Candidate(候选者)、提升 Epoch 并启动正式投票。
因此,无法接触多数派的孤立节点最多反复尝试预选,不会仅凭自己的超时不断抬高全局 Epoch。Pre-Vote 与 Check Quorum 必须配合:前者阻止无胜算节点扰动,后者确保失去多数派的旧 Leader 最终释放其追随者的预投票约束。
7. 第二阶段:正式投票与选主规则
Candidate 提升 Epoch,先给自己投票,并在发送请求前持久化当前 Epoch 与投票对象。这样即使节点立即重启,也不会在同一 Epoch 投给两个候选者。
Voter 处理正式 Vote 请求时,核心判断是:
- 集群与任期合法:Cluster ID 必须匹配;落后的 Epoch 会被拒绝,看到更高 Epoch 的节点必须更新自身仲裁状态;
- 一任期一票:同一 Voter 在一个 Epoch 最多支持一个 Candidate;对同一 Candidate 的重试可以重复返回已同意;
- 日志足够新:先比较双方最后一条记录的 Epoch,较大者更新;末端 Epoch 相同,再比较 Offset,较大者更新。申请者落后时拒绝投票;
- 成员配置能形成合法多数派:只有 Voter 才会主动进入 Candidate。静态仲裁组的多数派来自固定 Voter 集合;动态仲裁组的 Voter 配置来自日志,单次成员变更期间不同副本可能暂时看到旧、新配置,协议通过一次只变更一个成员以及相交多数派保持安全,不能简单套用"接收者本地列表中没有 Candidate 就永远拒绝"的静态规则。
"日志更新"不是比较文件大小。先比较末端记录的 Epoch,是为了避免某个节点凭借一段更长、但来自旧任期且未提交的尾部,压过包含较新任期历史的节点。
Candidate 必须从其有效 Voter 配置中获得 floor(N/2)+1 张正式票(包含自己)才能成为 Leader。多数派集合的交集、一任期一票和日志新鲜度共同保证:新 Leader 能承接已提交前缀,而不是重新创造一条历史。
8. 平票、失败与退避
多个 Voter 可能接近同时超时并参选,结果通常有三类:
- 一个 Candidate 先取得多数票并成为 Leader;
- 节点发现更高 Epoch 或有效 Leader,退出当前竞选并跟随;
- 票被分散,所有 Candidate 都无法过半。
Candidate 在选举超时或确认失败后回到 Prospective,再重新确认多数派支持;失败路径还会使用随机化、上限受 controller.quorum.election.backoff.max.ms 控制的退避,降低多个节点持续同步竞选的概率。Pre-Vote 的关键作用是:重试可以发生,但没有多数预投票时不再反复提升 Epoch。
没有多数派时,KRaft 停止产生新的已提交元数据,而不是选出一个"尽力而为"的 Controller。这是保持一致性的代价,也是 Controller 必须跨独立故障域部署的根本原因。
9. 新 Leader 如何真正建立权威
"拿到多数票"和"恢复可提交的控制面"之间还有一段必要过程:
- Candidate 获得多数票,持久化自己是当前 Epoch 的 Leader;
- 新 Leader 立即在日志中追加 LeaderChangeMessage(领导变更消息)等属于当前 Epoch 的控制记录;
- 它通过 BeginQuorumEpoch(开始仲裁任期)把新 Leader 与 Epoch 通知给 Voter;Observer 则主要通过 Fetch 响应发现新 Leader;
- Follower 对齐日志并 Fetch 当前 Epoch 的记录;
- 当前 Epoch 的记录到达多数派后,Leader 才能安全地让 HW 越过此前任期的尾部;
- Active Controller 基于已提交状态恢复对外的元数据写入与故障处理。
第五步是容易忽略的提交门槛:一条旧 Epoch 记录即使一度出现在多数节点上,也不能仅凭"副本数够多"就在新任期直接被判定为提交。新 Leader 先提交本 Epoch 的记录,才能证明此后的提交边界不会被另一个合法候选历史推翻。LeaderChangeMessage 让这一门槛不必等待下一次业务元数据变更。
10. 冲突日志如何收敛
旧 Leader 可能在失去多数派前追加了未提交记录,少数派节点也可能保留不同的日志尾部。恢复通信后,Follower 在 Fetch 中携带 Offset 与 Epoch;Leader 根据任期边界找到双方最后一致的位置,并让 Follower 截断冲突尾部,再从权威日志继续拉取。
需要区分两类内容:
- HW 之前的已提交前缀不能被正常换主截断;新 Leader 能合法当选,正是因为投票规则与成员变更规则保护了这段历史;
- HW 之后的未提交尾部可以被截断;本地"写过"不等于仲裁组"提交过"。
若节点落后过多,所需的旧日志段已经清理,它会先安装 Snapshot(快照),再拉取快照之后的增量记录。快照是某个已提交 Offset 上的完整状态检查点,不是第二套权威数据库;不同节点可以在不同位置生成快照,只要都来自同一条已提交历史。
11. 一次元数据变更的完整执行路径
以创建主题、修改配置或切换分区 Leader 为例:
text
管理请求 / Broker 状态事件
→ 请求被转发或送达 Active Controller
→ Controller 事件线程按序校验当前状态与尚在处理中的待定状态
→ 生成一组元数据记录
→ 追加到本地 __cluster_metadata 日志
→ Voter 通过 Fetch 复制
→ 满足多数派与当前 Epoch 提交条件,HW 推进
→ Controller 按序提交并应用,生成新的 Metadata Image
→ 依赖本次提交的管理请求可以返回成功
→ Broker Observer 获取已提交记录并更新本地镜像
→ Broker 异步调整分区角色,客户端刷新路由
Active Controller 为了串行处理后续事件,可以维护领先于已提交镜像的 Pending State(待定状态);但未越过 HW 的记录不是稳定集群事实,也不能作为成功结果暴露给集群其他节点。如果 Controller 在处理中宕机,新 Leader 只需重放已提交历史,未提交尾部由日志协调过程保留或截断,不需要猜测前任的内存状态。
12. Broker 存活检测不是 Controller 选举
Broker 启动后向 Active Controller 注册,获得用于标识本次有效注册的 Broker Epoch(Broker 任期),随后周期性发送 Broker Heartbeat(Broker 心跳)。
Kafka 4.3.1 的默认心跳间隔为 2 秒,会话租约为 9 秒,分别由 broker.heartbeat.interval.ms 和 broker.session.timeout.ms 控制。
心跳不仅表示进程仍在运行,还携带 Broker 已应用的元数据位置,以及主动隔离、解除隔离和受控关闭意图。新启动的 Broker 会先保持 Fenced(已隔离)状态;只有本地准备完成、请求解除隔离且元数据位置足够新时,Controller 才允许它恢复为 Active(活动)状态。这样可避免 Broker 在旧拓扑上过早接收请求。
Active Controller 维护 Broker 租约。租约到期意味着在指定窗口内没有观察到有效心跳,于是 Controller 生成 Broker 隔离及相关分区状态变化。它与 Controller 之间的 Fetch 存活检测是两套机制:
- Controller 是否仍有有效 Leader,由 KRaft Voter 的 Fetch、Pre-Vote、正式投票和多数派决定;
- Broker 是否可提供数据服务,由注册信息、Broker Epoch、心跳租约和 Fenced 状态决定。
13. Broker 故障如何触发普通分区选主
假设 Broker 1 领导 orders-0,Active Controller 判定其心跳租约过期,主要过程是:
- 在当前事件中生成 Broker 1 的隔离变更,并找出它领导的所有分区;
- 对每个分区从符合安全规则且未被隔离的副本中选择候选者;
- 生成新的 Partition Leader Epoch(分区领导任期),并更新 Leader、ISR、ELR 等分区状态;
- 将 Broker 与分区变化追加到元数据日志,等待 KRaft 多数派提交;
- Broker 应用新元数据:新 Leader 开始承接请求,其他副本转而跟随;
- 客户端收到
NOT_LEADER_OR_FOLLOWER等响应或定期刷新元数据后,改连新 Leader。
启用 ELR 时,Kafka 4.3.1 的候选顺序可概括为:
- ISR 非空时,从 ISR 中选择;
- ISR 为空而 ELR 非空时,从未被隔离的 ELR 中选择;
- 前两者都没有可用候选时,选择未被隔离的 Last Known Leader(最后已知领导者)。
unclean.leader.election.enable=true 还允许在最后手段下从 ISR 之外选主,但这明确接受数据丢失风险,默认值为 false;动态启用后也不保证立即发生,必要时应显式触发 unclean 选举。
候选副本不会因为"先响应"或"得到更多普通 Broker 支持"而胜选。Active Controller 根据元数据与确定性规则决策;新的 Partition Leader Epoch 用来隔离仍持有旧分区任期的请求。旧 Broker 恢复后必须先接受新元数据、协调日志并追赶,再作为 Follower 重新加入。
14. ELR 为什么补上了一个安全缺口
ELR 从 Kafka 4.0 引入,并从 4.1 起在新集群默认启用,因此 Kafka 4.3.1 的新集群通常会使用它。其前提是 Strict min ISR(严格最小同步副本)规则:当 ISR 数量低于 min.insync.replicas(最小同步副本数)时,数据分区 HW 不能继续推进,新的 acks=all 写入也无法成功。
这会产生一个重要结论:某个副本虽然已经不在当前 ISR 中,但如果它是在 HW 停止推进后才被移出,仍可能完整包含所有已提交数据,因而可以安全地等待接管。ELR 把这类副本显式记录下来,避免 Controller 只看当前 ISR 而过早遗忘安全恢复路径。
例如,副本集合为 A、B、C,min.insync.replicas=2。当 ISR 从 {A,B} 缩到 {A} 后,HW 不再推进;B 虽不在 ISR,却可能仍覆盖冻结前的全部已提交数据。若 A 随后故障,记录在 ELR 中的 B 比任意选择一个未知进度的副本更安全。
ELR 不表示"任何落后副本都安全",也不参与 KRaft 多数派提交。它是普通数据分区的候选资格元数据,由 Active Controller 维护;其价值是把连续故障下仍然安全的恢复信息显式保存下来。
15. 动态 Controller 成员变更如何保持安全
Kafka 4.3.1 同时支持静态和 Dynamic Controller Quorum(动态控制器仲裁组)。动态仲裁组由 kraft.version=1 启用,Voter 集合及端点以 VotersRecord(投票者记录)保存在 KRaft 日志与快照中。
controller.quorum.bootstrap.servers 只负责帮助节点发现仲裁组,不等同于权威成员列表。
安全变更遵循以下过程:
- 新 Controller 先以 Observer 身份启动并追上日志,再执行
add-controller,不能让空日志节点直接取得投票权; - 同一时间只处理一个 Voter 增删,避免多个并发配置形成难以证明安全的多数派关系;
- Leader 追加包含新集合的 VotersRecord。副本读到该记录后会据此更新仲裁计算;只有记录被新 Voter 集合的多数派提交,成员变更命令才算成功;
- 一次只增删一个成员,使旧、新多数派保持交集,保护已提交历史。
因此,扩容、缩容或替换 Controller 是共识协议操作,不是修改配置后同时重启所有节点。正确顺序是先启动并追赶新节点、加入并确认提交,再移除旧节点;移除 Controller 时应先执行 remove-controller,再关闭进程。
16. 五个故障场景推演
场景一:Active Controller 进程宕机
Follower 的 Fetch 超时后先 Pre-Vote,再正式选举。新 Leader 写入并复制本 Epoch 的领导变更记录,恢复安全提交边界后接管控制面。普通数据分区在元数据不变时可能继续服务,但管理操作和新的故障切换会短暂停顿。
场景二:旧 Controller Leader 被隔离在少数派
旧 Leader 的 Check Quorum 超时后放弃领导权,少数派无法提交新元数据;多数派完成 Pre-Vote 与正式选举。网络恢复后,旧 Leader 接受更高 Epoch,截断可能存在的未提交尾部并追赶。
场景三:失去 Controller 多数派
没有 Candidate 能取得多数票,控制面停止提交。现有 Broker 可能继续按照本地元数据镜像处理不需要新状态的数据请求,但无法可靠完成 Broker 故障隔离、分区重新选主、主题管理等操作。恢复目标应是找回原仲裁组多数派,而不是另建一套冲突的集群事实。
场景四:普通 Broker 宕机
Active Controller 的 Broker 心跳租约超时,生成隔离和分区变更,并经 KRaft 提交。
普通分区按 ISR → ELR → Last Known Leader 的安全顺序寻找候选;只有显式允许时,才使用可能丢数据的 unclean 选举。这里不需要 Controller 换届,除非故障进程同时承担 Controller 角色并影响了仲裁组。
场景五:Controller 落后过远或元数据磁盘丢失
如果 Controller 仍保留原有效目录身份,只是所需日志前缀已被清理,可以安装快照后继续拉取增量。若元数据磁盘真正丢失并重新格式化,Directory ID(目录标识)会变化;在动态仲裁组中,它不能自动继承旧 Voter 身份,而应先作为 Observer 追赶,再加入新的 (node.id, directory.id),最后移除旧身份。
17. 安全性与活性的边界
KRaft 的关键安全不变量是:
- 在一个确定的合法成员配置和 Epoch 中,最多只有一个 Candidate 能获得多数票;
- 一个 Voter 在同一 Epoch 最多投一张正式票;
- 新 Leader 的日志足以覆盖已提交前缀;
- 新 Leader 提交本 Epoch 记录前,不能借多数副本数量直接提交旧 Epoch 尾部;
- 动态成员变更一次只改变一个 Voter,并由新配置多数派提交;
- Controller Epoch、Broker Epoch 和 Partition Leader Epoch 分别隔离旧 Controller、旧 Broker 实例与旧分区 Leader。
活性则依赖现实条件:多数 Controller 可通信、存储能持续推进、超时高于正常尾延迟、成员配置与网络端点正确。共识算法不会消除网络或磁盘故障;它保证的是故障期间宁可暂停新的控制面提交,也不允许两套互不相容的元数据历史同时生效。
18. 运维观察点
- 使用
kafka-metadata-quorum.sh ... describe --status查看 Controller Leader、Leader Epoch、HW、Current Voters 和最大跟随延迟; - 同时观察 LEO、HW、Last Applied Record Offset,区分"没有复制""没有提交"和"已经提交但尚未应用";
- 关注
NewActiveControllersCount、Controller Event Queue 延迟、LastAppliedRecordLagMs和元数据加载/应用错误; - 关注
TimedOutBrokerHeartbeatCount、Fenced Broker 数、Offline Partition 数以及 unclean 选举; - 频繁 Controller 选举应优先排查网络尾延迟、长时间暂停和 Controller 磁盘延迟,再评估超时配置,避免仅靠放大超时掩盖故障;
- Controller 应跨独立故障域部署。三个稳定、低延迟的 Controller 通常比五个跨高延迟网络的 Controller 更可靠。
19. 小结
KRaft 的核心不只是"去掉 ZooKeeper",而是把 Kafka 控制面变成一套有明确任期、投票、提交、截断、快照和成员变更规则的日志状态机。
Pre-Vote 降低无意义的 Epoch 扰动,Check Quorum 约束失去多数派的旧 Leader,多数派与日志新鲜度保护已提交历史,当前任期记录又为换主后的 HW 建立安全门槛。
随后,Broker 心跳把节点可用性变化转换为元数据记录,Active Controller 再据此决定普通分区 Leader。沿着"检测 → Controller 选举 → 当前任期提交 → 元数据决策 → Broker 应用"的链路排查,才能判断一次故障究竟停在哪个阶段。
官方参考资料
- Kafka 4.3 KRaft Operations:kafka.apache.org/43/operatio...
- Kafka 4.3 Broker Configurations:kafka.apache.org/43/configur...
- Kafka 4.3 Topic Configurations:kafka.apache.org/43/configur...
- Kafka 4.3 Monitoring:kafka.apache.org/43/operatio...
- Kafka Improvement Proposal(Kafka 改进提案,KIP)-595 --- KRaft quorum protocol:cwiki.apache.org/confluence/...
- KIP-631 --- KRaft controller and metadata state machine:cwiki.apache.org/confluence/...
- KIP-853 --- KRaft controller membership changes:cwiki.apache.org/confluence/...
- KIP-996 --- Pre-Vote:cwiki.apache.org/confluence/...
- Kafka 4.3 Eligible Leader Replicas:kafka.apache.org/43/operatio...
- KIP-966 --- Eligible Leader Replicas:cwiki.apache.org/confluence/...
04|生产者与 Broker 写入链路:消息如何进入 Kafka
本篇结论:一次 Kafka 写入不是一次简单的网络调用,而是"路由---聚合---传输---校验---追加---复制---确认"的流水线。批处理提高吞吐,确认与重试决定可靠性,分区则决定顺序边界。
1. 发送之前:序列化与分区选择
生产者接收业务对象后,首先通过 Serializer(序列化器)把键和值转换为字节。Broker 不理解这些字节的业务含义,因此生产者和消费者必须通过契约确保格式兼容。Schema(模式)管理通常由 Kafka 之外的系统承担。
随后,生产者选择目标分区:
- 显式指定分区时,直接使用指定值。
- 有键时,通常对键计算稳定散列,使相同键落到同一分区。
- 无键时,生产者倾向于在一段时间内复用同一分区,形成更大的批次,再切换到其他可用分区。
第三种方式常被称为 Sticky Partitioning(粘性分区)。它的目标不是让流量永久粘在一个分区,而是在批次完成前减少分散,从而改善压缩率和网络利用率。
分区策略同时影响顺序与负载。如果所有记录使用同一个热点键,增加 Broker 数量也不能消除该分区的写入瓶颈。
2. 本地累加器:吞吐来自等待一点点
记录进入按主题分区组织的本地缓冲区,与同一分区的其他记录组成 Record Batch(记录批次)。发送线程在以下条件之一满足时取出批次:
- 批次接近目标大小;
- 等待达到
linger.ms; - 缓冲区或请求调度需要尽快发送;
- 应用显式刷新。
batch.size 是期望的批次容量,不是"必须凑满才发送";linger.ms 则允许生产者用少量等待换取更大批次。更大的批次意味着更少请求、更高压缩率和更连续的磁盘追加,但也增加排队延迟和单批失败重试的影响范围。
生产者缓冲区有上限。当应用写入速度持续超过网络发送能力时,新的发送调用会等待缓冲空间,超过等待限制后失败。这是客户端侧 Backpressure(背压),它比无限制占用内存更安全。
3. 压缩为什么以批次为单位
Kafka 支持 gzip、Snappy、LZ4 和 Zstandard(Zstd)等压缩方式。压缩发生在记录批次上,批次越大,重复模式越容易被压缩算法利用。
Broker 在大多数正常路径中可以保持批次压缩形态进行存储和复制,消费者最终解压。这意味着压缩不仅减少网络流量,也减少磁盘占用和页缓存压力,代价是生产者与消费者 CPU 开销。
选择压缩算法不能只比较压缩率:还要考虑编码速度、解码速度、消息大小、CPU 预算和网络成本。Kafka 的高吞吐来自端到端传递批次,而不是 Broker 对每条记录反复拆装。
4. 元数据与连接路由
生产者通过 bootstrap.servers 连接任意入口 Broker,获取集群元数据。元数据告诉客户端每个主题分区的领导者地址。之后,客户端按目标 Broker 聚合多个分区的批次,在少量连接上发送 Produce 请求。
如果 Broker 返回 NotLeaderOrFollower 等错误,说明客户端看到的领导者信息已经过期。生产者会刷新元数据并把可重试批次路由到新领导者。入口 Broker 不承担固定代理职责,客户端自己维护连接和路由。
5. Kafka 协议与请求关联
Kafka 使用长度定界的二进制请求---响应协议。请求头包含 API Key(接口标识)、API Version(接口版本)、Correlation ID(关联标识)和可选客户端标识。关联标识让客户端能够把异步收到的响应映射回原请求。
客户端会先探测 Broker 支持的 API 版本,再选择双方都理解的格式。这是滚动升级期间新旧客户端和 Broker 可以共存的重要基础。
记录批次的关键字段包括领导者任期、校验值、压缩类型、事务标志、生产者标识、生产者纪元和序列号。这里不需要记住二进制位布局,只需理解:同一个批次同时承载了数据、压缩、完整性、幂等和事务语义。
6. Broker 的请求处理流水线
Broker 的网络层和业务处理层分离:
- 监听线程接受连接。
- 网络处理线程读取并解析完整请求,把请求放入共享队列。
- 请求处理线程完成权限、配额、协议和分区状态校验。
- 分区领导者把批次追加到本地日志。
- 请求根据确认等级进入等待复制或立即响应路径。
- 网络线程把响应写回客户端。
分离的好处是慢磁盘或复杂请求不会让所有网络连接停在同一个线程上;代价是系统内部出现多个排队点。性能排查必须区分网络空闲度、请求队列时间、本地处理时间和等待复制时间。
7. 领导者追加前检查什么
Broker 并非收到字节就直接写盘,通常需要检查:
- 调用者是否有主题写权限;
- 请求是否到达当前领导者;
- 批次大小和格式是否合法;
- 校验和是否正确;
- 时间戳是否在允许范围内;
- 幂等生产者的纪元和序列号是否连续;
- 事务生产者是否具有有效状态;
- 配额是否允许继续处理。
校验通过后,Broker 为批次分配分区位点并追加到当前活动日志段。对于压缩批次,内部记录的相对位点与批次基础位点共同确定最终逻辑位置。
8. acks 到底确认什么
acks=0
客户端发送后不等待 Broker 响应。延迟最低,但无法知道请求是否到达、格式是否有效或领导者是否已切换。
acks=1
领导者本地追加成功后响应,不等待所有 ISR 复制。如果领导者在跟随者复制前永久丢失,已确认数据仍可能丢失。
acks=all
领导者等待当前同步副本集合完成复制,并满足 min.insync.replicas 约束后响应。它提供最强的常规复制保证,但若可用同步副本不足,会主动拒绝写入,以可用性换取耐久性。
acks=all 不是"等待副本因子指定的所有副本",而是围绕当前 ISR 和最小同步副本建立承诺。副本因子为 3、min.insync.replicas=2 是常见组合,但最终语义仍取决于 ISR 变化和选主策略。
9. 超时、重试和不确定结果
网络超时只能说明客户端没有及时收到结果,不能证明 Broker 没有写入。请求可能在服务端成功,但响应在返回途中丢失。生产者重试后,如果没有幂等机制,就可能形成重复记录。
需要区分:
- 请求等待超时:一次请求能等待多久。
- 投递总时限:从记录进入客户端到最终成功或失败的整体上限。
- 重试退避:失败后多快再次尝试。
- 元数据恢复:领导者变化后何时重新路由。
现代 Kafka 生产者通常启用幂等写入,用生产者标识和序列号识别重试批次。幂等解决的是 Kafka 分区日志内的重复追加,不会自动消除业务系统对同一事件的重复处理;完整边界将在事务篇讨论。
10. 一条记录的三种"成功"
应用常把以下状态混为一谈:
send被调用:记录可能仍在客户端缓冲区。- Future 或回调成功:满足配置的 Broker 确认条件。
- 业务完成:消费者已处理记录,并使下游副作用满足业务一致性。
Kafka 只能直接承诺第二层及其日志语义。第三层需要消费提交、幂等业务操作或端到端事务共同保证。
11. 小结
生产者通过分区选择确定顺序边界,通过批处理和压缩提高效率,通过元数据直连领导者。Broker 把请求处理与网络 I/O 分开,在校验后追加日志,再依据复制进度和确认等级决定何时响应。下一篇将进入日志内部,解释顺序追加为何仍需要段、索引、页缓存和清理机制。
官方参考资料
- Kafka 4.3 Producer Configurations:kafka.apache.org/43/configur...
- Kafka 4.3 Message Format:kafka.apache.org/43/implemen...
- Kafka 4.3 Network Layer:kafka.apache.org/43/implemen...
05|日志存储引擎:段、索引、清理与分层存储
本篇结论:Kafka 的存储效率来自"只在尾部追加、按段管理、用稀疏索引定位、让操作系统缓存热数据"。位点是逻辑位置,日志段是物理管理单位,保留和压缩都以段为主要操作边界。
1. 分区日志的物理形态
每个分区在 Broker 上对应一个日志目录。逻辑上它是一条不断增长的日志,物理上则被切分为多个 Log Segment(日志段)。只有最后一个段处于活动状态并接受追加,较旧段是不可变或近似不可变的封闭段。
段文件通常以该段第一条记录的基础位点命名。例如,一个名为 00000000000012340000.log 的文件表示该段从位点 12340000 附近开始。与数据文件配套的还有位点索引、时间索引以及事务和生产者状态相关文件。
分段解决了三个问题:
- 删除历史数据时不必改写巨型文件;
- 索引和恢复范围可控;
- 活动写入与旧段读取相互干扰较小。
2. 位点不是字节地址
Offset(位点)是 Kafka 暴露给协议的逻辑序号,并不等于数据文件中的字节位置。查找某个位点通常分两步:
- 根据各段的基础位点找到可能包含目标记录的日志段。
- 在该段的稀疏位点索引中找到不大于目标位点的最近索引项,再从对应文件位置顺序扫描到目标记录。
索引不记录每条消息,因而占用更少内存和磁盘。Kafka 利用"位点单调递增、段内顺序存储"把随机查找问题转化为小范围扫描。
3. 时间索引的作用
Time Index(时间索引)把时间戳映射到近似位点,支持"查找某个时间之后的位点"等操作。它同样是稀疏的,并不提供任意业务时间的精确数据库索引。
Kafka 允许使用生产者时间或 Broker 追加时间,选择会影响基于时间的查询和保留判断。若生产者时间严重漂移,还可能影响日志保留,因此 Broker 可以限制记录时间戳相对服务器时间的允许范围。
4. Record Batch 是磁盘和网络共同单位
Record Batch(记录批次)包含基础位点、最后位点增量、时间范围、压缩属性、领导者任期、校验和、生产者标识和序列号等信息。批次内部记录使用相对位点和相对时间,减少重复字段。
同一批次格式可以贯穿生产者、Broker 磁盘和消费者传输,避免 Broker 对每条记录进行不必要的对象化和格式转换。这是 Kafka 高吞吐的重要原因,也解释了为什么消息大小限制通常针对"压缩后的批次"而不是单条业务对象。
CRC-32C 校验覆盖批次的重要内容,用于发现传输或存储损坏。Broker 恢复活动日志段时,会验证记录边界与校验值,并把损坏或未完整写入的尾部截断到最后有效位置。
5. 顺序写与页缓存
Kafka 主要对活动日志尾部执行顺序追加。顺序写比随机更新更容易让磁盘、文件系统和操作系统合并 I/O。Kafka 大量依赖 Operating System Page Cache(操作系统页缓存)保存近期读写的数据,而不是在 Java 堆中维护一份同等规模的数据缓存。
这样做有几个好处:
- 减少 Java Heap(Java 堆)压力和垃圾回收停顿;
- 进程重启后页缓存未必立即丢失;
- 文件系统可以统一安排预读、写回和缓存淘汰;
- 热点尾部消费经常直接命中内存。
"写入页缓存 "不等于介质已完成持久化。Kafka 的常规耐久性主要依赖副本复制,而不是每条消息都强制 fsync。若所有副本和底层存储同时损坏,复制也无法替代备份或跨故障域设计。
6. 日志滚动与恢复
活动段达到大小或时间阈值后会 Roll(滚动)成封闭段,并创建新活动段。段太大会降低保留和清理粒度;段太小则增加文件数量、索引、打开关闭和扫描成本。
Broker 重启时重点恢复最新段,并加载检查点、生产者状态快照和事务索引等辅助信息。副本之间若存在分叉,还需要依据领导者任期和高水位截断不再属于权威历史的尾部。
7. Delete Retention:按时间或大小删除
Delete(删除型保留)适合事件流和审计历史。Kafka 根据 retention.ms、retention.bytes 等配置判断旧段是否可删除。删除通常以完整段为单位,因此"保留 7 天"并不意味着每条记录恰好在第 7 天立即消失;活动段滚动时间和后台检查周期都会产生粒度差异。
消费位点不会阻止日志删除。若消费者停顿时间超过保留窗口,它提交的位点可能已经早于日志起点,恢复时会触发 Offset Out Of Range(位点越界),再根据重置策略选择失败、最早或最新位置。
8. Log Compaction:按键保留最新状态
Log Compaction(日志压缩清理)不是压缩算法,而是按键清理旧版本记录。对同一个非空键,Kafka最终保留较新的值,使日志可以近似表达一张持续更新的状态表。
值为 null 的记录称为 Tombstone(墓碑记录),表示删除该键。墓碑也需要保留一段时间,确保下游重放者能够观察到删除事件;之后才可被清理。
压缩过程在后台读取封闭段,建立键到最新位置的映射,再重写仍需保留的记录。位点不会因为清理而重新编号,因此日志中允许出现位点空洞。消费者必须把位点看成游标,而不是连续消息计数器。
9. 分层存储
Tiered Storage(分层存储)把数据分为:
- 本地层:Broker 磁盘上的近期热段,延迟低;
- 远端层:对象存储或其他远端系统中的已封闭历史段,容量成本更低。
Broker 把符合条件的封闭段复制到远端,远端日志元数据记录段的位置、生命周期和领导者纪元。消费者读取较旧位点时,Broker 负责从远端获取数据,因此客户端仍使用普通 Fetch 协议。
本地保留和总体保留需要分开理解:本地层可以更早删除已上传段,而远端层继续满足更长的主题保留期。这样可以把 Broker 磁盘按热数据工作集规划,而不是按全部历史数据规划。
但分层存储不是透明的无限磁盘:远端读取延迟、并发、缓存、对象存储成本和故障都会进入系统边界。Kafka 4.3.1 还不支持对压缩主题使用分层存储,生产部署也需要外部 Remote Storage Manager(远端存储管理器)实现。
10. 容量与配置的因果关系
- 更大的段:文件更少,但保留和上传粒度更粗。
- 更长保留:历史可回放能力更强,但磁盘和远端成本更高。
- 更多分区:并行度更高,但段、索引、文件句柄和后台任务更多。
- 更大批次和更高压缩率:减少 I/O,但提高单次处理和解压开销。
- 频繁强制刷盘:降低单机缓存风险,但吞吐和尾延迟可能恶化。
调优不能孤立修改某个参数;应从写入速率、压缩后大小、副本因子、保留周期、热点读取窗口和故障恢复时间共同估算。
11. 小结
Kafka 把分区日志切成可管理的段,用稀疏索引定位,用批次格式贯穿网络和磁盘,并借助页缓存服务热数据。删除保留解决有限历史,日志压缩解决按键状态,分层存储解决冷热容量分离。下一篇将解释这些日志副本如何形成一条可提交、可恢复的权威历史。
官方参考资料
- Kafka 4.3 Log Implementation:kafka.apache.org/43/implemen...
- Kafka 4.3 Message Format:kafka.apache.org/43/implemen...
- Kafka 4.3 Tiered Storage:kafka.apache.org/43/operatio...
06|副本与一致性:复制、提交和故障恢复
本篇结论:领导者日志可以领先于已提交日志。Kafka 通过同步副本集合、高水位和领导者任期区分"已经写到领导者"与"故障后仍能保留",并用 ELR 改善严格最小同步副本条件下的安全选主。
1. 为什么有副本仍可能丢数据
假设一个分区有三个副本,生产者把记录写入领导者后立即收到成功,但两个跟随者尚未复制,领导者磁盘随即永久损坏。如果新领导者从未见过这条记录,数据仍然丢失。
因此,副本数量只描述潜在冗余,真正的耐久性还取决于:哪些副本足够同步、写入何时确认、领导者从哪些候选者中选择,以及故障是否跨越所有副本的共同故障域。
2. 跟随者如何复制
每个跟随者维护自己的 Log End Offset(日志末端位点,LEO),持续向领导者发送 Fetch 请求,从当前末端继续拉取批次。领导者在响应中返回数据和高水位等信息。
跟随者追加后再次 Fetch,新的请求位置也间接告诉领导者它已经复制到哪里。领导者根据所有同步副本的进度计算可提交范围。复制采用拉取模式,使慢副本自然按照自己的能力追赶,也复用普通消费的批量读取机制。
3. ISR 是动态承诺集合
In-Sync Replica(同步副本集合,ISR)包含领导者和当前被认为足够同步的跟随者。副本如果在规定时间内没有持续拉取并赶上领导者,会被移出 ISR;恢复追赶后可以重新加入。
"同步"并不要求每个时刻 LEO 完全相等,而是要求副本持续通信并在允许的时间窗口内追赶。低吞吐主题也不能仅靠消息差值判断健康,因此现代 Kafka 主要使用时间条件识别失速副本。
ISR 的意义是:正常选主优先从这个集合中选择,以确保新领导者包含已提交历史。
4. 高水位决定普通可见性
High Watermark(高水位,HW)表示该分区已复制到同步副本并可在正常故障恢复后保留的日志边界。普通消费者不会读取高水位之后的数据。
领导者本地 LEO 可能大于 HW:这部分尾部已经写入领导者,但尚未完成同步提交。若领导者故障,未提交尾部可能被截断。由此必须区分:
- LEO:某个副本实际拥有的数据末端;
- HW:非事务读取的已提交边界;
- Last Stable Offset(最后稳定位置,LSO):事务隔离下不会跨越未完成事务的可见边界。
LSO 不一定等于 HW;存在长事务时,read_committed 消费可能停在更早位置。
5. acks=all 与最小同步副本
生产者使用 acks=all 时,领导者等待同步副本复制并满足 min.insync.replicas 后确认。如果当前 ISR 小于最小值,写入会被拒绝。
例如副本因子为 3、最小同步副本为 2:
- 三个副本健康时可以正常写入;
- 一个跟随者故障后仍可保持两个 ISR 并写入;
- 只剩一个同步副本时,Kafka选择停止成功写入,而不是降低已经声明的耐久性。
这是典型的一致性---可用性取舍。把最小同步副本设为 1 可以提高故障期间可写性,却允许领导者在没有其他同步副本时确认数据。
6. Leader Epoch 防止错误截断
Leader Epoch(领导者任期)在每次分区领导权变化时递增。副本记录不同任期从哪个位点开始,从而把日志历史分成若干领导周期。
故障恢复时,仅比较 LEO 不足以判断哪段尾部权威:更长的日志也可能来自已经失效的旧领导者。副本通过任期和领导者返回的任期末端,找到双方最后一致的位置,再截断分叉尾部并重新复制。
任期把"长度比较"升级为"历史世代比较",减少旧领导者重新加入后传播错误尾部的风险。
7. 领导者切换过程
当 Controller 判定领导者 Broker 不可用时:
- 从安全候选集合中选择新领导者。
- 增加分区领导者任期并写入元数据日志。
- Broker 和客户端接收新的分区元数据。
- 新领导者根据已提交历史提供生产和消费服务。
- 旧领导者恢复后作为跟随者截断分叉尾部并追赶。
切换期间,客户端可能收到非领导者、过期纪元或连接错误。正确客户端会刷新元数据并重试;这也是生产请求结果可能暂时不确定的原因。
8. 非同步副本选主的风险
Unclean Leader Election(非同步副本选主)允许在 ISR 没有可用副本时,从落后副本中选择领导者。它可能快速恢复服务,但会永久丢失落后副本未包含的已提交数据。
因此,对大多数强调耐久性的业务应关闭非同步选主。只有在"恢复可用性比保留全部已确认数据更重要"的明确场景中,才应接受这一取舍,并通过监控和业务补偿管理后果。
9. ELR 如何扩展传统 ISR 模型
Eligible Leader Replicas(合格领导者副本,ELR)从 Kafka 4.0 起可用,并在 4.1 新集群中默认启用。
它服务于 Strict Min ISR(严格最小同步副本)语义:当 ISR 已小于最小同步副本时,高水位不再推进。此后某些被移出 ISR 的副本虽然不够"实时同步",却仍然包含全部已提交数据,因此可以作为安全候选者保留在 ELR 中。
选举时 Controller 按顺序考虑:
- 可用 ISR;
- 未被隔离的 ELR;
- 特定条件下的最后已知领导者。
ELR 的价值是避免把"暂时不在 ISR"一概等同于"不安全"。它扩展了安全候选集合,同时仍以已提交边界为依据。ELR 不是让任意落后副本都可以选主,也不能替代正确的副本因子和故障域规划。
10. 机架感知与共同故障
如果三个副本都位于同一机架、可用区或底层存储故障域,副本因子为 3 也无法抵御该故障域整体失效。Rack Awareness(机架感知)让副本分配尽量跨机架分布。
跨可用区复制会增加网络成本和确认延迟,但能提高区域故障容忍度。部署设计需要把业务恢复目标、写入延迟和故障域放在一起权衡。
11. 排障时看哪些信号
- Offline Partition Count 应保持为 0。
- Under Replicated Partitions 持续非零说明副本长期落后或不可用。
- ISR 频繁收缩和扩张通常意味着网络、磁盘或垃圾回收抖动。
- Follower Lag 要结合批次大小和低流量主题判断。
- Produce 请求错误应区分同步副本不足、领导者切换、超时和配额限制。
12. 小结
领导者的本地尾部并不天然等于已提交历史。ISR 描述正常同步集合,HW 描述普通读取的提交边界,Leader Epoch 帮助恢复时识别权威历史,ELR 则在严格最小同步副本语义下保留更多安全选主候选。下一篇将从消费者视角解释它读取哪个边界、怎样维护进度,以及消费组如何分配分区。
官方参考资料
- Kafka 4.3 Distribution Implementation:kafka.apache.org/43/implemen...
- Kafka 4.3 Eligible Leader Replicas:kafka.apache.org/43/operatio...
- Kafka 4.3 Broker Configurations:kafka.apache.org/43/configur...
07|消费者与消费组:拉取、位点与重平衡
本篇结论:消费者自己决定何时、从哪里拉取;消费组把分区所有权分配给成员;已提交位点只记录恢复起点。Kafka 4.3.1 同时支持经典协议和新一代服务端驱动的增量重平衡协议。
1. 为什么 Kafka 使用拉取模型
Kafka Consumer(消费者)主动向分区领导者发送 Fetch 请求。Pull Model(拉取模型)让消费者控制读取速度、批次大小和处理节奏,并能在落后时一次拉取大量连续数据。
如果由 Broker 主动推送,服务端必须猜测每个消费者能承受多大速率,还要处理慢消费者缓存。拉取把背压自然留在客户端:消费者处理不过来时减少或暂停 Fetch,日志仍按保留策略存在 Broker 上。
拉取不等于频繁空轮询。Fetch 请求支持最小数据量和最大等待时间:数据足够时立即返回,数据不足时可在 Broker 等待一小段时间,从而形成服务器端长轮询并减少空请求。
2. 一次 Fetch 如何读取多个分区
消费者从元数据中找到各分区领导者,并按 Broker 合并请求。一个 Fetch 可以包含多个分区和各自的起始位点、最大字节数。Broker 从高水位或事务可见边界内读取批次并返回。
Fetch Session(拉取会话)允许客户端和 Broker 缓存稳定的分区集合,后续只发送增量变化,减少拥有大量分区时的请求体和处理成本。
批次数据可以从文件或页缓存高效传输到网络。消费者拿到批次后完成解压、反序列化和拦截处理,所以高压缩比也会把更多 CPU 工作放到消费端。
3. Position、Committed Offset 和 Log End
必须区分三个位置:
- Current Position(当前位置):消费者内存中下一次准备返回或拉取的位置。
- Committed Offset(已提交位点):消费组持久化的恢复起点,通常表示下一条待处理记录。
- Log End Offset(日志末端位点):分区当前日志尾部。
消费者 poll 之后,当前位置可以前进,但已提交位点仍保持不变。只有发生位点提交,故障恢复起点才改变。
Lag 通常是日志末端与已提交位点之差。它衡量位置落后量,不包含单条记录处理耗时、下游排队和事件时间延迟,因此不能单独代表业务新鲜度。
4. 自动提交与手动提交
自动提交按周期提交最近一次 poll 推进的位置,使用简单,但如果应用在提交后、业务完成前失败,就可能跳过尚未完成的处理。
手动提交允许应用在更合适的边界推进位点:
- 处理一批成功后同步提交,语义清晰但会增加等待;
- 异步提交提高吞吐,但必须处理响应乱序和失败;
- 按分区提交可以让已完成分区独立前进;
- 与外部数据库配合时,Kafka 位点和业务副作用仍可能出现双写不一致。
"先处理后提交"通常形成 At-Least-Once(至少一次):处理成功但提交前宕机会导致重放。"先提交后处理"更接近 At-Most-Once(至多一次):提交后宕机会丢失本次处理机会。
5. 消费组解决什么问题
Consumer Group(消费组)让多个消费者协作处理一组订阅主题。传统消费组的关键约束是:同一消费组内,一个分区在同一时刻只分配给一个成员。因此组内最大有效并行度通常不超过可分配分区数。
不同消费组拥有独立的分区分配和已提交位点,可以从同一主题读取同一份数据。这让一份事件流同时服务搜索索引、风控、通知和分析等多个用途。
6. Group Coordinator 与内部主题
Group Coordinator(消费组协调器)由 group.id 映射到某个内部主题分区的领导者 Broker。它维护成员、订阅、组纪元、分区分配和位点提交。
已提交位点等状态写入 __consumer_offsets 内部主题。该主题经过复制,并使用日志压缩保留每个组---主题---分区键的较新状态。协调器故障后,新领导者可以重放内部主题恢复状态。
因此,消费组规模和提交频率会转化为协调器及内部主题负载。频繁逐条提交并不会提高数据处理能力,反而可能制造大量小请求和状态写入。
7. 经典重平衡协议
在经典协议中,成员通过 JoinGroup 加入,选出一个成员承担分配计算,再通过 SyncGroup 把结果同步给全组。成员加入、离开、超时或订阅主题分区变化都可能触发 Rebalance(重平衡)。
经典重平衡常包含全组同步屏障:成员需要在同一代中重新加入并获得分配。若使用"先全部撤销、再重新分配"的策略,处理会出现明显停顿。
静态成员标识可以让短暂重启的消费者保留身份,减少不必要的成员更替;协作式分配则允许分区逐步转移,而不是一次撤销全部所有权。但协议整体仍较复杂,部分状态由客户端协调。
8. 新一代消费者重平衡协议
Next Generation Consumer Rebalance Protocol(新一代消费者重平衡协议)从 Kafka 4.0 起达到 General Availability(正式可用,GA)。Kafka 4.3.1 服务端已支持,但客户端仍需设置 group.protocol=consumer 才会启用。
其核心变化包括:
- 分配器运行在服务端,不再由某个消费者担任组领导者计算;
- 心跳间隔和会话超时更多由服务端控制;
- 成员通过持续心跳接收增量分配变化;
- 不再依赖全组同步屏障;
- 组纪元、成员纪元和目标分配使状态转换更明确;
- 客户端线程模型与协议职责更简单。
增量设计意味着一个成员变化时,只转移必要分区,其他成员可以继续处理未受影响的分区。它降低大规模组的重平衡时间和协调峰值,但并不消除业务侧的分区交接问题。
9. 分区交接必须处理什么
当分区从成员 A 转移到成员 B 时,A 需要停止继续处理,并尽可能提交已完成进度;B 必须从最终可见的已提交位点恢复。如果 A 的异步任务仍在后台执行,即使协议已经撤销分区,也可能产生过期副作用。
稳健应用需要:
- 把分区撤销视为所有权丢失;
- 停止或隔离该分区的未完成异步任务;
- 避免在失去所有权后提交旧位点覆盖新成员进度;
- 使下游操作可以识别重复或过期执行。
重平衡协议只能转移 Kafka 分区所有权,不能自动撤销已经发往外部系统的请求。
10. 消费者活性与 poll
消费者需要同时满足两类时间约束:
- 会话心跳表明进程仍然存活;
- 最大处理间隔限制两次
poll之间可以停留多久。
如果业务处理长时间阻塞轮询,即使后台心跳仍在发送,客户端也可能被视为无法继续及时消费而离开组。常见做法是控制单次返回记录数、把耗时任务交给受控工作池、暂停相应分区并持续轮询协议事件,但必须保证位点提交与异步完成顺序一致。
11. 位点重置不是日常提交
当没有已提交位点或位点已被日志保留删除时,auto.offset.reset 决定从最早、最新或指定时长附近开始。它不是每次启动都执行的"默认起点",也不应替代正常位点管理。
人为重置消费组位点是有副作用的运维操作。重置前应停止活动成员、确认目标分区和新位置,并评估重复处理或跳过数据的业务影响。
12. 小结
消费者通过拉取控制自己的节奏,当前位置描述本次会话进度,已提交位点描述故障恢复起点。消费组协调器把分区分给成员,并把关键状态写入内部主题。新一代协议把分配和更多定时控制移到服务端,以增量状态转换减少全组停顿。下一篇将打破"一个分区只能由组内一个消费者处理"的约束,介绍 Share Groups。
官方参考资料
- Kafka 4.3 Consumer Rebalance Protocol:kafka.apache.org/43/operatio...
- Kafka 4.3 Consumer Configurations:kafka.apache.org/43/configur...
- Kafka 4.3 Basic Operations:kafka.apache.org/43/operatio...
08|Share Groups:Kafka 的队列式消费模型
本篇结论:传统消费组分配的是分区所有权,Share Groups(共享消费组)分配的是分区内记录的临时处理权。它用记录获取锁和显式确认换取更高并发,但也放弃了严格的分区处理顺序。
1. 传统消费组的并发上限
在传统 Consumer Group(消费组)中,同一分区只能分配给组内一个成员。如果主题只有 6 个分区,即使启动 20 个消费者,同一时刻通常也只有 6 个成员能获得有效分区。
这种模型很适合按键有序的事件流,但对大量相互独立、处理时间差异明显的任务并不理想。为了提高并发而增加大量分区,会带来元数据、文件、复制和重平衡成本。
Share Group 的目标是让多个消费者能够同时从同一分区获取不同记录,使处理并发不再严格受分区数限制。
2. 所有权从分区下沉到记录
传统消费组的核心状态是"分区 P 当前属于成员 A"。Share Group 的核心状态更接近"分区 P 中位点 100~110 的记录当前被成员 A 获取,其中哪些已经确认、哪些等待重投"。
Kafka 仍以分区保存数据,也仍由分区领导者提供读取;变化发生在消费协调层。Share Coordinator(共享消费协调器)维护 Share Partition(共享分区)的投递状态,Broker 为返回给消费者的记录建立有时限的 Acquisition Lock(获取锁)。
锁有效期间,同一条记录不会同时交给另一个消费者。锁到期、消费者释放或连接失效后,记录可以重新进入可投递状态。
3. 获取锁不是数据库行锁
获取锁是一项带超时的投递租约,而不是持有到事务结束的永久排他锁。它解决的是"当前由谁尝试处理",不保证外部业务操作的互斥性。
如果消费者处理时间超过锁时长且没有续期,记录可能被再次投递。原消费者的外部请求却未必能够撤回,因此业务处理仍应具备幂等性或去重机制。
每个共享分区允许同时被获取的记录数也有限制。达到上限后,新的 Fetch 会暂时得不到更多记录,直到部分记录被确认、释放或超时。这是一种服务端背压,防止无限累积在途任务。
4. 四种确认结果
显式确认模式下,消费者可以为记录指定不同结果:
- ACCEPT(接受):处理成功,完成该记录的投递。
- RELEASE(释放):本次暂时无法处理,允许再次投递。
- REJECT(拒绝):记录被判断为不可处理,不再继续正常投递。
- RENEW(续期):任务仍在处理,延长获取锁。
RENEW 适合少量确实超过锁时长的任务。若大量任务持续续期,会占据在途额度并阻碍投递进度。更好的设计通常是让任务粒度可控,或把极长任务拆分为可检查点化的步骤。
确认调用首先更新消费者本地待提交状态,随后在 poll 或显式提交时发送给 Kafka。应用不能把"调用了确认方法"直接等同于服务端已经持久化确认结果。
5. 隐式与显式确认
Implicit Acknowledgement(隐式确认)在下一次轮询或提交时,把前一批记录视为成功处理。代码简单,适合整批结果一致、处理快速的场景。
Explicit Acknowledgement(显式确认)要求应用在下一次轮询前为每条记录指定结果,适合一批记录中部分成功、部分暂时失败或永久失败的任务型负载。
显式确认提供更细控制,也增加了状态组合和错误处理复杂度。需要明确"处理完成""确认已发送""确认已提交"三个时刻,并在失败恢复时允许记录重投。
6. 投递次数与毒丸记录
Share Group 可以限制一条记录的最大投递次数,避免永久失败记录无限循环占用资源。达到限制或被拒绝后,Kafka 不会自动理解业务原因,也不会替应用生成完整的 Dead Letter Queue(死信队列,DLQ)处理流程。
应用通常仍需记录失败原因,并把需要人工或异步处理的内容写入独立主题。DLQ 应包含原主题、分区、位点、异常分类、尝试次数和必要的追踪信息,同时避免复制敏感数据。
7. 顺序性发生了什么变化
Kafka 仍按位点递增从单个分区取得记录,但多个记录可以被不同消费者并发处理。位点较大的记录可能先完成,因此业务完成顺序不再等于日志顺序。
如果业务依赖"同一账户的变更必须严格串行",传统消费组配合按键分区更合适。若任务彼此独立,Share Group 可以用较少分区支撑更大的工作消费者池。
8. 事务记录可见性
Share Group 的事务隔离级别是组级配置:
read_uncommitted可以读取普通记录和未决或已中止事务中的记录,可见边界受高水位约束。read_committed只投递普通记录和已提交事务记录,可见边界受 Last Stable Offset(最后稳定位置,LSO)约束。
一个长时间未完成的事务可能阻塞同分区后续事务记录的可见性。Share Group 提高的是消费并发,不会改变事务日志的可见性规则。
9. 与传统消费组的选择
选择传统消费组,当你需要:
- 分区内严格的处理顺序;
- 按键状态机或流式聚合;
- 明确的分区所有权;
- 与大量现有消费者生态兼容。
选择 Share Group,当你需要:
- 任务并发显著高于合理分区数;
- 单条任务耗时差异较大;
- 独立确认、释放、拒绝和续期;
- 更接近工作队列的处理模型。
如果需要复杂优先级、延时调度、任务依赖或工作流编排,专门的任务队列或工作流系统可能仍更合适。
10. 故障场景
消费者处理成功但确认未提交
锁到期后记录会再次投递,形成至少一次处理。业务侧应识别重复。
消费者处理时间超过锁时长
若没有 RENEW,其他成员可能取得同一记录。续期只能延长 Kafka 侧租约,不能撤销已经发生的并行外部副作用。
分区领导者切换
当前投递尝试可能结束,消费者重新连接新领导者并依据协调状态继续。应用不应假设持有的本地记录锁跨所有网络和领导者故障永久有效。
11. 监控重点
- 各确认类型的速率;
- 获取锁超时和续期比例;
- 投递次数接近上限的记录数;
- Share Fetch 与确认请求失败率;
- 在途记录数及其持续时间;
- 业务成功率与重复处理率。
传统 Lag 无法完整描述 Share Group:起始位点之后可能有些记录已完成,有些仍在途或等待重投。因此要结合投递状态和确认指标判断积压。
12. 小结
Share Group 把竞争粒度从分区下沉到记录,用有时限的获取锁和确认状态实现队列式并发。它提高了任务型负载的伸缩性,但处理顺序、重复执行和外部副作用仍需业务设计。下一篇将从生产端与消费端共同讨论幂等和事务,说明 Kafka 能把"一次"保证推进到什么边界。
官方参考资料
- KafkaShareConsumer 4.3.1 API:kafka.apache.org/43/javadoc/...
- Kafka 4.3 Group Configurations:kafka.apache.org/43/configur...
- Kafka 4.3 Monitoring:kafka.apache.org/43/operatio...
09|幂等与事务:从至少一次到精确一次
本篇结论:幂等生产者消除重试造成的分区内重复追加;事务把多个分区写入和消费位点提交组合成一个原子结果;Exactly-Once Semantics(精确一次语义,EOS)只在明确的 Kafka 事务边界内成立。
1. 先区分三种交付语义
- At-Most-Once(至多一次):可能丢失,但不主动重试导致重复。
- At-Least-Once(至少一次):允许重试,尽量不丢失,但同一业务记录可能被处理多次。
- Exactly-Once(精确一次):在定义好的观察边界内,每个输入只产生一次最终可见结果。
精确一次不是单个开关,而是生产重试、Broker 去重、事务提交、消费者隔离和业务副作用共同形成的协议。必须先说明"谁观察、观察什么结果",否则"恰好一次"没有可验证含义。
2. 重试为什么会重复
生产者发送批次,Broker 追加成功,但响应在网络中丢失。客户端超时后重试同一批次。如果 Broker 无法识别它是重试,日志就会出现两份内容完全相同但位点不同的记录。
仅减少重试会把重复问题变成丢失问题。更合理的方法是保留重试,同时让 Broker 能判断某批次是否已经接受。
3. 幂等生产者
Idempotent Producer(幂等生产者)使用三个关键元素:
- Producer ID(生产者标识,PID);
- Producer Epoch(生产者纪元);
- 每个主题分区独立递增的 Sequence Number(序列号)。
Broker 为分区维护近期生产者状态。收到批次后,它检查生产者纪元和序列范围:
- 序列紧接已接受批次,则正常追加;
- 序列与已接受批次重复,则把它识别为重试而不再次追加;
- 序列出现不允许的跳跃或旧纪元,则返回错误,防止日志顺序失控。
去重是按生产者会话和分区建立的。它不根据业务键或消息正文做全局去重,也不能阻止应用主动发送两次同一业务事件。
4. 纪元用于隔离旧生产者
网络分区或进程停顿可能使旧生产者在新实例接管后再次发送。Producer Epoch(生产者纪元)用于 Fencing(隔离)旧实例:协调器或 Broker 接受更高纪元后,携带旧纪元的请求失效。
这与 KRaft 的 Controller Epoch、Broker Epoch 和分区 Leader Epoch 体现同一原则:仅有身份还不够,必须用单调递增的世代阻止旧持有者重新获得权力。
5. 为什么幂等还不等于事务
幂等可以确保每个分区上的重试批次不重复,却不能原子地回答:
- 向主题 A 和主题 B 的写入是否必须同时可见;
- 消费输入后,输出写入和输入位点提交是否必须一起成功;
- 失败事务中的记录是否应被下游看见。
这些问题需要 Transaction(事务)协议。
6. Transactional ID 与协调器
应用配置稳定的 Transactional ID(事务标识)。Transaction Coordinator(事务协调器)根据该标识映射到 __transaction_state 内部主题的某个分区,并维护:生产者标识与纪元、事务状态、参与的主题分区和相关超时。
稳定事务标识让新进程能够隔离旧实例,并结束旧实例遗留的未完成事务。事务标识应与应用并发实例建立稳定、一一对应的所有权,多个活动实例不能随意共享同一标识。
7. 事务状态机
一个典型事务经历以下过程:
- 初始化事务生产者并获得生产者标识和纪元。
- 开始本地事务。
- 第一次写入某分区前,把该分区加入事务。
- 以事务批次形式向多个分区写入。
- 若处理的是 Kafka 输入,把消费位点作为事务的一部分发送。
- 请求提交或中止事务。
- 协调器持久化状态,并要求各参与分区写入 Commit Marker(提交标记)或 Abort Marker(中止标记)。
- 协调器完成事务状态。
标记是写入分区日志的控制记录,不会作为普通业务记录返回给应用。它让每个分区能够在重放时判断之前的事务批次最终是提交还是中止。
8. 原子性的真实形态
Kafka 事务不是让多个分区在同一物理瞬间写入。事务记录可以先后追加到不同分区,最终通过协调器状态和控制标记形成统一的可见结果。
在提交完成前,read_committed 消费者不会越过相关分区的 Last Stable Offset(最后稳定位置,LSO)返回未决事务结果。提交标记到达后,事务记录变为可见;中止标记到达后,消费者跳过相应事务批次。
因此,原子性体现为最终观察结果不可见一半,而不是磁盘写入动作绝对同时发生。
9. read_committed 与 LSO
High Watermark(高水位,HW)表示已复制的日志边界,但 HW 之前仍可能包含未完成事务。LSO 通常是 HW 与最早未完成事务起点之间的较小者。
read_uncommitted 消费者可以读取 HW 之前的事务记录,无论最终是否中止;read_committed 消费者只返回已提交事务和普通记录,并跳过已中止事务。
一个长事务会把 LSO 留在较早位置,使同分区之后已经复制的数据暂时不可见。这是事务隔离带来的 Head-of-Line Blocking(队头阻塞)。事务范围应尽量小,不能把 Kafka 事务当成长时间业务工作流。
10. Consume---Transform---Produce
Kafka 内部最典型的 EOS 流程是:
- 从输入主题读取记录;
- 计算输出;
- 在事务中写入一个或多个输出主题;
- 把输入消费位点一并加入同一事务;
- 提交事务。
如果提交成功,输出和输入进度一起可见;如果失败并中止,下游 read_committed 看不到输出,输入位点也不会前进,因此可以重新处理。
Kafka Streams 的精确一次处理保障就是建立在这些能力之上,并额外管理任务、状态存储和变更日志。
11. Kafka 4.x 的事务防御增强
Kafka 4.0 起的 Transactions Server-Side Defense(事务服务端防御)强化了服务端验证。在新的事务协议中,生产者纪元会更积极地推进,以确保每个事务只包含预期记录,并进一步隔离跨事务的过期请求。
该能力由事务功能版本控制。滚动升级时,先升级二进制,再在验证后提升功能版本,能够避免新协议在旧节点仍存在时过早启用。
12. 精确一次的边界
Kafka 事务可以覆盖 Kafka 主题写入和 Kafka 消费位点,但不能自动把外部数据库、HTTP 服务、邮件、支付请求或对象存储纳入同一原子提交。
面对外部副作用,常见策略包括:
- 让下游操作按业务键幂等;
- 使用唯一约束或去重表;
- 使用 Transactional Outbox(事务发件箱)把数据库变更与待发送事件写入同一本地事务;
- 采用可恢复的状态机和补偿,而不是宣称跨系统全局精确一次。
正确表述不是"Kafka 保证业务恰好执行一次",而是"Kafka 在指定主题和消费位点的事务读取模型下提供一次最终可见结果"。
13. 小结
幂等生产者用生产者标识、纪元和分区序列号消除网络重试重复;事务协调器把多分区写入与消费位点提交组织成状态机;控制标记和 read_committed 决定最终可见性。Kafka 的 EOS 很强,但边界明确,外部副作用仍需业务幂等或本地事务设计。
官方参考资料
- Kafka 4.3 Transaction Protocol:kafka.apache.org/43/operatio...
- Kafka 4.3 Message Format:kafka.apache.org/43/implemen...
- Kafka 4.3 Producer Configurations:kafka.apache.org/43/configur...
10|性能、容量与可观测性:Kafka 为什么快,边界在哪里
本篇结论:Kafka 的性能不是某个"零拷贝"技巧带来的,而是分区并行、顺序追加、批处理、压缩、页缓存和端到端背压共同作用的结果。调优必须先找到瓶颈资源和排队位置。
1. 先定义性能目标
吞吐、延迟和耐久性不能被同一个参数同时最大化:
- 更长等待可以形成更大批次、提高吞吐,却增加低流量时延迟;
- 更多同步副本和跨区域复制提高耐久性,却增加确认路径;
- 更强压缩减少网络和磁盘,但增加 CPU;
- 更多分区提高并行度,也增加元数据和后台任务成本。
因此,调优前应明确服务级目标:平均吞吐、峰值吞吐、端到端 P99 延迟、允许的恢复点、允许的恢复时间和成本上限。
2. 六个性能支点
2.1 顺序追加
每个分区只向活动段尾部追加,避免在大文件中随机更新。磁盘和文件系统更容易合并写入,恢复也主要检查尾部。
2.2 批处理
生产者把多条记录合并为批次,一个请求也能携带多个分区批次。固定的系统调用、协议头和请求调度成本被更多记录分摊。
2.3 压缩
批次压缩同时减少客户端到 Broker、Broker 副本之间以及磁盘上的字节数。系统瓶颈如果在网络或磁盘,适当增加 CPU 换压缩通常有收益。
2.4 页缓存
热数据由操作系统页缓存服务,避免在 Java 堆中复制一套大型对象缓存。尾部消费者经常直接读取刚刚写入内存的页面。
2.5 少复制的数据路径
Kafka 尽量让同一记录批次格式贯穿生产、存储、复制和消费。在支持的读取路径上,文件数据可通过操作系统机制高效传送到网络,减少用户态重复复制。
2.6 分区并行
不同分区可以位于不同 Broker,并由不同客户端线程并行处理。横向扩展来自把负载拆到多个独立日志,而不是让单个分区无限变快。
3. 零拷贝不应被神化
Zero-Copy(零拷贝)通常指减少数据在内核态和用户态之间的重复搬运。它对未加密、从本地文件直接发送等路径很有效,但不能覆盖所有情况:
- Transport Layer Security(传输层安全协议,TLS)可能需要用户态加解密;
- 远端分层存储需要额外读取和缓存;
- 消息转换或检查可能要求访问内容;
- 消费者仍要解压和反序列化。
Kafka 快的根本是整条路径少做无意义工作,零拷贝只是其中一个环节。
4. 一次请求在哪里排队
端到端延迟可以粗略拆分为:
客户端聚合等待+ 网络往返+ Broker 请求队列+ 本地处理与日志追加+ 副本确认等待+ 响应队列+ 消费者拉取等待+ 业务处理
只观察总延迟无法定位问题。Broker 网络线程空闲度很高但请求队列时间上升,可能是请求处理或存储瓶颈;本地处理很快但远端时间高,可能在客户端网络;生产确认等待上升且 ISR 波动,可能是慢副本。
5. 生产者调优的因果关系
- 增加
linger.ms:低流量下增加等待,通常改善批次与压缩。 - 增加批次容量:允许更大批次,但只有流量足够时才会实际使用。
- 增加缓冲区:吸收短时突发,但不能解决长期发送能力不足。
- 选择更强压缩:减少网络和磁盘,增加生产和消费 CPU。
- 增加在途请求:提高链路利用率,但必须依赖幂等和正确顺序语义。
- 降低确认等级:减少复制等待,同时降低故障耐久性。
调优顺序通常是先确认分区均衡和批次是否足够,再判断 CPU、网络或 Broker 是否成为瓶颈,而不是一开始就扩大所有缓冲区。
6. 消费者调优的因果关系
- 增加 Fetch 最小字节和等待时间可以提高批量效率,但增加低流量延迟。
- 增加单次拉取上限可以提高吞吐,也增加解压、反序列化和内存峰值。
- 增加消费者数量只在存在可分配分区或使用 Share Group 时提高并行度。
- 频繁提交位点会增加协调器和内部主题负载。
- 业务处理慢时,应先优化处理或建立受控并行,不应只让消费者预取无限记录。
消费者 Lag 增长可能来自生产突发、分区倾斜、下游变慢、重平衡、长事务或远端历史读取,必须结合业务处理速率和分区维度判断。
7. 分区数如何影响容量
分区数决定最大并行度,却也带来:
- 每副本更多日志段和索引文件;
- 更多领导者和复制 Fetch;
- 更大的元数据与选举批次;
- 更复杂的消费组分配;
- 更多文件句柄和后台清理任务。
规划分区时,可以从单分区在目标批次、压缩、复制和延迟条件下的实测吞吐反推所需分区数,再预留增长空间。不要只用理论磁盘带宽除以消息大小,因为瓶颈可能在键倾斜、客户端连接或副本网络。
8. 容量估算
先用压缩后的实际 Broker 入站速率估算,而不是使用业务对象的内存大小:
每日新增数据 ≈ 压缩后写入字节/秒 × 86,400本地副本容量 ≈ 每日新增 × 本地保留天数 × 副本因子总资源需求 ≈ 数据容量 ÷ 目标磁盘使用率 + 恢复与突发余量
还应加入:索引和内部主题、段滚动临时空间、日志压缩重写空间、重新分配流量以及一个副本故障后剩余节点接管的容量。
使用分层存储时,总历史容量主要进入远端层,本地磁盘按热数据窗口规划;但回溯读取会消耗远端吞吐、缓存和出口成本。
9. 四类关键指标
客户端
生产请求速率、批次大小、压缩率、缓冲等待、重试和错误;消费 Fetch 延迟、返回字节、记录处理速率和按分区 Lag。
Broker 请求层
请求总时间、队列时间、本地处理时间、远端等待时间、网络线程空闲度、请求处理线程空闲度和被限流时间。
复制与存储
离线分区、未充分复制分区、ISR 收缩/扩张、跟随者延迟、磁盘使用率、磁盘服务时间、页缓存命中趋势和日志清理积压。
控制与协调
Controller 事件队列、元数据加载延迟、领导者选举;消费组重平衡、位点提交失败、事务超时以及 Share Group 获取和确认指标。
10. Java 与操作系统观察
Kafka 服务端主要把数据缓存交给操作系统,因此不能只盯 Java Heap(Java 堆)。同时观察:
- Garbage Collection(垃圾回收,GC)停顿和分配速率;
- 进程常驻内存与页缓存;
- 磁盘吞吐、延迟、队列深度和使用率;
- 网络吞吐、重传和跨区流量;
- 文件句柄与线程数;
- CPU 使用率和上下文切换。
远程 Java Management Extensions(Java 管理扩展,JMX)默认并不安全。启用远程监控时必须配置认证、加密和网络访问控制。
11. 推荐的调优流程
- 固定消息大小、键分布、副本因子、确认等级和压缩方式。
- 用接近生产的数据分布建立基线,不只发送零字节或同一重复文本。
- 同时记录吞吐、P95/P99 延迟、错误、资源和队列指标。
- 找到第一个饱和资源或排队点。
- 每次只改变一类变量,并记录因果假设。
- 加入节点故障、领导者切换和消费者重平衡测试。
- 验证优化没有破坏耐久性、恢复时间或成本目标。
12. 小结
Kafka 通过顺序追加、批次、压缩、页缓存和分区并行减少单位记录成本。真实性能问题则表现为某个资源饱和后在相邻队列形成等待。有效调优从服务目标和瓶颈证据出发,并同时验证故障条件。最后一篇将把这些机制放入长期生产环境,讨论安全、租户隔离、容灾和升级。
官方参考资料
- Kafka 4.3 Monitoring:kafka.apache.org/43/operatio...
- Kafka 4.3 Hardware and OS:kafka.apache.org/43/operatio...
- Kafka 4.3 Producer Configurations:kafka.apache.org/43/configur...
11|生产治理与技术演进:安全、容灾、升级及生态边界
本篇结论:生产 Kafka 的难点不是启动 Broker,而是持续维护身份、权限、故障域、容量和版本不变量。安全、容灾和升级都必须同时覆盖数据面、控制面和协调面。
1. 从"能运行"到"可治理"
一套生产集群至少要回答:
- 谁可以连接,连接的是哪个 Listener(监听器);
- 客户端身份如何认证,能访问哪些资源;
- 某个租户能消耗多少网络和请求资源;
- 单机、机架、可用区或区域故障后损失多少数据、多久恢复;
- 二进制、元数据和协议功能如何安全升级;
- 哪些能力属于 Kafka,哪些应交给上下游系统。
如果这些问题没有明确答案,副本因子和监控面板无法自动把实验集群变成生产平台。
2. Listener 是安全边界起点
Kafka 可以配置多个 Listener(监听器),分别承载客户端、Broker、Controller 或不同网络区域的通信。监听地址决定进程在哪里接收连接,Advertised Listener(通告监听地址)决定客户端获取元数据后实际连接哪里。
常见错误是 Broker 在容器或内网监听正确,却向客户端通告不可达的主机名;安全上更危险的错误则是把 Controller 或内部 Broker 通信端口暴露到不可信网络。
应根据通信角色隔离监听器、网络策略、证书和认证机制,而不是让所有流量共用一个明文端口。
3. 加密、认证与授权
Kafka 安全可以分成三层:
3.1 加密
Transport Layer Security(传输层安全协议,TLS)保护传输内容并校验服务器身份,也可以使用双向 TLS 认证客户端。启用 TLS 会增加握手和加解密 CPU,应通过连接复用、硬件和容量测试评估,而不是因此关闭安全。
3.2 认证
Simple Authentication and Security Layer(简单认证与安全层,SASL)支持 GSSAPI、PLAIN、SCRAM-SHA-256、SCRAM-SHA-512 和 OAUTHBEARER 等机制。PLAIN 凭据必须配合 TLS,避免在网络中暴露。
3.3 授权
Access Control List(访问控制列表,ACL)把已认证主体映射到主题、消费组、事务标识和集群操作。授权应遵循最小权限:生产者只写目标主题,消费者只读目标主题并访问自己的消费组,自动建主题和集群管理权限应严格控制。
内部主题、Controller 管理接口和运维工具同样需要权限设计,不能只保护业务主题。
4. 多租户与配额
共享集群中的一个异常客户端可能通过高带宽、海量小请求、大量分区或频繁位点提交影响其他租户。Quota(配额)可以按用户、客户端标识等维度限制生产和消费带宽或请求资源。
配额不是完整隔离。主题命名、ACL、分区上限、消息大小、保留期、连接数、变更审批和成本归属也需要平台规则。对强隔离、独立升级或监管要求高的租户,独立集群通常比复杂共享策略更可控。
5. 高可用从故障域开始
副本应跨机架或可用区放置,Controller 仲裁节点也应跨独立故障域分布。必须避免:三个副本共用同一宿主机、存储阵列或网络出口;三个 Controller 在同一机架;监控与 Kafka 同时依赖同一故障域。
常见生产基线是三个副本、acks=all、最小同步副本为 2,并关闭非同步副本选主。但这只是起点,还要验证在单区故障、磁盘满、网络抖动和滚动维护期间能否维持目标。
Recovery Point Objective(恢复点目标,RPO)描述最多可丢失多少数据;Recovery Time Objective(恢复时间目标,RTO)描述多久恢复服务。Kafka 配置必须服务于明确的 RPO/RTO,而不是追求抽象的"高可用"。
6. 备份与跨集群容灾
同一集群副本主要防节点和局部故障,不等于防误删除、凭据泄漏、软件缺陷或整个区域灾难。跨集群复制可以使用 MirrorMaker 2 或其他复制方案,把选定主题复制到灾备集群。
需要认识到:
- 跨集群复制通常是异步的,因此存在 RPO;
- 主题数据、消费位点、ACL、配置和应用切换并非自动完全一致;
- 双向复制必须处理主题命名、循环和冲突;
- 灾备有效性取决于定期演练,而不是"目标集群有数据"。
对关键数据,还应保留独立、不可由日常 Kafka 管理权限直接删除的备份或归档路径。
7. 滚动升级的三层版本
升级时要区分:
- 软件二进制版本:节点实际运行的 Kafka 程序版本。
- Metadata Version(元数据版本):集群元数据允许使用的格式与行为版本。
- Feature Level(功能版本):KRaft、消费者协议、事务、ELR 等能力的启用级别。
安全流程通常是:
- 阅读目标版本升级说明、兼容矩阵和已知限制。
- 确认当前集群已经满足 KRaft、Java 和中间版本要求。
- 备份关键配置和元数据观察结果。
- 逐台升级 Controller 和 Broker,保持仲裁多数与足够 ISR。
- 验证生产、消费、重平衡、事务、延迟和错误率。
- 观察稳定后,再提升元数据或功能版本。
- 功能版本提升前确认降级是否仍被支持。
Kafka 4.3 只支持 KRaft。仍在 ZooKeeper 模式的旧集群必须先完成迁移,不能直接把 4.3 二进制放入原集群。
8. 客户端兼容与升级顺序
Kafka 协议具有版本协商能力,但"能连接"不代表所有新行为自动生效。例如新一代消费者协议在 Kafka 4.3.1 服务端可用,客户端仍需显式选择;新的事务协议也受功能版本和客户端能力共同控制。
升级应覆盖真实客户端组合,特别检查:
- 生产者重试、幂等和事务;
- 消费组在线滚动转换;
- 自定义分区器、分配器和拦截器;
- Kafka Connect 连接器与 Kafka Streams 状态恢复;
- 管理脚本和监控指标名称变化。
9. 日常变更的风险排序
高风险变更通常包括:
- 降低副本因子或最小同步副本;
- 开启非同步副本选主;
- 大规模分区重分配;
- 缩短保留期或删除主题;
- 重置消费位点;
- 修改 Controller 仲裁成员;
- 提升不可降级的功能版本;
- 轮换认证和加密材料。
这些操作应有明确目标、影响范围、前置检查、回滚或恢复方案和变更后验证指标。Kafka 中很多破坏性结果会快速复制到所有副本,副本不能替代变更治理。
10. Kafka Connect、Kafka Streams 与模式管理
Kafka Core 提供日志、复制、客户端协议和协调能力,周边组件解决不同层面的问题:
- Kafka Connect:以 Source Connector(源连接器)和 Sink Connector(汇连接器)持续搬运外部系统数据,管理任务、配置和位点。
- Kafka Streams:在应用进程内提供流处理拓扑、状态存储、变更日志和重分区能力。
- Schema Registry(模式注册中心):管理数据模式、兼容规则和序列化契约,通常由生态产品提供,不是 Kafka Broker 内建的业务模式数据库。
- MirrorMaker 2:用于跨集群主题复制和部分消费组位点同步,不是同步全局数据库。
理解边界能避免把所有问题都塞进 Broker:复杂数据转换属于流处理或 Connect,业务模式兼容属于数据治理,跨系统原子提交仍需要应用设计。
11. Kafka 不适合什么
Kafka 并非所有异步场景的默认答案。以下需求可能更适合其他系统:
- 任意字段低延迟查询和频繁原地更新;
- 跨大量键的强一致事务;
- 复杂优先级、延时任务和工作流编排;
- 单条消息超大且主要作为文件传输;
- 极小规模且无回放、并行和多订阅需求的简单任务通知。
Kafka 的优势在于高吞吐、可回放、多订阅、分区有序和可复制事件日志。只有这些特性解决了真实问题,它的运维成本才值得。
12. 最终检查清单
- 主题分区、键分布和容量模型有依据。
- 副本、ISR、最小同步副本和选主策略符合 RPO。
- Controller 仲裁和数据副本跨故障域。
- 客户端启用认证、加密、授权和必要配额。
- 监控覆盖请求、复制、控制器、协调器、磁盘和业务 Lag。
- 位点重置、主题删除、仲裁变更和版本提升受到审批。
- 升级和灾备切换经过演练。
- 外部副作用的重复处理和补偿边界清楚。
13. 全文总结
Kafka 以分区追加日志承载数据,以副本和提交边界保护历史,以 KRaft 元数据日志维护控制面,以协调器状态机组织消费和事务。批处理、页缓存和分区并行带来性能;纪元、隔离和功能版本让系统能够在故障与演进中拒绝过期状态。
真正掌握 Kafka,不是记住全部配置,而是能够在任何场景中追问:当前权威领导者是谁、可见边界在哪里、哪个状态已持久化、失败后由什么信息恢复、为了可用性牺牲了什么。这些问题构成了 Kafka 原理和生产实践之间最稳定的桥梁。
官方参考资料
- Kafka 4.3 Security Overview:kafka.apache.org/43/security...
- Kafka 4.3 Operations:kafka.apache.org/43/operatio...
- Kafka 4.3 Upgrade Guide:kafka.apache.org/43/getting-...
- Kafka 4.3 Compatibility:kafka.apache.org/43/getting-...