《Java 消息中间件·可靠消息与消费治理》6.3 毒消息隔离与人工干预
毒消息不是「处理失败」,而是每次都以同样方式失败、无法自愈的消息。本篇在本机让一条坏消息把整个分区卡住 30 秒:消费位移停在 2,LAG 从 4 涨到 9;再演示转入隔离主题、提交位移、分区恢复消费,并用 libraryd 的退货事件给出人工修复后回灌必须满足的幂等前提。
tag
毒消息不是「处理失败」,而是每次都以同样方式失败、无法自愈的消息。本篇在本机让一条坏消息把整个分区卡住 30 秒:消费位移停在 2,LAG 从 4 涨到 9;再演示转入隔离主题、提交位移、分区恢复消费,并用 libraryd 的退货事件给出人工修复后回灌必须满足的幂等前提。
原地重试会占住分区,进程一崩还会丢掉等待中的重试。本篇用三级重试主题把等待外置,实测一条消息从主主题到 DLT 的完整时间轨迹(5 秒、15 秒、45 秒三档退避),量化重试带来的记录放大:100 条业务消息产生 140 条记录,并给出可落地的参数与观测判据。
死信队列不是失败消息的垃圾桶,而是一份可回放的失败证据。本篇给出进入死信的判据、必须随消息搬运的元信息清单,并在 Kafka 4.3.0 上跑通完整链路:九条消息中一条重试耗尽进入 DLT,贴出死信头原文,再把修复后的消息回灌原主题,最后给出可验收的判据与反模式清单。
下游只有每秒 870 条的能力,消费者硬拉会怎样?实测不限速时 3.7 秒把 3 万条全拉进 JVM,队列峰值 29783,客户端内 p99 滞留 29.7 秒;用 pause/resume 把队列压到 1995,p99 降到 1.9 秒。附一份可运行的背压实现与 AIMD 限速器。
积压处置的第一步不是扩容,而是算清楚「追得上还是追不上」。本篇用 9 万条真实积压,分别走加消费者、提高批量和跳过三条路径,记录每秒采样的 LAG 曲线:3 个消费者只快 1.4 倍、批量放大 10 倍只快 13%、跳过 7.2 万条要先等组变空。
同一时刻同一分区,kafka-consumer-groups.sh 报 LAG=800,客户端 records-lag-max 显示 900,手算 LEO−position 是 600。三个数都不是 bug,而是三种口径。本篇用一次真实取数把这三种口径拆开,附源码依据与 70 秒空转后指标变 NaN 的实测。
扩容一个消费者会触发整组再平衡,停机的姿势决定了消息是重复、丢失还是要等会话超时。本篇给出一份完整可运行的优雅停止代码,用 1200 条消息实测 SIGTERM 后零重复零丢失;再让一个消费者停 poll 25 秒,贴出 max.poll.interval.ms 踢出的原始 WARN 日志,并数清 1212 次投递里的重复。
7 个分区、2 到 3 个消费者,range、round-robin、sticky、cooperative-sticky 四种策略各跑一遍:谁撤销了全部分区,谁只撤销搬走的那几个,谁多花一轮协议,谁把 8 个分区分成 4-2-2。本篇把分配结果、迁移分区数、再平衡耗时与倾斜成因放进同一组实测数据,并给出观测与选型方法。
消费组不是一个进程列表,而是一份职责加一套位移。本篇用 600 条消息、两个快慢不同的消费者和三个订阅同一主题的组,量出「同组能力必须一致」与「多组订阅的复制成本」,再给出按业务域、按处理职责、按分区手工分配三种方案的对照,以及组 ID 命名、空组回收与选择清单。
事务不是免费的:多一次协调器往返、多一个状态主题、多一段 LSO 阻塞,还要为 abort 的数据付磁盘钱。本篇用 broker 请求日志数出每个事务多发了哪些 RPC,用两组单次运行对比事务与非事务的发送耗时,并给出判断「什么时候不该用事务」的工程判据。
事务不会永远开着:transaction.timeout.ms 到期后由协调器主动中止,客户端下一次提交只会收到一个致命错误。本篇实测超时事务的 abort 时点与数据形态,再用两个同 transactional.id 的生产者复现 fencing,说明僵尸写入为什么会被拒绝。
事务生产者把多条消息与消费位移放进一个原子单元。本篇跑通 transactional.id、initTransactions、begin/commit/abort 全流程,用同一份数据分别喂给 read_committed 与 read_uncommitted 消费者,对照提交与中止的可见性,并解释 LSO 为什么会挡住事务之后的消息。
Kafka 事务保证的是「多条记录与位移一起提交或一起回滚」,业务恰好一次保证的是「副作用只发生一次」,两者覆盖的范围不同。本篇实测每条一事务、十条一批事务与非事务发送的耗时,再用一个 read_committed 消费者重放已提交记录,展示事务提交后业务仍然重复的完整链路,并给出两种方案的对照表与成立前提清单。
唯一约束、去重表、状态机是三种成本不同的幂等实现,选错一种会在并发或重放时失效。本篇先比较三者的写放大、存储与失效边界,再给出一份完整可运行的 IdempotentConsumer:按业务键串行化、PROCESSING/DONE 状态、失败不记账、TTL 惰性清理、容量硬上限,并用 3000 次重复投递和 16 线程同键并发验证它。
重复不是一种故障,而是四种:生产侧重试、消费者再平衡、位移回退、下游重放。本篇用单节点 Kafka 分别构造四类重复,给出每类的原始输出与可识别特征——位移是否相同、eventId 是否相同、是否留下新的生产者序列号,并据此给出一张排查判据表。最后给出处理账本的最小设计,让四类重复在日志里可被区分。
自动提交提交的是「拉取位置」而不是「处理进度」:一次 close 就把 20 条里没处理的 15 条永久跳过。本机用 40 条消息对照 auto-close、auto-kill 与手动提交三种写法,给出已提交位移、重启后重读范围与源码调用链,并划清提交失败时哪些异常可以重试、哪些必须重新加入消费组。
acks=all 只是必要条件:还要看 min.insync.replicas 是否被副本数钳制、ISR 当时够不够、以及是否发生不干净选举。本篇用一个 controller 加三个 broker 杀掉两任 leader,对照 acks=1 的 100 条静默丢失与 acks=all 的完整保留,再拆开 retries、delivery.timeout.ms 与幂等生产者的边界。
一条消息从 send() 到下游落库要经过生产端、broker、消费端、下游存储四段,每段对「成功」的定义都不同。本篇给出四段责任表与各自的失败模式,并用 8 条消息把一次完整追踪和一次崩溃恢复跑出来,说明「消息不丢」到底停在哪一段,以及三段交接处必须写明的契约。
卷二全 50 篇的完整目录:五部分、15 章、45 节,加序、前言与两个附录。每章标注它回答的那一个问题,并给出三条阅读路径——赶时间只看判据的、要落地一条链路的、准备做演练与验收的,另外标明哪些实验可以跳过、哪些必须亲手做,以及每节末尾那条判据该怎么用。
把这一卷的实验底座写清楚:单节点与三节点集群怎么起、客户端工程怎么配,以及最关键的故障注入口径——stop、kill、pause、改配置四种手段各自模拟什么、ISR 何时收缩、本机实测到多少,还有哪些故障本机做不出来、必须标注为未执行并说明它影响哪一条结论。