Kafka 如何保证消息不丢?acks 与 ISR 到底什么关系?
"Kafka 不丢消息"不是靠一个开关,而是生产端、Broker、消费端三段各自配置共同保证的。 绝大多数丢消息事故,都是某一段用了默认配置却没意识到默认值很弱。
1. 先分清"三个位置"上的丢失
Producer ──① 发送失败/重试耗尽──> Broker(Leader)──② 未刷盘/未同步副本──> OS PageCache / Disk
└── ISR 副本
Consumer ──③ 先提交 offset 后处理业务(处理失败但 offset 已前移)──> 丢失
2. 生产端:acks 是关键
| acks | 语义 | 丢失风险 |
|---|---|---|
0 | 发出去就认为成功(不等 Broker 响应) | 极高(网络抖动就丢) |
1 | Leader 写入本地日志即返回 | Leader 挂掉、未同步到副本时丢 |
all(-1) | 所有 ISR 副本写入才返回 | 最低(配合 min.insync.replicas) |
acks=all
retries=2147483647 # 用很大的值 + 幂等,让重试尽可能发生
enable.idempotence=true # 幂等:避免重试导致重复
max.in.flight.requests.per.connection=5 # 幂等开启后 5 以内可保序
delivery.timeout.ms=120000 # 总超时(含重试);不要设得太小
三个要点:
acks=all只保证"ISR 都收到",不等于"落盘"——Broker 默认写 PageCache 就返回;- 幂等 +
acks=all是生产端最佳组合:既不丢(重试)又不重复(幂等),且分区内保序; retries与delivery.timeout.ms要配合:重试次数再多,总超时到点也会失败。
3. Broker:ISR 与 min.insync.replicas
ISR(In-Sync Replicas) = 与 Leader 保持同步的副本集合(含 Leader)。
acks=all 的语义是"ISR 内所有副本都写成功",所以ISR 缩小到只剩 Leader 时,acks=all 退化成 acks=1。
# 主题级
min.insync.replicas=2 # 至少 2 个副本才算写成功
replication.factor=3 # 副本数 3,容忍 1 个挂掉
unclean.leader.election.enable=false # 不允许非 ISR 副本当 Leader(否则会丢数据)
unclean.leader.election 是最容易被忽略的一条:设为 true 时,ISR 全挂后允许落后副本当 Leader,
可用性提升但会静默丢数据。金融类场景必须为 false。
刷盘策略:
# 默认:flush 由 OS 决定(PageCache),性能好但掉电可能丢
log.flush.interval.messages=10000
log.flush.interval.ms=1000
其实靠副本冗余比靠 fsync 更划算:3 副本 + acks=all 已能容忍单机故障;
频繁 fsync 会显著降低吞吐。真要求"断电也不丢",再叠加 log.flush 配置。
4. 消费端:先处理还是先提交
这是业务侧最常写错的地方:
// 错误:先提交 offset,再处理业务
consumer.commitSync();
process(record); // 处理失败 → 消息永久丢失
// 正确:处理成功后再提交
process(record);
consumer.commitSync();
更稳的模式是幂等消费 + 至少一次:处理可能重复,但业务用唯一键去重, 这样"重复"可接受、"丢失"不可接受。
| 提交方式 | 风险 |
|---|---|
自动提交(enable.auto.commit=true) | 提交时机不可控,可能"未处理就提交" |
| 手动同步提交 | 安全但阻塞,吞吐略低 |
| 手动异步提交 | 吞吐高,但失败要能回滚/重试 |
5. 一张"不丢配置"清单
| 段 | 配置 | 建议值 |
|---|---|---|
| Producer | acks | all |
| Producer | enable.idempotence | true |
| Producer | retries | 很大(配合幂等) |
| Broker | replication.factor | 3 |
| Broker | min.insync.replicas | 2 |
| Broker | unclean.leader.election.enable | false |
| Broker | log.flush.* | 按可靠性要求取舍 |
| Consumer | enable.auto.commit | false |
| Consumer | 提交时机 | 业务处理成功后 |
6. 还有两类"看起来不丢其实丢"的情况
- 消费位点被重置:
auto.offset.reset=earliest/latest只在"无位点"时生效, 但如果消费者组被误删、位点过期(offsets.retention.minutes),重连后可能从最新开始 —— 中间消息被跳过; - 重平衡期间:分区被收回后,未提交位点的消息会被重新投递(重复),
而如果用了"处理完才提交"却在新消费者上处理失败,就可能反复卡住。用
max.poll.interval.ms与处理时长匹配,避免"处理太久被踢出组"。
小结
Kafka 的可靠性是端到端配置问题,不是单点开关:
- 生产端:
acks=all+ 幂等 + 足够重试; - Broker:3 副本 +
min.insync.replicas=2+ 禁止非 ISR 选主; - 消费端:处理成功再提交 + 幂等去重。
三处任一为默认值,都可能出现"配置了 acks=all 还是丢"的事故。