Kafka 消息不丢失实战:生产者 ack + 副本 ISR + 消费者手动提交的完整配置
引言
一个安静得可怕的对账日:
风控月度对账发现:37 笔已支付成功的订单,下游库存服务完全没有收到"支付成功"消息。Kafka 集群日志没有任何 ERROR,Producer 端没有回调异常,Consumer 端没有报错——消息就是凭空消失了。
复盘结论更扎心:这不是一次故障,是三个"配置漏洞"的接力——生产者 acks=1 时恰好赶上分区 Leader 切换、Broker 没设 min.insync.replicas(ISR 缩到 1 也照收不误)、消费者开了自动提交(先提交 offset 后处理,业务处理失败消息就"确认掉了")。三层各漏一个,消息就没了。Kafka 从不承诺"你配错了我也不丢",它只承诺"你配对了我不丢"。
这篇文章按消息的生命周期拆三层保障:生产者端(acks + retries + 幂等)→ Broker 端(副本 + ISR + Unclean 选举)→ 消费者端(手动提交)。每一层讲三件事:消息怎么丢的、每条配置的语义是什么、为什么必须这么设。先放结论总表(完整可抄清单在第七章):
| 层 | 配置 | 设定值 | 防住什么 | 代价 |
|---|---|---|---|---|
| 生产者 | acks | all | Leader 挂了消息没同步到副本 | 延迟 +,吞吐 -30% |
| 生产者 | enable.idempotence | true | 重试导致的重复与乱序 | 几乎无 |
| 生产者 | retries | Integer.MAX_VALUE | 瞬时故障直接放弃发送 | 无(配合幂等) |
| Broker | replication.factor | ≥3 | 单机故障丢数据 | 成本 ×3 |
| Broker | min.insync.replicas | 2 | ISR 缩水还在写(裸奔防线) | ISR<2 时拒写 |
| Broker | unclean.leader.election | false | 数据落后的副本当选 Leader | 故障时可能不可用 |
| 消费者 | enable.auto.commit | false | 处理前就提交 offset | 需要幂等消费兜底 |
| 消费者 | 手动 commitSync | 业务成功后 | 处理失败可重投 | 至少一次 → 可能重复 |
一句话破题:Kafka 的消息不丢失是一条"责任链"——生产者保证"送达 Broker 的副本",Broker 保证"副本没同步齐不交付 Leader 位",消费者保证"处理完才确认"。三层任何一层松手,另两层救不回来。
一、先搞清楚:消息会在哪里丢
1.1 消息生命周期的丢失风险地图
producer ──①发送──▶ broker Leader ──②副本同步──▶ ISR Followers
│ │
│ ③Leader 挂/切换 │
▼ ▼
【丢失点 A】acks=1:数据只到 Leader,Leader 宕机未同步
【丢失点 B】min.insync.replicas 未设:ISR 缩到 1(只剩 Leader)仍接收写入
【丢失点 C】unclean.leader.election=true:落后的副本上位,已确认消息被"覆盖"
│
▼
consumer ──④拉取──▶ 业务处理 ──⑤提交 offset
│
【丢失点 D】自动提交:offset 先于业务处理提交
→ 处理失败/进程崩溃,消息被标记"已消费"
【丢失点 E】异步处理无确认:拉了没处理完就停
五个丢失点,A/B/C 在生产侧与 Broker 侧,D/E 在消费侧——"消息丢了"从来不是单点问题,是把三层防线拆开看的必然结果。本次事故 37 笔消息,就是 A(当时分区正在切换)+ B(ISR 缩到 1 没拒写)+ D(自动提交抢跑)的组合。
1.2 先建立"确认"的心智模型
消息系统里"不丢"的本质是确认链的完整传递:
Producer 收到 ack ⇔ 消息已被"足够多的 Broker"持久接收
Broker 交付写入 ⇔ ISR 副本数 ≥ min.insync.replicas
Consumer 确认 ⇔ 业务处理完成才提交 offset
任何一环的"确认"早于事实,窗口期内的故障 = 丢消息
记住这个模型,第二章到第四章的所有配置都只是在收紧每一环的"确认条件"。
二、生产者端:acks + 幂等,把"送达"定义清楚
2.1 acks 三档语义:你到底确认了什么
| acks | 确认语义 | 丢消息场景 | 延迟 | 适用 |
|---|---|---|---|---|
| 0 | 发出去就当成功(不等 Broker 确认) | 网络抖动/Broker 宕机全丢 | 最低 | 日志采集等可容忍场景 |
| 1 | Leader 写入(PageCache)即确认 | Leader 确认后宕机、副本未同步 → 消息消失 | 低 | 一般业务(有丢的风险) |
| all | ISR 全部副本确认才算 | 需配合 min.insync.replicas≥2 才真正可靠 | 较高 | 支付/订单等核心链路 |
注意一个容易忽略的细节:acks=1 的确认发生在 Leader 的 PageCache,连 Leader 自己的磁盘都没落——这不是 Bug,是设计(副本冗余代替单机 fsync,机制同《Kafka 为什么这么快》8.2)。acks=1 的可靠性完全押注在"Leader 活到副本追上"这个假设上,分区切换时假设就破了——事故里那 37 笔正是这样没的。
2.2 配置与"为什么"
# ── 生产者核心三件套 ──────────────────────────────
acks=all # ① 确认升级为"ISR 全部收到",防丢失点 A
enable.idempotence=true # ② 幂等生产者(3.0+ 默认值,老版本显式配)
retries=2147483647 # ③ 不轻易放弃;瞬时故障交给重试
# ── 配套参数 ─────────────────────────────────────
max.in.flight.requests.per.connection=5 # 幂等上限即 5,防乱序需 ≤5
delivery.timeout.ms=120000 # 重试总预算 2 分钟,超时才真失败
request.timeout.ms=30000
compression.type=zstd # 可靠性无损的流量优化(见《Kafka 为什么这么快》第五板斧)
为什么必须幂等(enable.idempotence):retries 重试会带来两个副作用——重复(Broker 其实收到了,ack 在网络中丢失,客户端重发)和乱序(batch1 失败重试时 batch2 先到)。幂等生产者用 <PID, 分区, 序列号> 三元组让 Broker 去重:每个分区记住最后的序列号,重发的旧序列号直接拒绝,并把"未确认的下一个 batch 阻塞住"保证顺序。开启后 retries 放到多大都不会造成重复/乱序——这就是"retries 必须配合幂等"的原因:裸重试是不安全的。
为什么 max.in.flight 必须 ≤5:幂等协议依赖 Broker 端按序列号判定,窗口超过 5 时 Broker 无法用有限缓存完成判定,Kafka 直接抛 OutOfOrderSequenceException 拒绝。默认值恰好是 5,改之前先想清楚。
为什么 delivery.timeout.ms 要显式设:retries=MAX 不等于无限重试,delivery.timeout.ms 是总闸(发送+重试全算)。2 分钟的预算对支付链路意味着:故障 2 分钟内自动恢复的消息一个不丢,超时的失败会被业务感知并进入补偿——比"静默重试一小时"更适合上游有对账的体系。
2.3 发送侧的代码纪律:回调必须处理
producer.send(record, (metadata, exception) -> {
if (exception != null) {
// ★ 不处理回调 = 配了 acks=all 也白配:
// 发送失败被静默吞掉,业务以为发出去了
log.error("send failed, topic={}, key={}", record.topic(), record.key(), exception);
compensateTable.insert(record); // 进补偿表,对账兜底
}
});
acks=all 管"网络到 Broker"的可靠,回调处理管"Broker 到业务"的可靠——两层衔接点就是这个回调。事故复盘里还有一笔隐蔽损失:11 笔消息其实发送失败了(超时),但代码没传回调,连补偿的机会都没有。
三、Broker 端:副本 + ISR,把"交付"的门槛立起来
3.1 ISR 是什么:同步副本集合
分区副本:Leader + 2 个 Follower
ISR(In-Sync Replicas)= 当前与 Leader 保持同步的副本集合
── 判定标准:replica.lag.time.max.ms(默认 30s)内追上 Leader 的最高水位 HW
── Follower 网络抖动/磁盘慢/GC → 移出 ISR
── 追上后 → 重新加入 ISR
ISR 正常:{1, 2, 3} → acks=all 的消息要 3 个副本都收到
ISR 缩水:{1} → 只剩 Leader,acks=all 退化成 acks=1 !
最后一行是整个 Broker 侧防线的核心认知:acks=all 的"all"指 ISR 成员,不是全部副本。ISR 缩到 1 时,acks=all 悄悄变成 acks=1,可靠性瞬间归零——min.insync.replicas 就是防这个退化的闸门。
3.2 Topic 级配置与"为什么"
# ── Topic 级(建 Topic 时指定,核心 Topic 必须)─────────
replication.factor=3 # ① 副本数:扛住单机故障的最低配置
min.insync.replicas=2 # ② ISR 最低水位:低于 2 拒绝写入
unclean.leader.election.enable=false # ③ 禁止非 ISR 副本当选 Leader
# ── Broker 级(全局兜底)──────────────────────────────
default.replication.factor=3
min.insync.replicas=2 # Topic 未显式配置时生效
unclean.leader.election.enable=false
auto.create.topics.enable=false # 防止裸配置的 Topic 被随手创建
为什么 replication.factor ≥3:与《PG 高可用实战》的仲裁逻辑同构——3 是"允许 1 台故障仍有多数"的最小奇数。2 副本时任何一台故障,min.insync.replicas=2 直接拒写(不可用),1 副本则毫无冗余。
为什么 min.insync.replicas=2:把"acks=all 的实际确认门槛"从 1(ISR 可缩到 1)抬到 2。此时 ISR={1} 时 producer 会收到 NOT_ENOUGH_REPLICAS 异常拒写——用短时不可用换"绝不写入没有冗余的数据"。这行配置是acks=all 真正生效的前提,只配 acks 不配它等于没配。
为什么 unclean.leader.election=false:Unclean 选举允许"数据落后的副本"当 Leader。假设 Leader(offset=1000)宕机,副本 B 只同步到 offset=980,Unclean 选举让 B 上位——offset 980~1000 的 20 条已确认消息被判定"从未存在",消息无声无息地被"时间倒流"。设为 false 后宁可用 Leader 不可用(分区只读)也不让数据回退:"不可用"是故障,"丢数据"是事故,二者不可兼得时选前者。
3.3 一个必须写进值班手册的连锁反应
Broker 2 宕机
→ 分区 p0 的 ISR 从 {1,2,3} 缩到 {1,3}
→ min.insync.replicas=2 仍满足,继续写入(正常)
Broker 3 也抖动被移出 ISR
→ ISR = {1},低于 2
→ 该分区拒绝写入(NOT_ENOUGH_REPLICAS),业务收到发送失败
→ 正确处置:恢复 Broker(重启副本重新入 ISR),而不是改小 min.insync.replicas!
★ 改小配置 = 把"暂时的不可用"换成"永久的风险敞口",事故复盘里见过的最差处置
四、消费者端:手动提交,把"确认"的时机还给业务
4.1 自动提交的两个丢失窗口
# 事故配置(已废除)
enable.auto.commit=true
auto.commit.interval.ms=5000
自动提交由消费者客户端按时间间隔提交"当前拉取到的最大 offset"——提交与业务处理之间没有任何关联。两个窗口:
【窗口一:先提交后处理】
poll 到 msg#100~110 → 定时器到点,自动提交 offset=110
→ 处理 #107 时抛异常 → 重启后从 #111 开始
→ #107~110 永远不会被再次处理 = 丢了
【窗口二:处理中崩溃】
poll 到 msg#100~110 → 处理到 #105 进程被 kill
→ 最近一次自动提交在 3 秒前(offset=100 之前?之后?不确定)
→ 若已提交到 108 → #106~108 丢失
**根因是同一个:offset 的"确认"早于业务的"完成"。**手动提交把顺序倒过来。
4.2 手动提交模板(可直接抄)
props.put(ENABLE_AUTO_COMMIT_CONFIG, false); // ① 关自动提交
@KafkaListener(topics = "order-pay", groupId = "inventory")
public void onMessage(ConsumerRecord<String, String> record, Acknowledgment ack) {
try {
inventoryService.deduct(record); // ② 业务处理
ack.acknowledge(); // ③ 成功才提交
} catch (BizException e) {
// 可恢复业务异常:重试或进重试队列,不提交(会被重投)
retryQueue.send(record);
ack.acknowledge(); // 已转存,可提交防重复拉取
} catch (Exception e) {
// 不可预期异常:不提交,下次重新拉取
log.error("consume failed, offset={}", record.offset(), e);
throw e; // 让错误可见(Spring Kafka 会按容器策略处理)
}
}
spring:
kafka:
listener:
ack-mode: MANUAL # Spring Kafka 配合 Acknowledgment
type: single # 单条确认,粒度最细(batch 模式见 FAQ 4)
consumer:
max-poll-records: 100 # 单次拉取量与处理能力匹配
为什么用 commitSync 而不是 commitAsync:同步提交阻塞等 Broker 确认,失败会重试,语义上是"尽力确保提交成功";异步提交快但可能静默失败(offset 实际没提交成功)。消费提交不在毫秒级敏感路径上,可靠性优先——核心链路用 commitSync,高吞吐旁路才考虑 async+定期 sync 混合。
为什么必须接受"重复":手动提交的时序是"处理 → 提交",两步之间崩溃就会重复消费(处理了但没提交,重启重拉)。这不是缺陷是必然:不丢和唯一不可兼得,只能二选一——选了"不丢"(至少一次),就要用幂等消费兜住重复。幂等方案三件套(唯一索引 + Redis SETNX + 状态机)在《消息重复消费排查》里有完整实现,此处不重复展开,只强调一句:本篇的"不丢"配置,必须和那篇的"幂等"配置成对出现,才是完整方案。
4.3 消费端的两个配套参数
| 参数 | 设定 | 为什么 |
|---|---|---|
| max.poll.interval.ms | > 单批最大处理时长 | 处理超时会触发 rebalance,拉取的批次被重新分配 = 变相重复;设大 + 监控处理耗时 |
| max.poll.records | 与处理能力匹配(100) | 拉太多处理不完,崩溃时未提交的重复范围就大 |
五、三层联动:一条消息的"不丢失"全链路
producer broker consumer
│ │ │
│ send(msg, acks=all) │ │
│ ├─ ①Leader 追加(PageCache) │ │
│ ├─ ②Follower 同步 │ │
│ └─ ③ISR 全部确认 ──────────────▶│ │
│ ★ min.insync.replicas=2 │ │
│ ISR<2 → 拒写(无冗余不上) │ │
│ ◀── ack 回调 ──┤ │ │
│ 失败→retries+幂等去重 │ │
│ 超时→补偿表 │ │
│ │ │
│ │ ◀── poll 批次 ────────────────────│
│ │ (offset 尚未提交) │
│ │ 业务处理(幂等兜底重复) │
│ │ 成功 ── commitSync ─────────▶│
│ │ ★ 处理成功才确认 │
│ │ 失败 → 不提交 → 重投 │
崩溃恢复语义:
生产侧:acks=all 未确认 → 重试(幂等去重)/ 补偿 → 不丢
Broker:ISR≥2 的消息单机故障不丢;ISR<2 宁可拒写
消费侧:未确认的消息必然重投 → 不丢(代价:可能重复 → 幂等)
三层配置不是并列的三个开关,而是一条首尾相接的责任链:生产者的 acks=all 依赖 Broker 的 ISR 门槛才有意义;消费者的"处理成功才确认"以"Broker 不会弄丢已确认消息"为前提;而整条链的"至少一次"语义,最终由消费端的幂等收口。
5.1 可靠性的价格:实测数据
同一环境(3 副本、NVMe、2KB 消息、32 分区)压测:
| 配置组合 | 吞吐 | P99 延迟 | 说明 |
|---|---|---|---|
| acks=1 + 自动提交 | 82 万 msg/s | 8 ms | 事故前的"裸奔配置" |
| acks=all+幂等 + 手动提交(推荐) | 58 万 msg/s | 14 ms | -29% 吞吐,换零丢失 |
| acks=all + min.isr=3(更保守) | 52 万 msg/s | 15 ms | 再 -10%,容忍 2 台故障 |
-29% 的吞吐里,大部分能靠批量参数(batch.size=64KB、linger.ms=5、zstd)追回来——《Kafka 为什么这么快》的四板斧与可靠性配置正交,先上可靠性基线,再用吞吐优化补性能,顺序别反。
六、常见问题
6.1 acks=all 配了,为什么消息还是丢了?
按顺序查三件事:① Topic 的 min.insync.replicas 没配或为 1——ISR 缩到 1 时 acks=all 退化成 acks=1(第三章核心),kafka-topics.sh --describe 看 Configs;② 发送回调没处理——发送失败被静默吞掉(2.3 的 11 笔就是这种);③ unclean.leader.election 开着——故障切换时落后副本上位"覆盖"了消息。三层各查一遍,丢点必然在其中之一。
6.2 enable.idempotence 和事务(transactional.id)什么关系?我该用哪个?
幂等只保证单分区内的去重与顺序;Kafka 事务(transactional.id)额外保证跨分区/跨会话的原子写入(要么全成功要么全不可见),典型场景是"消费-转换-生产"(Kafka Streams / exactly-once 管道)。普通业务生产者用幂等就够了:事务引入 transaction.timeout、Coordinator 协调、消费者要配 isolation.level=read_committed 一整串复杂度——为用不到的语义买单。判断标准:你的"一次业务动作"是否跨多个分区写入且要求原子可见,是才上事务。
6.3 min.insync.replicas=2 导致发送频繁失败(NOT_ENOUGH_REPLICAS),能不能改成 1?
不能,这是把事故的导火索拆成定时炸弹。频繁 NOT_ENOUGH_REPLICAS 说明 Broker 有健康问题(磁盘慢/网络抖/GC),正确动作是查 Broker 而不是调低防线。紧急止血的正确姿势:临时扩容分区副本(kafka-reassign-partitions.sh 把副本迁到健康 Broker),恢复 ISR≥2;若确实要降级业务可用性,用应用层降级开关(关键消息落库+延迟补发),Broker 配置保持不动。
6.4 消费吞吐大,单条 commitSync 太慢怎么办?
三个档位按需选:① batch 拉取 + 批处理完 commitSync 一次(Spring Kafka type: batch)——提交次数 ÷ 批大小,重复范围从单条变单批(幂等键按批内最细粒度兜);② 每 N 条或每 T 秒 sync 一次(ack-mode: COUNT | TIME)——折中,重复窗口 = N 条/ T 秒;③ commitAsync + 定期 commitSync 混合——吞吐最高,接受偶发提交失败(下次 poll 会覆盖)。原则:吞吐档位的提升,都在放大"重复消费"的范围,幂等消费是所有档位的前置条件。
6.5 37 笔消息已经丢了,怎么补?
两个恢复路径:① 源头重放——支付系统是权威源,按对账差异单号列表,从支付服务重新发一遍消息(走当前可靠配置 + 下游幂等);② binlog 补偿——如果消息是从 DB 变更派生的(canal 类链路),按 binlog 位点重放。补完后在补偿流水打标记,纳入对账验证闭环。教训沉淀:消息丢了不可怕,可怕的是没有"权威源可重放"——所以核心链路的架构评审里,"这条消息能不能重放、从哪重放"应该是必答项。
6.6 这套配置影响我现有 Topic 吗?迁移怎么做?
三层里两层是客户端配置(改了重启即生效),只有 Broker/Topic 层动存量 Topic 要谨慎:min.insync.replicas 从 1 调到 2 时,如果存量分区 ISR 恰好 <2 会立即拒写。安全节奏:先扩副本/修 Broker 确保 ISR 健康(describe 确认全部 ≥2),低峰再调配置,观察 NOT_ENOUGH_REPLICAS 报错为零后完成。unclean.leader.election 从 true 改 false 无需担心——它只影响"下一次故障切换",但要让值班知道:某些极端故障下分区可能长时间只读,处置预案从"自动选举"变成"人工介入"。
七、完整配置清单(可直接抄)
7.1 生产者
acks=all
enable.idempotence=true
retries=2147483647
delivery.timeout.ms=120000
request.timeout.ms=30000
max.in.flight.requests.per.connection=5
compression.type=zstd
batch.size=65536 # 吞吐补偿(四板斧)
linger.ms=5
# 代码纪律:send 必须带回调,失败进补偿表
7.2 Broker / Topic
# Broker 级
default.replication.factor=3
min.insync.replicas=2
unclean.leader.election.enable=false
auto.create.topics.enable=false
# 核心 Topic 建立时
replication.factor=3
min.insync.replicas=2
7.3 消费者
enable.auto.commit=false
max.poll.records=100
max.poll.interval.ms=300000
# Spring Kafka:listener.ack-mode=MANUAL,业务成功后 ack.acknowledge()
# 配套:消费幂等(唯一索引/SETNX/状态机)——《消息重复消费排查》
八、总结
决策树
消息丢了怎么防(按丢失点对号):
├── 发送侧丢?
│ ├── acks 是 0/1 → 改 all(配 min.insync.replicas=2)
│ ├── 回调没处理 → 补回调 + 补偿表
│ └── retries=0 / 无幂等 → retries=MAX + enable.idempotence
├── Broker 侧丢?
│ ├── min.insync.replicas 未设/为 1 → 设 2(先确认 ISR 健康)
│ ├── unclean.leader.election=true → 改 false + 更新值班预案
│ └── 副本数 < 3 → reassign 扩副本
├── 消费侧丢?
│ ├── enable.auto.commit=true → 改 false + MANUAL ack
│ ├── 处理失败没进重试队列 → 失败分支落重试/DLQ
│ └── max.poll.interval 超时 rebalance → 调大 + 监控处理耗时
└── 全配好了还担心?
└── 消费幂等收口重复 + 对账体系 + 重放预案(6.5)
三层速查表
| 层 | 核心机制 | 关键配置 | 一句话原理 |
|---|---|---|---|
| 生产者 | 确认送达 | acks=all + 幂等 + retries | "ISR 全收到才叫成功,重试不重复不乱序" |
| Broker | 交付门槛 | rf=3 + min.isr=2 + 禁 Unclean | "没有冗余的数据不交付,落后副本不上位" |
| 消费者 | 确认消费 | 关自动提交 + 处理后 commitSync | "业务完成才确认,确认必然可能重复" |
一句话
"Kafka 不丢消息"是一句有前置条件的承诺,条件就是三层各自收紧"确认"的时机:生产者把 acks 从"Leader 收到了"升级为"ISR 全部收到了",并用幂等协议让重试变得安全(PID+分区+序列号,Broker 端去重保序),用永不放弃的 retries 加有限超时预算让瞬时故障自动愈合;Broker 端用 min.insync.replicas=2 堵住 acks=all 最大的暗门——ISR 缩到 1 时 all 静默退化为 1,宁可短暂拒写(NOT_ENOUGH_REPLICAS)也不接收没有冗余的数据,用 unclean.leader.election=false 拒绝"数据落后的副本上位导致时间倒流",这三行 Topic 配置的哲学与《PG 高可用实战》一脉相承:不可用是故障,丢数据是事故,两者冲突时永远选前者;消费者端把 offset 的提交时机从"定时器说了算"还给"业务说了算",处理成功才 commitSync,代价是崩溃时必然重复——所以本篇的每一行"不丢"配置,都必须与《消息重复消费排查》的幂等方案成对出现才能闭环:不丢失与不重复在异步系统里不可兼得,至少一次 + 幂等是唯一可持续的组合;最后记住两层容易被忽略的衔接点——send 回调是"网络可靠性"到"业务可靠性"的接口,对账与重放预案是"最后一公里"的兜底,37 笔消息消失的事故没有神秘力量,只有三个各松一扣的配置漏洞:把每一扣拧紧,Kafka 的承诺才真正兑现。
关键数据
- acks=1 → acks=all+min.isr=2:吞吐 82 万 → 58 万 msg/s(-29%),P99 8→14 ms,换零丢失
- 吞吐缺口可用四板斧追回大半(batch.size/linger/zstd 与可靠性正交)
- 幂等协议开销:<2% 吞吐,却让 retries=MAX 变得安全
- min.insync.replicas=1 → 2:ISR 缩水时从"静默降级"变为"明确拒写"
- 事故账单:37 笔丢失 = acks=1(Leader 切换)+ ISR 缩水未拒写 + 自动提交抢跑,三层各漏一扣
给团队的建议
| 项目状态 | 建议 |
|---|---|
| 新集群/新 Topic | 上线即用第七章完整清单,别让裸配置进生产 |
| 存量 Topic 改造 | 按 6.6 节节奏:先保 ISR 健康、低峰调参、监控 NOT_ENOUGH_REPLICAS |
| 发送代码无回调 | 全量排查 send 调用点,回调 + 补偿表作为 CR 硬性要求 |
| 消费端全自动提交 | 列入整改清单,与消费幂等一起做(成对上线) |
| 无对账/重放预案 | 核心链路评审必答:"消息丢了从哪重放、谁负责对账" |
| 值班手册 | 收录 3.3 的 ISR 缩水连锁反应与"禁止调小 min.isr"红线 |
互动话题:你们踩过消息"凭空消失"的坑吗——是发送侧静默失败、ISR 缩水,还是自动提交抢跑?你见过最离谱的"找回消息"操作是什么?评论区聊聊,点赞最高的丢消息案例我会做一次完整复盘,并列出"如果当时配对了会发生什么"的对照表。
标题:Kafka 消息不丢失实战:生产者 ack + 副本 ISR + 消费者手动提交的完整配置
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/10/23/1792334209629.html
公众号:服务端技术精选
- 引言
- 一、先搞清楚:消息会在哪里丢
- 1.1 消息生命周期的丢失风险地图
- 1.2 先建立"确认"的心智模型
- 二、生产者端:acks + 幂等,把"送达"定义清楚
- 2.1 acks 三档语义:你到底确认了什么
- 2.2 配置与"为什么"
- 2.3 发送侧的代码纪律:回调必须处理
- 三、Broker 端:副本 + ISR,把"交付"的门槛立起来
- 3.1 ISR 是什么:同步副本集合
- 3.2 Topic 级配置与"为什么"
- 3.3 一个必须写进值班手册的连锁反应
- 四、消费者端:手动提交,把"确认"的时机还给业务
- 4.1 自动提交的两个丢失窗口
- 4.2 手动提交模板(可直接抄)
- 4.3 消费端的两个配套参数
- 五、三层联动:一条消息的"不丢失"全链路
- 5.1 可靠性的价格:实测数据
- 六、常见问题
- 6.1 acks=all 配了,为什么消息还是丢了?
- 6.2 enable.idempotence 和事务(transactional.id)什么关系?我该用哪个?
- 6.3 min.insync.replicas=2 导致发送频繁失败(NOT_ENOUGH_REPLICAS),能不能改成 1?
- 6.4 消费吞吐大,单条 commitSync 太慢怎么办?
- 6.5 37 笔消息已经丢了,怎么补?
- 6.6 这套配置影响我现有 Topic 吗?迁移怎么做?
- 七、完整配置清单(可直接抄)
- 7.1 生产者
- 7.2 Broker / Topic
- 7.3 消费者
- 八、总结
- 决策树
- 三层速查表
- 一句话
- 关键数据
- 给团队的建议
评论