一篇读懂消息不丢:MQ 可靠性的三方合同

面试里「怎么保证消息不丢失」几乎是必考题,但多数回答只背了一句「生产者确认、Broker 持久化、消费者手动提交」。这句话没错,可一旦追问——acks=all 到底在等谁确认?min.insync.replicas 配 1 会怎样?自动提交位移为什么会丢消息?——就露馅了。消息不丢从来不是某一个开关,而是生产端、Broker、消费端三方各自履行义务的一份合同。本文把这份合同逐条拆开,每一条都对应一个真实的丢消息现场。

消息不丢的三方合同手绘信息图:三个丢消息现场、acks 语义、Broker 承诺、消费端位移与幂等

一、三个丢消息的现场

把「消息丢了」拆成三种案发位置:生产端发出去就没了(网络抖动、发送失败没重试);Broker 收下但没存住(单机宕机、副本没同步、内存消息没落盘就断电);消费端拿到了却弄丢了(处理到一半崩溃、位移提前提交)。一个不丢消息的系统,等于三份承诺同时兑现——任何一方缺席,消息就有缝可钻。Kafka、RocketMQ、RabbitMQ 三家机制名字不同,骨架却惊人一致:发送确认、持久化与副本、消费确认。下面逐方拆解,以 Kafka 为主轴。

二、生产端:acks 背后的确认语义

Kafka 生产端的核心是 acks 参数,三个取值对应三种确认语义:

  • acks=0:发后即忘,不等 Broker 任何确认。吞吐最高,但 Broker 宕机即可能丢数据——只适合指标采集这类丢了无所谓的场景。
  • acks=1:分区 leader 落盘即确认。折中方案,但它有个具体的失败现场:消息到达 leader、确认返回生产端,follower 的复制还在异步进行——就在这个间隙 leader 宕机,新选举出的 leader 上没有这条消息,而生产端早已认为发送成功。消息就这样无声无息地消失了。
  • acks=all(等价于 -1):等待所有 ISR(in-sync replicas,与 leader 保持同步的副本集合)确认才算成功。只要 ISR 里还有存活副本,消息就不会丢。

acks=all 有一个必须配套的参数:min.insync.replicas。它规定 ISR 里至少要有多少个副本在线才允许写入。复制品数(RF)为 3 时配 2,意味着 leader 加至少一个 follower 确认、可容忍一台 Broker 宕机;若配 1,则 acks=all 退化成几乎等于 acks=1——确认语义被悄悄掏空,这是生产上最常见的隐性配置事故。当 ISR 数量低于阈值时,写入直接报错、用可用性换持久性,这是设计内行为,不是故障。还要注意 ISR 是个动态集合:某个 follower 因为网络或负载落后太多,会被暂时踢出 ISR;如果此时在线副本数恰好跌破 min.insync.replicas,写入就会不可用——所以可靠性配置必须和容量规划放在一起做,副本数给足余量,而不是掐着最小值配置。

重试还有个副作用要堵:网络抖动导致 Broker 实际收到了、但确认没送达,生产端重试就造成重复写入。enable.idempotence=true 让 Broker 用生产者 ID 加序列号去重,实现单分区内的幂等发送——Broker 端追踪每个 PID 在每个分区上的序列号,低于预期序列号的消息直接丢弃。顺带一提,Kafka 3.0 起 acks 默认值已改为 all、幂等默认开启——十年前那套「默认会丢」的印象需要更新了;不过默认值只覆盖新客户端,老版本客户端与显式覆盖配置的存量代码仍要逐个核对。

RabbitMQ 与 RocketMQ 的对应物名字不同,义务相同。RabbitMQ 提供了 publisher confirm 回调(Broker 收妥后异步通知)与事务两种机制,但官方思路下事务形同虚设——开启后等同于同步执行,性能大幅下降,实践中 confirm 是公认的正确姿势。RocketMQ 则以同步发送为生产端的基本功:send 方法拿到 SendResult(含发送状态)才算完成,发送失败的异常与重试策略由应用显式处理。三家殊途同归:发送方必须知道消息真的被收下了。

三、Broker:持久化与副本的两道保险

Broker 端的承诺分两道。第一道是副本:为分区配置多副本(RF=3 是常见起点),leader 挂了 follower 顶上;配合上面说过的 acks=all + min.insync.replicas=2,单机故障既不丢数据也不停服。第二道是落盘:Kafka 的消息本来就写日志文件,页缓存的异步落盘在机器断电时可能丢最后一段,极端要求下可调刷盘策略;RocketMQ 把这个选择做得更显式——同步刷盘(SYNC_FLUSH)要等消息真正写入磁盘才返回成功,异步刷盘则可能在小概率断电时丢消息,官方最佳实践写得很清楚:要确保不丢任何消息,应启用同步 Master 或同步刷盘(SYNC_MASTER 或 SYNC_FLUSH)。落盘路径上还有一个容易被忽略的状态码:RocketMQ 的 FLUSH_DISK_TIMEOUT——发送看起来成功、但刷盘超时,消息可能已落盘也可能没有,收到这个状态该不该重试、重试又可能引入重复,都要在应用层定义清楚。两种做法的代价同样是延迟上升——可靠性从来是拿延迟买来的。

四、消费端:位移提交的经典陷阱

消费端的主角是位移(offset)提交——它标记「我处理到哪里了」,而提交时机决定了丢消息的形态。自动提交(enable.auto.commit=true)有两个经典事故现场:一是位移在处理之前就被提交,随后进程崩溃,那条消息再也不会被消费——丢了;二是处理完、提交前崩溃,重启后消息被重复消费——不丢但重复。前者对应至多一次(at-most-once),后者对应至少一次(at-least-once)。自动提交的定时器并不知道你的业务处理到哪了,它只按时间间隔机械地标记进度——这正是问题的根源:进度标记必须由业务完成情况驱动,而不是由时间驱动。

不丢消息的标准姿势是手动提交:先处理业务、落库完成,再提交位移;崩溃后从旧位移重放,宁可重复不可丢失。重复消费带来的问题交给消费端幂等解决,常见模式有三种:业务唯一键去重(给每条消息一个业务 ID,消费前查幂等表,处理过就跳过);状态机约束(更新操作设计成「只有处于状态 A 才能迁移到状态 B」,重复消息第二次执行时条件已不满足,自然空转);数据库唯一约束兜底(插入类操作靠唯一索引把重复变成主键冲突报错,事务回滚即可)。三种模式都不依赖消息系统的特性,任何 MQ 上都能用——这也是为什么说幂等是消费端自己的修行。

不丢消息的代价通常是「至少一次」的重复,这是合同里必须提前想清楚的一条。顺带还要处理一个工程细节:重放场景下消息的顺序可能与首次投递不同,对顺序敏感的业务要在幂等键里带上版本号或时间戳,避免「旧消息后到覆盖新状态」。

五、恰好一次与工程取舍

Kafka 提供了把上述全部补齐的终极形态:事务。生产端 transactional.id 加多分区原子写入,消费端 isolation.level=read_committed 只读已提交消息,再配合 sendOffsetsToTransaction 把消费位移的提交纳入同一事务——「消费-处理-生产」整个流程原子完成,任一步失败整体回滚,这就是端到端恰好一次(exactly-once)。事务机制还自带隔离旧实例的能力:新生产者用同一个 transactional.id 上线时,旧实例(「僵尸生产者」)的操作会被 fencing 掉,抛出 ProducerFencedException。代价也明码标价:每批约增加 5–10ms 延迟、吞吐约降两成,且要求 Kafka 0.11 以上、注意 transaction.timeout.ms(默认 60 秒)与 transactional.id 不得多实例复用。

工程上的诚实建议是:大多数业务用「at-least-once + 应用层幂等」就够了,不必为恰好一次付出复杂度与性能;真正需要事务的是「消费 A 主题、加工、写入 B 主题」这类流处理管线。

还可以再进一步:可靠性本身也应该分层。把业务消息按丢失代价分级——监控埋点丢了无所谓(acks=0 或 acks=1 换吞吐),订单与支付用满配合同,中间地带按「丢一条的损失 × 概率」定价。全站一把梭到最强配置是常见的过度设计:延迟和吞吐的代价是全体业务共同支付的,而绝大多数流量并不需要它们。最后把不丢消息的默认配置合同收拢成一张清单:生产端 acks=all、min.insync.replicas=2(RF≥3)、幂等开启、重试拉满;Broker 同步刷盘或接受页缓存窗口;消费端关自动提交、处理完手动提交、幂等去重兜底;监控端盯发送失败率与消费滞后。三条线各守其职,消息才真正无缝可钻。

结语

消息可靠性合同没有玄学:生产端确认与重试、Broker 副本与落盘、消费端位移与幂等,三方各自兑现承诺,再把重复消费当成默认前提去设计幂等。监控是把合同落地的眼睛——发送失败率与超时状态码的突增说明生产端合同被打破,消费滞后(lag)的持续增长说明消费端处理能力不足或位移提交出了问题,两者都该有告警阈值而不是靠用户投诉来发现。

最后回到那句面试标准答案:「生产者确认、Broker 持久化、消费者手动提交」。它没错,只是每一句背后都站着一张具体的参数表与一个具体的失败现场——acks=all 等的是整个 ISR,min.insync.replicas=1 会把它掏空,自动提交会在错误的时刻标记进度。Kafka 3.0 把很多安全项变成了默认值,但合同的灵魂从未变化——每一环的「我知道它没丢」,都必须建立在该环的确认机制上,而不是希望上。

参考资料

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?