Skip to content

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 拷贝,这是超高吞吐的关键。
java
// 零拷贝示意(内核态完成, 不经过用户缓冲)
// fileChannel.transferTo(position, count, socketChannel);
// Kafka 消费者拉取: 数据从磁盘文件经 sendfile 直达网卡

三、副本与 ISR:可靠性与可用的平衡 ​

每个 Partition 有 1 个 Leader(处理读写)和 N 个 Follower(从 Leader 拉取同步)。核心是 ISR(In-Sync Replicas,同步副本集合):

  • LEO(Log End Offset):每个副本当前日志末尾。Follower 不断 fetch Leader 的日志,追上 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 提交策略 ​

java
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(消费滞后):持续增长是处理能力瓶颈的早期信号。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 解释 ISR 机制如何兼顾可靠性与可用,acks=all 与 acks=1 在 Leader 宕机时的差异。说明 HW 与 LEO 的作用。
  2. 设计订单 Topic:如何保证同一用户的订单消息有序且可水平扩展(key 选取 + 分区数)。
  3. 消费组 Rebalance 的触发条件有哪些?如何用 Cooperative Sticky 协议减少其影响?
  4. 用幂等 Producer + 手动提交,说明如何做到"不丢不重"(at-least-once + 消费幂等)。

本节小结

Kafka 的威力来自"分区日志(有序+并行)+ 顺序写盘 + 零拷贝 sendfile + ISR 副本(HW/LEO)"。Producer 靠批处理+压缩+幂等提吞吐保不重;Consumer 靠 Offset + 手动提交 + 幂等保可靠。理解分区、ISR、acks、Lag,你就掌握了可靠性与吞吐的平衡点。落地口诀:分区定并行、acks 保可靠、手动提交防丢、Lag 看健康。