先说结论
Kafka 消息不丢失不是一个参数可以保证的,需要同时保证生产者发送成功、Broker 副本可靠保存、消费者业务成功后再提交位点,并对最终失败建立重试、补偿、告警和故障演练。
消息可能在哪些环节丢失
flowchart LR
A["业务事务"] --> B["生产者发送"]
B --> C["Leader 写入"]
C --> D["Follower 同步"]
D --> E["消费者拉取"]
E --> F["业务处理"]
F --> G["提交位点"]
B -. "失败回调 / Outbox" .-> H["补偿"]
C -. "副本不足拒绝写" .-> H
F -. "业务失败 / 死信" .-> H
业务事务
↓
生产者缓冲与网络发送
↓
Kafka Leader 写入
↓
Follower 副本同步
↓
消费者拉取
↓
业务处理与位点提交
任何箭头都可能失败。讨论“不丢失”之前,必须定义消息从哪里产生、以哪个系统为事实源,以及什么状态算最终成功。
生产者端怎么保证
1. 正确设置确认级别
acks=0:不等待 Broker 确认,吞吐高但发送失败不容易发现。acks=1:Leader 写入后返回,Leader 突然故障且副本未同步时可能丢失。acks=all:等待 ISR 中满足条件的副本确认,可靠性最高。
生产环境的关键消息通常使用 acks=all,但它不是绝对保证,还要配合副本配置。
2. 开启幂等生产者
网络超时后,生产者无法确认 Broker 是否已经写入,通常需要重试。不开启幂等时,重试可能写入重复消息;幂等生产者使用 Producer ID 和序列号,让 Broker 对单个生产者会话的重复批次去重。
幂等生产者解决的是 Kafka 写入重试重复,不会自动保证数据库与 Kafka 双写一致,也不会让消费业务幂等。
3. 处理最终发送失败
异步 send 不代表消息一定成功。必须检查回调、Future 或框架提供的发送结果:
producer.send(record, (metadata, exception) -> {
if (exception != null) {
log.error("kafka send failed, key={}", record.key(), exception);
// 进入可靠补偿流程,而不是只打印日志
}
});
重试必须有上限。达到重试上限后,应落入本地事件表、补偿队列或告警系统,不能静默丢弃。
4. 避免进程退出丢失缓冲消息
生产者会在客户端批量缓冲。应用优雅关闭时应停止接收新请求、等待正在发送的消息完成,再关闭 Producer。强制 kill -9、容器超时退出或机器掉电仍需依赖业务事实源补偿。
Broker 端怎么保证
1. 设置合理副本数
关键 Topic 通常至少配置 3 个副本,并分布到不同 Broker 和故障域。单副本 Topic 即使使用 acks=all,也只有 Leader 一份数据。
2. 配置最小同步副本
min.insync.replicas 规定使用 acks=all 时至少需要多少个 ISR 副本保持同步。例如副本数为 3、最小同步副本为 2,可以容忍一个副本暂时离线;若同步副本不足,Broker 拒绝写入,而不是冒险接受只有一份的数据。
replication.factor = 3
min.insync.replicas = 2
producer acks = all
这组配置用一部分可用性换取数据安全。副本不足时业务会收到写入失败,所以生产者必须正确处理失败。
3. 禁止不安全 Leader 选举
落后太多的副本不在 ISR 中。若允许它在 Leader 故障时成为新 Leader,可能丢失尚未同步的数据。关键集群应避免以数据丢失换可用性,并通过容量和运维保证 ISR 稳定。
4. 监控副本健康
必须持续关注:
- Under Replicated Partitions。
- ISR 频繁收缩和扩张。
- Offline Partitions。
- Broker 磁盘使用率与请求延迟。
- Controller 和 Leader 频繁切换。
副本参数正确但磁盘长期接近满载,仍可能在故障时失去保护能力。
消费者端怎么保证
1. 业务成功后再提交位点
如果先提交位点再执行业务,消费者崩溃后 Kafka 会认为消息已经消费,业务结果却没有产生,形成真正的消息丢失。
推荐顺序:
拉取消息 → 处理业务 → 业务成功 → 提交位点
这样崩溃时可能重复消费,但不会因为位点超前而跳过消息。重复问题通过幂等解决。
2. 批量消费不能越过失败消息
一批包含 100 条消息,若第 50 条失败,却提交了第 100 条的位点,第 50 条不会再次收到。应按分区维护连续成功位点,失败时停止推进该分区或使用可控的重试、死信策略。
3. 正确处理再均衡
再均衡前应停止接收新任务、等待已领取消息在合理时间内完成,并提交已连续处理成功的位点。若业务线程与 poll 线程分离,需要记录每个分区的完成水位,不能简单提交最后拉取位置。
4. 下游失败不能直接吞掉
数据库超时、第三方接口失败或数据格式异常都要有明确策略:有限重试、延迟重试、死信队列、人工补偿。捕获异常后只记录日志并返回成功,相当于主动丢失消息。
数据库与 Kafka 双写怎么保证
业务先写数据库再发送 Kafka,会出现数据库成功但发送失败;先发 Kafka 再写数据库,则可能消息成功但业务事务失败。
常见解决方案是 Transactional Outbox:
同一个数据库事务:写业务表 + 写事件表
↓
后台投递任务读取事件表
↓
发送 Kafka,成功后标记已投递
数据库事务保证业务状态和事件同时存在。投递任务可以重试,消费者仍需幂等。也可以通过 Binlog CDC 把数据库变更转换为 Kafka 消息。
Kafka 事务适用什么场景
Kafka 事务可以将多分区写入和消费位点提交组成原子操作,适合 Kafka-to-Kafka 的处理链路。例如消费原 Topic、转换后写入目标 Topic,并同时提交源位点。
它不能直接把普通 MySQL 事务和 Kafka 写入变成一个全局事务。跨系统仍需要 Outbox、幂等和补偿。
消息保留与误操作风险
消息可能不是在发送链路丢失,而是消费前已经超过保留时间或 Topic 被误删。需要根据最大故障恢复时间设置保留周期和磁盘容量,并限制删除 Topic、缩短保留时间等高风险权限。
线上排查流程
- 根据业务消息 ID 查询生产端是否创建事件。
- 检查发送回调、重试和最终失败记录。
- 根据 Topic、Partition、Offset 确认 Broker 中是否存在。
- 检查副本、ISR 和 Leader 切换历史。
- 检查消费者位点是否已经越过目标消息。
- 检查业务日志、幂等表和死信队列。
- 明确是未生产、未写入、已过期、位点超前还是业务处理失败。
容易踩坑的地方
acks=all不等于绝对不丢,它依赖副本和 ISR 配置。- 自动提交位点不等于自动保证业务成功。
- Kafka 保存了消息,不代表数据库和第三方业务结果成功。
- 无限重试不是可靠性,会造成阻塞和重试风暴。
- 副本不能替代跨集群备份和业务补偿。
常见问题
追问 1:acks=all 后 Leader 宕机还会丢吗?
若副本数、ISR 和最小同步副本配置合理,已确认消息会存在于足够副本中,可由同步副本接任 Leader。但若允许不安全选举、磁盘同时损坏或跨故障域部署不合理,仍可能丢失。
追问 2:为什么通常选择至少一次而不是最多一次?
最多一次可能在位点先提交、业务后失败时丢消息。至少一次允许失败后重放,再通过业务幂等消除重复,通常更符合关键业务可靠性要求。
追问 3:发送超时意味着消息没进 Kafka 吗?
不一定。Broker 可能已经写入,只是响应丢失。生产者需要重试和幂等,不能根据超时直接认定失败或生成新的业务消息 ID。
追问 4:消费者处理成功但提交位点失败怎么办?
消息会再次消费,因此业务必须幂等。下次处理识别已完成后返回成功,再推进位点。
追问 5:如何证明系统真的不丢消息?
通过业务唯一 ID 建立生产、Kafka、消费和业务结果的全链路审计,定期对账,并演练 Broker 宕机、网络超时、消费者崩溃和数据库失败,而不是只检查配置。