Producer 端:应用侧通过 Producer API 将业务消息发往指定 Topic。
Broker 集群:Leader Broker 接收消息并写入本地日志(Log),然后将消息复制到 Follower 副本,根据配置返回 ACK。
ZooKeeper/KRaft:负责元数据管理、Leader 选举、Topic/Partition 管理,保证集群健康和一致性。
Consumer 端:应用侧通过 Consumer API 向 Group Coordinator 订阅 Topic 并分配 Partition,定期 Poll Fetch 拉取消息,处理并异步或同步提交消费位移(Offset)。
下图为简化的端到端消息链路示意:
应用 → Producer API → Serializer → Partitioner → Buffer → NetworkClient
→ Broker(Leader) → Log Append → ReplicaManager → ISR 同步
→ ACK 回传 → Producer Callback
↓
Offset Commit
↓
应用 ← Consumer API ← Fetch → Fetcher Threads ← Broker(Leader) ← Log Read
↑ ↑
└── Coordinator ← Group Coordinator ─────────┘
Producer API 调用
应用通过KafkaProducer.send()
接口提交
。
序列化(Serializer)
Key/Value 通过用户配置的Serializer
(如 StringSerializer、Avro/Schemaregistry)编码为字节数组。
分区(Partitioner)
若用户指定partition
,则直接发送到该分区;否则根据key
哈希或自定义算法选择分区,保证同一 Key 的消息有序落到同一 Partition。
Batch 与缓冲(Buffer & Batching)
Producer 维护内存缓冲区,将同一 Partition 的消息合并成 batch,提升 IO 和网络吞吐。
当 batch 达到batch.size
或等待时间超过linger.ms
时触发发送。
NetworkClient 发送
使用异步 IO(Netty 或 Java NIO),向 Partition Leader 的 Broker 发起ProduceRequest
。
可配置acks=0/1/all
决定发送可靠性:
0
:不等待 ACK
1
:等待 Leader 写入本地 Log 即 ACK
all
:等待 ISR(In-Sync Replicas)中所有副本确认
幂等 & 事务
开启enable.idempotence=true
后,Producer 会为每个 partition 分配序列号,Broker 端检测并排重,保证消息不重复。
若使用事务(transactional.id
),多分区/多主题的原子写入由 Transaction Coordinator 管理。
接收请求 & 请求调度
Broker 的SocketServer
接收网络请求,分发给对应的 Request Handler 线程。
日志追加(Log Append)
ReplicaManager
定位到对应 Topic-Partition 的日志文件段(Segment),将消息追加至磁盘(pagecache+fsync)。
副本复制(Replication)
Leader 将消息推送到所有 ISR 中的 Followers。
Follower 收到后写本地 Log 并向 Leader 发送 ACK。
高可用与故障切换
ZooKeeper/KRaft 监控 ISR 列表:若某 Follower 长时间未响应,则移出 ISR;若 Leader 宕机,Controller 选举新的 Leader。
ACK 返回
当满足acks
策略后,Leader 将ProduceResponse
返回给 Producer。
内部组件
Controller:管理 Topic、Partition 的元数据、Leader 变更、配额控制等。
QuotaManager:流量限流保障多租户公平。
LogCleaner:后台合并压缩删除标记消息。
订阅与分组(Group Coordinator)
Consumer 启动时向Group Coordinator(某 Broker)发送 JoinGroup 请求,注册 GroupID 和订阅的 Topic 列表。
Coordinator 收集全体成员,执行 Partition Assignment(Range/Sticky/RoundRobin),生成分配方案。
心跳 & Session 维护
Consumer 定期发送 Heartbeat,若超过session.timeout.ms
未响应,则认为掉线,Group Coordinator 触发 Rebalance。
拉取消息(Fetch)
Consumer 根据分配到的 Partition,在 Poll() 时向对应 Partition 的 Leader Broker 发起FetchRequest
,指定fetch.offset
、fetch.max.bytes
等参数。
Broker 从日志文件中读取消息、封装FetchResponse
返回。
消息处理 & Offset 提交
Consumer 收到批量消息后,按序处理并更新内存中当前偏移量。
可选择自动提交(enable.auto.commit=true,间隔auto.commit.interval.ms
)或手动提交(通过commitSync()
/commitAsync()
写入__consumer_offsets
内置 Topic)。
幂等消费 & 事务消费
结合事务,Consumer 可在同一事务内拉取、处理并提交偏移,保证精确一次(EOS)端到端语义。
组件 | 上游 | 下游 | 作用 |
---|---|---|---|
应用 | — | Producer API | 业务侧产生消息 |
Producer API | 应用 | Serializer → Partitioner | 序列化、分区、Batch 缓冲 |
NetworkClient | Buffer/Batches | Broker (Leader) | 发送 ProduceRequest |
Broker (Leader) | Producer | ReplicaManager → Followers | 日志追加、复制、ACK |
Controller | ZooKeeper/KRaft | Broker 集群 | 元数据管理、Leader 选举 |
ZooKeeper/KRaft | Broker 注册 | Controller | 集群元数据、一致性管理 |
Group Coordinator | Consumer JoinGroup/Heartbeat | 分区分配 → 响应 Consumer | 管理消费组成员、分配 Partition |
Consumer Fetcher | Fetch 请求 | Broker → FetchResponse | 拉取消息 |
Offset Manager | commitOffset 请求 | __consumer_offsets Topic Broker | 保存消费位移 |
吞吐 vs 延迟
调整batch.size
、linger.ms
、fetch.min.bytes
等平衡吞吐与响应时延。
副本因子与 ISR
副本因子(replication.factor)≥3,保证单点故障可恢复,ISR 机制确保数据可见性。
资源隔离
使用quota
限流、隔离不同业务流量。
监控与报警
关注Under-Replicated Partitions、Consumer Lag、Throughput、Latency等关键指标。
从 Producer 端的序列化、分区与批量发送,到 Broker 端的日志追加、ReplicaManager 复制,再到 Consumer 端的分组协调、Fetch 拉取与 Offset 提交,Kafka 通过多层异步解耦、分布式复制和 Group 协调机制,高效可靠地串联起完整的消息流转过程。架构师在设计时,可针对业务场景调整批量参数、可靠性级别、Topic 分区及副本因子,并结合监控告警,确保系统稳定与性能最优。
Topic 分区(Partition)
每个 Topic 被切分为若干个 Partition。
Partition 是 Kafka 中并行度的基本单位:不同 Partition 可以分布在不同 Broker 上,实现水平扩展。
副本因子(Replication Factor)
指每个 Partition 的副本(Replica)数目。
副本包括 1 个 Leader 和 N-1
个 Follower(跟随者)。
Broker 集群
一组运行 Kafka 服务的节点。
所有 Partition 的 Leader 与 Follower 分散在不同 Broker 上。
ISR(In-Sync Replicas)
ISR 列表记录了与 Leader 保持同步的 Follower 副本。
只有 ISR 内的副本,才能在 Leader 接受写入并对外返回 ACK 时被视为真正“同步”完成。
假设我们有一个 Topic,配置了:
Partition 数:4
副本因子:3
Broker 数:5
则 Kafka 会根据分配算法(默认的轮询+偏移策略),把这 4×3=12 个副本分散到 5 台 Broker 上,例如:
Partition | Leader Broker | Follower 1 | Follower 2 |
---|---|---|---|
0 | Broker-1 | Broker-3 | Broker-5 |
1 | Broker-2 | Broker-4 | Broker-1 |
2 | Broker-3 | Broker-5 | Broker-2 |
3 | Broker-4 | Broker-1 | Broker-3 |
Leader 处理读写请求;Followers 被动复制 Leader 数据。
通过这样的分布,各 Broker 承担不同 Partition 的 Leader 和 Follower,尽量均衡负载。
写入请求(Produce)
Producer 向 Partition 的 Leader 发送 ProduceRequest。
Leader 将消息追加到本地日志(Log)。
副本同步
Leader 并行地将同批消息推送给所有 ISR 内的 Follower。
Follower 收到后写入本地日志并向 Leader 回送 ACK。
ACK 策略(acks
配置)
acks=0
:Producer 不等待 ACK,即“火并”式写入,最低延迟、最高丢消息风险。
acks=1
:等待 Leader 写入后立即 ACK,若 Leader 挂掉、Follower 未完全同步,则可能丢数据。
acks=all
(或 -1
):等待 ISR 中所有副本写入后才 ACK,是最强一致性保证。
最小同步副本数(min.insync.replicas
)
配置可写入的最小 ISR 数量,当 ISR 小于该值时,所有 acks=all
的写操作会被拒绝,以保证足够的复制度。
容错点:
如果某个 Follower 异常或网络抖动导致落后过多,Controller 会将其从 ISR 中剔除;但只要 ISR 中仍保留至少 min.insync.replicas
个副本,Leader 仍能继续接收写入。
当 Leader 宕机时,Controller(由 ZooKeeper 或 KRaft 协调)会在 ISR 中选举新的 Leader,保证该 Partition 可继续读写。
故障检测:Controller 周期性通过心跳或 ZooKeeper Session 感知 Broker 状态。
Leader 选举:若 Leader 宕机,Controller 在 ISR 列表中选出下一个 Follower 作为新的 Leader,并更新元数据广播给 Producers/Consumers。
默认配置下,只有 ISR 内的副本才可被选为 Leader,避免“脏”数据被当作最新数据(保证一致性)。
若为了可用性,也可开启 unclean.leader.election.enable=true
,允许非 ISR 副本提升为 Leader,快速恢复可用性,但可能丢失最近的一批消息。
Rack-Aware 分配
配合 broker.rack
配置,确保同一 Partition 的副本分布在不同机架或可用区(AZ)。
机架故障或同机架网络抖动时,仍有跨机架的副本可用。
MirrorMaker 双活 / 异地 DR
使用 Kafka MirrorMaker、Confluent Replicator 等工具,将重要 Topic 实时复制到异地集群。
避免单个数据中心全丢的极端灾难。
Log Retention(retention.ms
/ retention.bytes
)与 Log Compaction
根据时间/大小策略定期删除过期数据;对关键业务可启用消息紧凑,仅保留最新 Key,优化存储。
在恢复场景中,新的集群可从镜像或快照中重新加载数据。
参数 | 作用 | 建议值 / 实践 |
---|---|---|
replication.factor |
每个 Partition 副本数 | ≥3(跨机架/可用区),保证至少 2 台可用时可写入 |
acks=all |
最强一致性 ACK 策略 | 关键业务一律使用 |
min.insync.replicas |
允许写入的最小同步副本数 | replication.factor - 1 或 2 |
unclean.leader.election |
是否允许非 ISR 副本当 Leader | 默认 false (保证一致性);对可用性要求极高且可容忍丢数据可 true |
broker.rack |
指定 Broker 所属机架/可用区 | 明确设置并配合分区分配策略,保证副本跨机架 |
MirrorMaker | 跨集群实时镜像 | 关键 Topic 建议异地双活 |
分区(Partition) 提供并行性;副本因子(Replication Factor) 与 ISR 机制确保数据在多 Broker 间冗余。
Leader/Follower 架构配合 acks 与 min.insync.replicas 实现可调的容错保障。
借助 Rack-Aware、跨数据中心镜像,以及 Controller 的快速故障切换,Kafka 能在单机、单机架乃至整个数据中心级别的故障中,保持消息系统的高可用与可靠性。
语义 | 含义 | 典型场景 |
---|---|---|
At-most-once | 消费可能丢失,但绝不重复。消费成功前即提交 Offset。 | 对丢失可容忍、重复不可接受的场景 |
At-least-once | 消费可能重复,但绝不丢失。先处理消息,再提交 Offset(或失败重试)。 | 对重复可容忍、丢失不可接受的场景 |
Exactly-once | 既不丢失,也不重复(EOS)。需客户端+Broker+下游系统配合。 | 金融交易、计费、库存等强一致性场景 |
At-most-once
流程:poll()
→ 立即commitSync()
→ 再执行业务处理。
优点:重复消费不会发生。
缺点:在处理逻辑失败重试时,消息已提交,存在丢失风险。
At-least-once
流程:poll()
→ 业务处理 → commitSync()
/commitAsync()
。
优点:消费失败可重试,不会丢失消息。
缺点:若处理成功但提交 Offset 失败,则可能重复消费。
业务幂等:确保同一条消息处理多次,结果与一次相同。
示例:支付接口根据order_id
做“先查后写”或“INSERT … ON DUPLICATE KEY UPDATE”,避免重复账务记录。
优点:无需严格控制 Offset,简单易用。
缺点:需要下游系统或数据库支持幂等操作。
做法:在数据库中维护一张“已处理消息 ID”(如消息 Key 或唯一流水号)表:
Consumer 取到消息后,先在 Dedup 表做INSERT
,如果已存在则跳过处理;
新插入则执行业务逻辑并提交 Offset。
事务保证:可将“写 Dedup 表 + 业务处理 + 提交 Offset”放在同一分布式事务(见下文),或采用两阶段提交。
Kafka 原生支持的 EOS 流程:
Consumer 以事务消费者模式(isolation.level=read_committed
)读取事务化 Producer 发布的消息;
Consumer 处理后,使用Transactional Producer向下游 Topic/系统写入结果,并在同一事务中提交自己的消费 Offset到内部 Topic __consumer_offsets
;
Broker 保证要么整个事务(消息生产 + Offset 提交)成功,要么全部回滚,从而实现端到端 Exactly-Once。
配置:enable.idempotence=true
、transactional.id
、transaction.timeout.ms
等。
XA 两阶段提交
依赖外部事务协调器(如 Atomikos、Bitronix),将 Kafka Producer、数据库或其他资源纳入同一全局事务。
优点:保证跨系统一致性。
缺点:性能开销大、运维复杂,易出现僵死事务。
Outbox-Inbox 模式
Outbox(应用数据库):业务服务写入业务表的同时,写入“待发消息”表;
Relay/Bridge:后台定时扫描 Outbox,将消息 Publish 到 Kafka,再标记已发送;
Consumer Inbox:下游服务消费消息前,先写自己的 Inbox 表去重,再执行业务;
优点:避免 XA,可靠性高。
缺点:增加存储 & 扫描复杂度,延迟略高。
手动提交
推荐使用 commitSync()
,在处理完批次后立即提交;
在commitSync()
失败时,应用可捕获异常并重试,最大化保证 Offset 提交成功。
针对异常的补偿/消息重试
在处理逻辑中区分可重试异常与不可重试异常:
可重试:抛出异常,让 Consumer 重试(或通过 DLQ 机制)
不可重试:记录日志/告警,提交 Offset 跳过,避免阻塞队列。
控制批次大小
max.poll.records
限制每次处理的消息条数,避免单次批量过大导致处理超时和 Offset 晚提交。
Kafka Streams 内部集成了 EOS:对 State Store 的更新、下游 Topic 写入及 Offset 提交,都在同一 Kafka 事务中完成。
特点:
无需额外编码两段事务;
自动恢复与重平衡时,确保状态一致;
仅需在 StreamsConfig
中设置 PROCESSING_GUARANTEE = EXACTLY_ONCE
。
场景类型 | 方案 | 优劣势 |
---|---|---|
日志收集 | At-least-once + 幂等写入 ElasticSearch | 简单,ES 支持幂等;可能出现少量重复日志可被接受 |
订单支付 | Kafka 原生事务(EOS) | 端到端 Exactly-Once;配置相对复杂,需要 transactional.id 等 |
库存扣减 | Outbox-Inbox 模式 + 消息 ID 去重 | 解耦,适合微服务;需要额外 DB 表与桥接服务维护 |
实时指标聚合 | Kafka Streams with EXACTLY_ONCE | 语义简洁,状态存储与 Offset 一致;需使用 Streams API |
不漏消费:核心在于“业务处理后再提交 Offset”(At-least-once),并针对失败提供重试或 DLQ。
不重复消费:依赖幂等业务逻辑、去重存储或事务消费。
Exactly-Once 最强保障:可选 Kafka 原生事务或 Kafka Streams EOS,也可借助 Outbox-Inbox 两阶段模式。
poll()
方法内部 Offset 管理逻辑poll()
与 Offset 提交的总体流程调用 poll()
拉取新消息(Fetch
)并返回给应用层,同时更新 Consumer 内部维护的“已取到但未提交”记录(records
)。
poll()
还会触发与 Group Coordinator 的心跳(Heartbeat
)以及触发自动提交逻辑(若开启)。
Offset 确定
对于每个 Partition,Consumer 记录下本次返回的最后一条消息的 Offset(例如最后一条消息的 Offset 为 n
,则“可提交 Offset”记为 n+1
,意味着下次从 n+1
开始消费)。
提交策略
自动提交(enable.auto.commit=true
):
在 poll()
返回后,如果距离上次自动提交已过 auto.commit.interval.ms
,Consumer 背景线程会向 Broker 发起 OffsetCommitRequest
,提交所有 Partition 的“可提交 Offset”。
手动提交(enable.auto.commit=false
):
应用需显式调用 consumer.commitSync()
或 consumer.commitAsync()
,提交当前所有 Partition 的“可提交 Offset”。
假设对单个 Partition 的 Topic,Consumer 配置为:
enable.auto.commit=true
auto.commit.interval.ms=5000
时间点 | 操作 | 内部 Offset 状态(next offset) | 提交行为 |
---|---|---|---|
T0 | consumer.poll(1000) → 得到消息 [0,1,2] | 3 | 距上次提交 >5s?是 → 提交 offset=3 |
T1 = T0+1s | consumer.poll(1000) → 得到消息 [3,4] | 5 | 距上次提交 >5s?否 → 不提交 |
T2 = T0+5s | consumer.poll(1000) → 得到消息 [5] | 6 | 距上次提交 ≥5s?是 → 提交 offset=6 |
T3 | … | … | … |
说明:
每次 poll()
返回后,自动提交线程检测上次提交时间,若超过 auto.commit.interval.ms
,就会把“当前每个 Partition 最后取到的 Offset +1”一并提交给 Broker。
如果应用在处理消息期间崩溃,未处理消息自 T_lastCommit 到 T_crash 间的消息会被视为已消费(Offset 已提交),可能造成漏消费。
commitSync()
(同步阻塞)配置:
enable.auto.commit=false
代码:
while (true) {
ConsumerRecords rs = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord r : rs) {
process(r); // 业务处理
}
// 同步提交:确保提交成功才返回
consumer.commitSync();
}
时间点 | 操作 | 内部 Offset 状态 | 提交行为 |
---|---|---|---|
T0 | poll → 得到消息 [0,1,2] | 3 | 调用 commitSync() → 提交3 |
T1 | poll → 得到消息 [3,4] | 5 | 调用 commitSync() → 提交5 |
Crash | 在处理消息 [5] 期间应用崩溃 | 尚未提交 (next=6) | — |
Restart | 新实例从已提交的 offset=5 继续消费 | — | — |
说明:
如果应用在处理消息 5
时崩溃,由于未提交 Offset,下次启动会重新从 5
开始拉取(包含 5
),保证不漏消费。
若 commitSync()
本身出现网络异常,会抛出异常,应用可捕获并重试,最大化保证提交成功。
commitAsync()
(异步非阻塞)while (true) {
ConsumerRecords rs = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord r : rs) {
process(r);
}
// 异步提交:快速返回,不保证提交一定成功
consumer.commitAsync((offsets, exception) -> {
if (exception != null) {
log.error("Commit failed for offsets {}", offsets, exception);
}
});
}
时间点 | 操作 | 内部 Offset 状态 | 提交行为 |
---|---|---|---|
T0 | poll → 得到消息 [0,1,2] | 3 | 异步提交3 |
T1 | poll → 得到消息 [3,4] | 5 | 异步提交5 |
T2 | 异步回调:offset=3 提交失败 | — | 日志记录;无自动重试 |
Crash | 在处理消息 [5] 期间应用崩溃 | 尚未 commitOffset=6 | — |
Restart | 新实例从已提交的 offset=5 继续消费 | — | — |
说明:
commitAsync()
性能更好但不保证成功,适合可容忍偶尔重复或丢失的场景;对于关键业务,通常在批次结束后再补一个 commitSync()
。
poll()
触发提交的关键点自动提交时机
由 poll()
驱动的后台线程周期性检查并提交。
提交内容
提交“每个 Partition 最后返回的消息 Offset +1”。
提交失败处理
commitSync()
会重试并抛出异常,应用可做补偿;
commitAsync()
通过回调告警,但不自动重试。
语义保证
At-least-once(手动提交、业务处理后再提交);
At-most-once(自动提交或提早提交 Offset);
Exactly-once(结合 Kafka 事务或业务幂等)。
poll()
既是消息拉取的入口,也触发了自动提交的检查。
自动提交简单、易用,但可能造成漏消费或重复消费;手动提交更灵活,可在业务处理后精确控制提交时机。
Kafka 在每个 Broker 上的每个 Topic-Partition 对应一个目录,目录下按**日志段(Segment)**组织文件。每个 Segment 包含三类核心文件:
/
└── -/
├── 00000000000000000000.log ← 消息实体
├── 00000000000000000000.index ← offset 索引
├── 00000000000000000000.timeindex ← 时间索引
├── 00000000000000000010.log
├── 00000000000000000010.index
├── 00000000000000000010.timeindex
└── ……
.log:存放按顺序追加的原始消息数据,每条消息格式化为 [Length][CRC][MessageSet]
,连续写入。
.index(OffsetIndex):稀疏索引文件,将 消息偏移量 映射到日志文件中的 字节位置。每条索引记录通常是 (relativeOffset, filePosition)
。
.timeindex(TimeIndex):将 消息时间戳 (CreateTime)映射到日志文件中对应的 字节位置,支持按时间查找。
一个 Segment 完全滚动(roll)后,新消息就写入下一个一系列文件名更大的新 Segment。
Producer 发来 ProduceRequest,Leader Broker 在内存页缓存(pagecache)中将消息批次以追加(append)的方式写入当前活跃的 .log
文件末尾。
写入细节:
消息先序列化并封装成 MessageSet
(可多条消息合批)。
写入操作是一次 顺序写,因此具备极高吞吐与操作系统文件缓存优势。
刷新策略:
根据 flush.ms
或 flush.messages
配置,Broker 会周期性或按条数调用 fsync()
,将 pagecache 中的数据落盘,保证持久化。
稀疏更新:默认每写入 index.interval.bytes
(例如 4096 bytes)后,就添加一条新的索引记录。
索引记录内容:
relativeOffset
= 当前消息全局 Offset − Segment 起始 Offset
position
= .log
文件中该消息的字节偏移
文件格式:固定大小条目,通常 12 字节(4 字节 relativeOffset + 8 字节 position),可通过 mmap 映射并快速二分查找。
类似稀疏机制,每写入足够字节后,插入 (timestamp, position)
记录。
支持按时间查找起始读取位置。
当 .log
文件大小超过 segment.bytes
或时间超过 segment.ms
,当前 Segment 被关闭并换新文件。新 Segment 的文件名以下一个 Offset 命名(向上对齐到 segment.bytes
边界)。
客户端调用 consumer.seek(topicPartition, offset)
或内部 poll()
触发 Fetch,Broker 端按流程:
在目录中定位到包含目标 Offset 的 Segment:
通过比较各 Segment 文件名(即起始 Offset),找到 baseOffset ≤ targetOffset < nextBaseOffset
的 Segment。
在 .index 中二分查找:
打开对应 Segment 的 .index
,执行二分查找或 mmap + binarySearch,找到小于等于目标 Offset 的最大索引记录 (relOffset_i, pos_i)
。
计算精确字节位置:startPos = pos_i + scanBackBytes
,从这里开始顺序扫描日志。
顺序扫描 .log:
从 startPos
读取消息批次,逐条解码 MessageSet
,直到遇到目标 Offset。
返回后续一批消息(受 fetch.min.bytes
、fetch.max.bytes
等参数限制)。
类似流程,只不过先在 .timeindex
查 desiredTimestamp
,得出初始 pos
,然后在 .log
顺序解码并筛选出第一个大于等于该时间戳的消息位置。
假设当前有一个 Segment 00000000000000000000.log
,起始 Offset=0,写入如下消息(简化表示):
全局 Offset | Message | 累计 Bytes | 索引记录 |
---|---|---|---|
0 | A | 100 | idx[0]: (0, 0) |
1 | B | 200 | |
2 | C | 3100 | idx[1]: (2, 3100) |
3 | D | 4200 |
当客户端请求 offset=2
:
在 .index
找到 (relOffset=2, pos=3100)
;
直接定位 .log
字节 3100 处开始读,顺序返回 C, D…
当客户端请求 timestamp=T_C
:
.timeindex
查到对应 pos=3100
;
同上顺序扫描,直到读到时间戳 ≥ T_C 的消息。
顺序写与 mmap 索引:
顺序追加最大化磁盘吞吐;
mmap 索引支持零拷贝查找,定位延迟极低。
段化管理:
小 Segment 带来更少的寻址开销;
旧 Segment 可根据保留策略异步删除/压缩,控制存储。
稀疏索引节省空间:
不为每条消息写索引,而是每 N 字节一条,减少索引文件大小;
索引精度与扫描成本在可控范围内平衡。
批量刷新与 fsync:
将批量写入与批量落盘拆分,提高吞吐同时保证低时长持久性窗口。
全局Offset Message 累计 Bytes 索引记录
全局 Offset | Message | 累计 Bytes | 索引记录 |
---|---|---|---|
0 | A | 100 | idx[0]: (0, 0) |
1 | B | 200 | |
2 | C | 3100 | idx[1]: (2, 3100) |
3 | D | 4200 |
当客户端请求 offset = 1
时,Kafka 会按以下步骤在该 Segment 的 .index
和 .log
文件中定位并读取消息:
假设只有一个活跃 Segment 文件 00000000000000000000.log
,它的起始全局 Offset 为 0,覆盖了 [0…3]
这四条消息。
.index
中二分查找最接近的索引条目我们的稀疏 Offset 索引 (.index
) 只有两条记录:
索引条目 | relativeOffset | filePosition |
---|---|---|
idx[0] | 0 | 0 |
idx[1] | 2 | 3100 |
目标 Offset:1
在索引里找出 最大的relativeOffset
≤ (targetOffset − baseOffset)
baseOffset = 0 (Segment 起始 Offset)
targetOffset − baseOffset = 1
比较索引条目:
idx[0].relativeOffset = 0 ≤ 1
idx[1].relativeOffset = 2 > 1
因此选中 idx[0],对应的 filePosition = 0
。
.log
文件的该字节位置开始顺序扫描打开 00000000000000000000.log
,定位到字节偏移 0
。
依次解码消息(按照 [Length][CRC][MessageSet]
格式):
第一条消息
读出 Offset = 0,跳过(不是目标)。
累计读取字节 ~100(“累计 Bytes”列),当前文件指针移到约 100。
第二条消息
读出 Offset = 1,正是我们要的消息 “B”。
停止扫描,返回这条消息以及随后可用的更多消息(若有)。
Broker 将从 byte position 0
顺序扫描到 Offset 1
处,并将消息 B 及之后的消息返回给客户端。
客户端实际收到的第一条数据即为 Offset 1
的消息。
虽然我们只保存了 Offset = 0 和 Offset = 2 两个索引点,但由于消息在 .log
中是顺序紧密排列的,从最近的索引点开始顺序扫描,只需跳过少量消息,就能快速定位任何 Offset。
对于高吞吐场景,Spark 保留索引间隔(如每 4 KB 一条),通常跳过的字节量非常小,扫描带来的延迟可忽略不计。
Kafka 通过 分段化的 Log 文件 + 稀疏的 Offset/Time 索引,同时结合 顺序写、mmap 二分查找、顺序读 的高效 IO 模型,实现了对海量消息的低延迟读写与灵活查找。写入时追加到 .log
并周期性更新索引,读取时通过索引快速定位,再顺序扫描,二者协同达到高性能和可靠性的平衡。