Kafka
Java 高级 · 消息中间件
Kafka 是高吞吐分布式提交日志系统,是大数据与事件驱动架构的 backbone。它的"快"和"可靠"不是靠堆机器,而是几个精妙设计:分区日志 + 顺序写盘 + 零拷贝发送 + ISR 副本 + 批量与压缩。本文把分区模型、副本同步(HW/LEO)、零拷贝、消费位移与再均衡、幂等/事务讲透。
一、核心模型:Topic / Partition / Offset / Consumer Group
- Topic:逻辑主题。物理上拆成多个 Partition(有序、不可变、只追加的日志)。
- Offset:分区内每条消息的单调递增位移,Consumer 靠 Offset 记录消费进度。
- Partition 有序性:同一 Partition 内消息严格有序;同 key 的消息按
hash(key) % 分区数落入同一分区,从而保证"同 key 有序"(跨分区不保证有序)。 - Consumer Group:同组消费者瓜分分区(一个分区只被组内一个消费者消费),实现并行;不同组互不干扰(各自维护 Offset)。
二、存储与零拷贝:为什么 Kafka 能扛百万 TPS
- 顺序写盘:消息只追加(append)到 Partition 的日志段(
log segment),顺序 IO 远快于随机写。每个 Partition 是磁盘上多个.log+.index文件。 - 页缓存(PageCache):Broker 写消息先进 OS 页缓存,由后台线程刷盘;读也从页缓存命中,几乎不碰磁盘。
- 零拷贝(sendfile):Consumer 拉消息时,Broker 用
FileChannel.transferTo(底层sendfile系统调用)把数据从页缓存直接拷到网卡,省去"内核→用户态→内核 socket"两次拷贝和一次 CPU 拷贝,这是超高吞吐的关键。
// 零拷贝示意(内核态完成, 不经过用户缓冲)
// fileChannel.transferTo(position, count, socketChannel);
// Kafka 消费者拉取: 数据从磁盘文件经 sendfile 直达网卡三、副本与 ISR:可靠性与可用的平衡
每个 Partition 有 1 个 Leader(处理读写)和 N 个 Follower(从 Leader 拉取同步)。核心是 ISR(In-Sync Replicas,同步副本集合):
- LEO(Log End Offset):每个副本当前日志末尾。Follower 不断
fetchLeader 的日志,追上 LEO。 - HW(High Watermark,高水位):ISR 中最小的 LEO——只有"所有 ISR 副本都写到的位置"才对 Consumer 可见。HW 保证"已提交的消息不会因 Leader 切换而丢失"。
acks 语义:
acks=0:发了就当成功,可能丢(最快)。acks=1:Leader 写入即成功,Leader 宕机且未同步则丢。acks=all:需 ISR 全部确认才成功,可靠性最高(吞吐略低)。配合min.insync.replicas=2保证"至少 2 副本落盘"。
Leader 选举与 Controller:集群中一个 Broker 是 Controller,负责分区 Leader 选举、副本重分配。Leader 宕机时,Controller 从 ISR 中选新 Leader(若 unclean.leader.election.enable=false,禁止从非 ISR 选,避免丢已提交消息)。
TIP
ISR 的设计精髓:用"ISR 这个动态集合"把"强一致(写入需多数派确认)"和"高可用(副本掉线只是移出 ISR,不影响服务)"统一。Follower 卡顿/网络抖动会被踢出 ISR,Leader 继续服务,等它追上再拉回——这就是 Kafka "既可靠又不脆"的根本。
四、Producer:批量、压缩、幂等
- 批量(batching):Producer 不是来一条发一条,而是按
linger.ms(攒批等待)和batch.size攒成一批再发,大幅减少网络往返。代价是少量延迟换吞吐。 - 压缩:
compression.type(lz4/snappy/zstd)在批量基础上压缩,省带宽。 - 幂等 Producer(enable.idempotence=true):Producer 带
PID+ 序列号,Broker 去重,保证"同分区同一消息不重复写入"(解决网络重试导致的重复)。底层是"每个分区维护 (PID, sequence) 去重"。 - 事务(transactional.id):跨分区原子写,配合
enable.idempotence,实现"消费-处理-生产"的 exactly-once(读位移和写消息在同一个事务)。read_committed消费者只看到已提交事务的消息。
五、消费位移与再均衡
5.1 提交策略
props.put("enable.auto.commit", "false"); // 关闭自动提交, 手动控制
consumer.subscribe(List.of("order"));
while (true) {
ConsumerRecords<String,String> rs = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String,String> r : rs) { handle(r); }
consumer.commitSync(); // 处理完再提交位移 → 不丢
}- 自动提交(enable.auto.commit=true):后台定时提交,可能"消息还没处理完就提交 offset"→ 宕机丢消息。高可靠务必手动提交。
- 最少一次(at-least-once):先处理再提交,可能重复(宕机在处理后提交前)→ 消费端需幂等。
- 最多一次(at-most-once):先提交再处理,可能丢。
5.2 再均衡(Rebalance)
当消费者加入/退出、订阅分区数变化、Group Coordinator 触发,会发生 Rebalance:组内所有消费者暂停消费,重新分配分区,再继续。问题:
- Rebalance 期间消费暂停(STW),频繁 Rebalance 严重拖吞吐。
- 单条消费阻塞会触发
max.poll.interval.ms超时 → 被踢出 → Rebalance 漂移。 - 新消费组协议(Cooperative Sticky)支持"增量再均衡"(只动变化的分区),减少停顿。
WARNING
- auto.commit=true 丢消息:消费逻辑未处理完就自动提交 offset,宕机后该消息"已消费"再不会被处理。高可靠场景务必手动提交。
- 分区数定小了难扩容:分区数决定消费并行度上限,初期评估不足后期调整成本大(key 哈希会变,需谨慎)。
- key 设计不当破坏顺序:同一业务实体的消息必须用相同 key,否则跨分区乱序。
- 消费者处理慢拖垮整个组:单条阻塞触发 rebalance,分区在消费者间频繁漂移,吞吐骤降。
六、生产落地
acks=all+ 幂等/事务 Producer +replication.factor=3+min.insync.replicas=2防丢。- 分区数按"峰值吞吐 / 单分区消费能力"评估,预留 2-3 倍余量。
- 消费端幂等(业务唯一键/offset 幂等表),应对 at-least-once 重复。
- 监控 Lag(消费滞后):持续增长是处理能力瓶颈的早期信号。
📚 官方文档与参考资料
- Kafka 官方文档:https://kafka.apache.org/documentation/
- Kafka 快速入门:https://kafka.apache.org/quickstart
- Kafka 设计(复制):https://kafka.apache.org/documentation/#replication
✏️ 练习与自测
- 解释 ISR 机制如何兼顾可靠性与可用,acks=all 与 acks=1 在 Leader 宕机时的差异。说明 HW 与 LEO 的作用。
- 设计订单 Topic:如何保证同一用户的订单消息有序且可水平扩展(key 选取 + 分区数)。
- 消费组 Rebalance 的触发条件有哪些?如何用 Cooperative Sticky 协议减少其影响?
- 用幂等 Producer + 手动提交,说明如何做到"不丢不重"(at-least-once + 消费幂等)。
本节小结
Kafka 的威力来自"分区日志(有序+并行)+ 顺序写盘 + 零拷贝 sendfile + ISR 副本(HW/LEO)"。Producer 靠批处理+压缩+幂等提吞吐保不重;Consumer 靠 Offset + 手动提交 + 幂等保可靠。理解分区、ISR、acks、Lag,你就掌握了可靠性与吞吐的平衡点。落地口诀:分区定并行、acks 保可靠、手动提交防丢、Lag 看健康。