这篇是我整理 Kafka 笔记时写下的理解。Kafka 的可靠性经常被一句 “Exactly-Once” 概括,但真正落到工程里,它其实是分区、副本、生产者确认、消费者 offset、事务和下游幂等一起构成的语义边界。

我更愿意把问题拆成两句:

  1. Kafka 在 broker 和 topic 内部能保证什么?
  2. 端到端数据链路还需要业务系统自己补什么?

01分区决定吞吐,也决定顺序边界

Kafka 的 Topic 会被拆成多个 Partition。每个分区本质上是 append-only commit log,消息在分区内按 offset 递增写入。

这带来两个结论:

  1. Kafka 可以保证单分区内有序。
  2. Kafka 不能保证整个 Topic 全局有序。

生产者如果不指定 key,消息会被分散到不同分区以提高吞吐;如果指定 key,Kafka 会根据 key 的 hash 选择分区,让同一个 key 的消息更稳定地进入同一个分区。

因此订单事件、用户行为、支付状态这类需要局部有序的链路,通常要选择合适的 key,例如 order_id 或 user_id。否则后面再谈 Exactly-Once 也没有意义,因为顺序边界一开始就没定义清楚。

02副本与 ISR:可靠写入不是单点成功

Kafka 的每个分区可以有多个副本,其中一个是 Leader,其他是 Follower。读写都走 Leader,Follower 从 Leader 拉取数据保持同步。

ISR 是 in-sync replicas,也就是当前和 Leader 保持同步的一组副本。可靠性配置主要围绕三项:

配置作用
acks=0生产者不等 broker 确认,吞吐高但可能丢数据
acks=1Leader 写入成功即返回,Follower 未同步时仍可能丢
acks=allISR 内副本确认后才返回,可靠性最高
min.insync.replicas至少多少 ISR 副本可用才允许写入

如果业务要求可靠写入,常见组合是 acks=all + 合理的 min.insync.replicas + 禁止 unclean leader election。这样做牺牲可用性和吞吐,但能避免落后副本被选为新 Leader 后造成数据回退。

03Offset 提交:重复和丢失的分界线

消费者真正容易出问题的地方是 offset 提交时机。

如果先提交 offset,再处理业务,处理失败时这批消息不会再被消费,可能丢数据。

如果先处理业务,再提交 offset,提交前消费者宕机,消息会被重新消费,可能重复。

所以 Kafka consumer 默认的自动提交虽然简单,但在严肃链路里通常要关闭:

properties
enable.auto.commit=false

然后在业务处理完成后手动提交。即使这样,也只能把语义推进到 at-least-once:消息可能重复,但不应该丢。端到端 exactly-once 还需要下游写入幂等,或者和 Kafka 事务一起设计。

04Kafka 的 EOS 到底覆盖哪里

Kafka 的 Exactly-Once Semantics 主要解决两类问题:

  1. 幂等生产者:生产者重试不会在同一分区产生重复消息。
  2. 事务:一组跨分区写入和 offset 提交可以原子提交或回滚。

这对 Kafka Streams 这类“读 Kafka、处理、再写 Kafka”的场景很有价值。它能把消费 offset 和输出 topic 写入放进同一个事务里。

但如果你的链路是:

text
Kafka -> Spark / Flink / Java Consumer -> MySQL / Doris / Redis / 外部接口

Kafka 自己无法保证外部系统的 exactly-once。外部系统必须提供幂等写、唯一键、事务表、去重表或二阶段提交能力。

所以我理解的 EOS 边界是:Kafka 可以保证 Kafka 内部的幂等和事务,但业务链路要自己保证下游副作用的幂等。

05在 Olist 重放设计里的对应理解

Olist 湖仓项目里,我为历史事件重放设计了下面这条链路:

text
Olist CSV -> Kafka events -> Spark Structured Streaming -> windowed GMV

这里最关键的问题不是“Kafka 是否 Exactly-Once”,而是 replay 和 streaming 的比较口径是否一致。

为避免常见的错误,设计里把下面几件事写成了明确约束:

  1. replay 按 event_time + event_id 排序。
  2. 追加 watermark event 推进最后窗口输出。
  3. 用 replay_manifest 记录实际重放的 (order_id, order_item_id)。
  4. batch 侧只比较 manifest 中的 item。

当前本地环境没有实际拉起 Kafka broker,所以这条链路仍是待补的端到端验证项。我保留 replay_manifest、事件时间和窗口对账的设计,是为了在运行环境补齐后能明确回答“比较的是不是同一批数据”,而不是只拿一组漂亮的 GMV 数字当结论。

这也说明端到端一致性不能只靠 Kafka 配置,还要靠事件时间、窗口、水位线和比较口径一起闭环。

06小结

我现在会用四句话记 Kafka 可靠性:

  1. 分区决定吞吐和局部顺序。
  2. ISR、acks 和 min.insync.replicas 决定写入可靠性。
  3. offset 提交时机决定重复与丢失的风险。
  4. Exactly-Once 不是一句配置,而是 Kafka 内部事务加下游幂等的系统设计。

面试或项目复盘里,与其直接说“Kafka 支持 Exactly-Once”,不如先说清楚:在哪个边界内 exactly-once,出了 Kafka 之后又靠什么兜底。