资讯动态

Kafka丢消息怎么排查?生产端、Broker、消费端防丢配置模板

发布时间:2026/10/20 2:45:07 来源:尧图企业网站定制
Kafka丢不丢消息这个问题我在不同场合听到过太多次面试里、技术群里、生产故障复盘会上都有。每次被问到我一般不会直接说“不丢”或者“会丢”因为这两个答案放在不同配置和场景下都能对上也都能被打脸。Kafka是一个分布式消息中间件核心价值是处理高吞吐的实时数据流但它不是一把万能的“数据保险箱”。你说它丢消息它有一套副本机制、ACK确认机制、幂等机制配置得当的情况下可以做到非常高的可靠性你说它不丢消息生产环境里我又确实见过订单消息凭空消失的例子。所以“丢不丢”这个问题本质上是一道问答题你在什么配置、什么使用方式下允许它丢到什么程度又准备怎么兜底。这篇文章我把自己的实际经验和踩过的坑整理出来分四个层面拆什么是真正的丢消息、生产端怎么防丢、Broker端怎么防丢、消费端怎么防丢最后再给一套可以“抄作业”的配置模板和排查思路。内容以我长期使用Kafka的经验为基础参数细节对照官方文档的习惯配置整理不保证100%覆盖所有坑但能帮你把“Kafka会不会丢消息”这个问题想明白。1. 先把“丢消息”这件事定义清楚很多人一上来就问“Kafka保证不丢吗”其实这个问题本身缺少上下文。Kafka对外提供的是三种投递语义最多一次、至少一次、恰好一次。这三种语义对应的是不同场景的取舍Kafka本身不承诺“必然不丢”它承诺的是“你按某种方式配置后可以做到哪种级别的可靠性”。最多一次发出去就完事无所谓对方收没收到丢也就丢了。适合一些不计较丢失的日志、打点数据。至少一次保证消息不会丢但可能重复。这是Kafka默认的、也是绝大多数业务场景在用的语义。恰好一次既不会丢也不会重复但对性能有影响实现也最复杂通常要结合事务API使用。所以“丢消息”可以从两个视角来看。系统视角的丢指的是消息进入了Kafka但最终没有成功写入副本、或者副本确认了你才收到成功返回、但数据后面又没了业务视角的丢指的是业务侧觉得自己“应该收到了”而实际上没收到或者消费端处理失败但位点已经自动提交了这种丢往往比Kafka底层丢数据更常见。从我接手过的生产环境来看真正让消息“消失”的原因80%以上出在生产和消费者配置上Broker本身倒不太容易丢。但正因为如此很多人踩坑都踩在“我以为默认配置就够用了”结果默认配置在某些极端情况下就是会丢。后面几节我会说得详细一点把每个环节的丢法、丢的原因、对应解法拆开来讲。2. 生产者端消息还没进Kafka就丢问题出在确认机制生产者把消息发到Kafka怎么才算“发送成功”这里有一个很关键的参数acks。很多同学知道这个参数但理解得不够透。简单说acks决定的是“Broker在什么情况下给生产者返回成功”。acks0生产者发出去就算完根本不等Broker回复。acks1分区Leader把消息写入自己的本地日志就返回成功不等待其他副本同步。acksall也就是-1分区Leader要等所有ISR副本都写入成功才返回成功。如果你用acks0那消息大概率会丢。生产环境一般不会这么干但有些日志场景会选它换吞吐。用acks1是不是就稳了也不一定。假设消息成功写入Leader节点Leader还没来得及把数据同步给其他FollowerLeader直接宕机了。新的Leader被选出来之后如果这个新Leader的日志里没有那条消息那这条消息就丢了。很多人觉得我自己设置的是acksall为什么还是丢了这里要提醒一个细节acksall要生效还依赖另一个参数min.insync.replicas。打个比方acksall是说“我要所有同步副本都确认”但如果一个分区只有1个副本在线ISR集合里就它一个那它自己确认一下也就等于“所有同步副本都确认”了。结果就是Leader宕机的时候数据照样丢。所以生产环境要求高可靠min.insync.replicas至少设置成2最好配合3副本。还有一个看着不起眼但实际很重要的参数retries。生产者发送消息如果Broker暂时不可用发送失败会触发重试。如果你把retries设成0那一次失败就直接丢消息。默认值是Integer.MAX_VALUE看起来没问题但重试不生效还和max.in.flight.requests.per.connection有关。假如这个参数大于1而retries又开启着一旦发送过程中某条消息重试但后面更新的消息已经先发出去了就会造成乱序。这也是为什么很多老项目会把max.in.flight.requests.per.connection设置成1宁可牺牲吞吐来保顺序。那有没有办法既保证不重复、不乱序还不牺牲吞吐有。从0.11版本开始Kafka引入了幂等生产者。开启enable.idempotencetrue之后生产者会为每条消息附带一个序列号Broker端按序列号去重同一个生产者发送的同一条消息即使因为网络重试被提交多次Kafka内部也只会保留一次。这样你可以放心地把max.in.flight.requests.per.connection调大性能不牺牲消息也不重复。注意两点第一幂等机制只对单分区有效第二它解决的是“生产者重试导致的重复”不能解决业务侧的重复消费。生产者的防丢配置我整理成了一段示例后续会统一放在第五部分的模板里这里先简单看一眼Properties props new Properties(); props.put(bootstrap.servers, node1:9092,node2:9092,node3:9092); props.put(acks, all); props.put(retries, Integer.MAX_VALUE); props.put(enable.idempotence, true); props.put(max.in.flight.requests.per.connection, 5);这套配置的核心思路就一句话让“发送成功”这个信号只在数据真正落到多个副本之后才返回。3. Broker端数据进了Kafka还会不会丢消息通过生产者成功写入Kafka之后是不是就进保险箱了不是。Broker端同样有数据丢失的风险而且这部分问题往往更隐蔽因为它不是每次故障都会暴露可能运行几个月都没事某一次节点宕机就出问题。先看Kafka的存储模型。一个Topic分成多个分区每个分区有多个副本副本分为Leader和Follower。生产者和消费者都只和Leader通信Follower在后台异步拉取Leader的数据保持同步。所有和Leader保持同步、没有明显落后的副本组成了ISR集合也就是同步副本列表。如果某个Follower在replica.lag.time.max.ms时间内默认30秒没有跟上Leader的进度就会被踢出ISR。现在重点来了。如果Leader节点突然宕机了Kafka需要从ISR里选一个新的Leader。这个过程叫Leader选举。这里有一个参数unclean.leader.election.enable它的字面意思是“是否允许非同步副本参与Leader选举”。这个参数如果设置成true那么当ISR里所有副本都挂了或者ISR为空的时候Kafka可以选一个不在ISR里的、数据明显落后的副本当Leader。那这个新Leader的日志里可能缺了之前的部分消息这些消息就直接丢了。这个参数默认是false很多资料也反复强调一定要设置成false宁可让分区短暂不可用也不要选一个落后副本当Leader。我自己在生产环境是严格保持false的遇到过分区Leader挂掉后等待ISR恢复的情况短时间确实有一小段写入报错但数据没有丢。再说一个更容易被忽略的点刷盘机制。Kafka消息写入时是先写操作系统的Page Cache异步刷到磁盘的。如果机器突然断电或者内核崩溃Page Cache里还没落盘的数据就会丢。Kafka提供log.flush.interval.messages和log.flush.interval.ms参数控制强制刷盘但生产环境我基本不会调它一是因为强刷会严重掉吞吐二是Kafka的副本机制本身已经把“单机崩溃丢数据”这个风险消化掉了只要消息被成功复制到多个副本上任何一台机器丢了Page Cache别的副本上还有数据。你要防的是极端情况下的整机断电那属于机房层面的容灾范畴得靠多机架部署和镜像方案解决。Broker端的可靠性本质靠的是“多副本同步确认严格选举”。只要ISR里有多于一个副本acksallmin.insync.replicas≥2unclean选举关掉那么单台Broker挂掉不会丢数据最多是分区不可用的那几秒钟生产者的请求会报错然后重试。这里我再强调一个计算方式方便你判断自己的副本配置够不够如果副本因子是1任何一台Broker宕机这个分区就不可用数据如果还没刷盘也会丢。这就是典型的不满足高可用场景。如果副本因子是3min.insync.replicas2允许同一时间最多挂1台Broker数据不丢。如果你业务要求极端可靠可以副本因子3min.insync.replicas2然后机架感知分配副本让三个副本分布在不同的机架上。还要记住副本因子不是越大越好。每多一个副本Leader就要多等一个副本的同步确认写入延迟会上升。3副本是绝大多数生产环境的性价比选择。4. 消费者端最容易在不知不觉中丢消息如果说生产者端和Broker端的坑还可以靠配置参数来防那消费者端就是名副其实的“重灾区”。因为消费者端的丢消息很多时候不是Kafka丢的而是你消费逻辑写得不对消息其实还在Kafka里但你的消费者已经把位点提交了导致它“看起来丢了”或者“永远没人处理了”。消费者从Kafka拉取消息的时候Kafka会记录消费者当前读到了哪个位置这个位置叫Offset消费位点。消费者默认会开启自动提交位点的功能参数enable.auto.commit默认就是true每隔5秒参数auto.commit.interval.ms自动提交一次当前的消费位点。这套默认逻辑有什么问题假设你有一个消费者从Kafka拉取了一批量消息开始处理业务。这批消息你还没处理完或者处理到一半抛异常了结果自动提交把当前位点给提交上去了。Kafka就认为这些消息你已经消费成功了等新一轮拉取直接从记录的位点开始。那批没处理完的消息就被“跳过”了。如果你没有其他兜底消息在这里丢掉了。这个场景太常见了。我见过一个真实的案例有个团队做用户积分系统消费者从Kafka拉取用户行为消息然后同步到数据库。有一次因为一个字段解析异常某条消息处理报错但process方法里没做try-catch消费者进程没有崩溃只是那条消息后面的逻辑全没执行。自动提交照常进行最终结果是那一条行为数据没入库后续的补偿也没做用户积分少加了一档。你Kafka再可靠也拦不住这种丢法。解决方式也有好几层第一层最简单也最推荐的把enable.auto.commit设为false改成手动提交。在你确保业务处理成功之后调用commitSync或者commitAsync。第二层如果你就想用自动提交那必须保证你的消费逻辑是“先处理完再提交”的语义。比如使用Spring-Kafka的AckMode.RECORD确认位点是在记录处理完后提交。但自动提交总归有窗口期遇到进程崩溃还是可能丢。第三层消费逻辑里做幂等。对于订单、支付、积分这类关键业务消息重复消费不可怕可怕的是重复消费造成数据错误。加上唯一业务ID每次处理前先查一下有没有处理过这是最稳的兜底。这里还要提一种情况很多新手不理解消费者宕机后重启为什么有时候会“感觉消息丢了一部分”其实是因为故障发生后消费者组发生Rebalance分区从一个消费者挪到另一个消费者而位点还是上次提交的。刚才宕机的消费者可能已经拉下来一批数据但没提交这时候新消费者从旧位点开始消费那批数据会被再次拉取表现为重复消费而不是丢失。真正丢失的情况是位点已经提前提交、数据却没处理完比如自动提交模式下处理线程还跑着进程就挂了。消费者端防丢的核心其实就是两条第一不要在消息还没处理完之前就把位点提交了第二做好幂等让重复变成无害的。另外补充一个常见误区把消息送到消费者之后消费者业务代码里先把它扔到某个内存队列里再由另一个线程处理。这属于典型的“位点先提交、业务后处理”的异步消费模式一旦进程崩溃内存队列里未处理完的消息全丢。如果非要这么做要么接受丢数据的可能性要么把中间状态落到外部存储比如Redis或者数据库并保证重启之后能从外部存储恢复。5. 可以直接抄的防丢配置模板前面每一层都讲了原理这一节给一套具体的配置分不同可靠级别大家按场景去选。先看生产者配置。这里我按“高可靠”和“高吞吐”分两种参数高可靠推荐值高吞吐推荐值说明acksall1高吞吐场景可接受极小概率丢数据enable.idempotencetruetrue避免重试造成重复消息retriesInteger.MAX_VALUELong.MAX_VALUE保证网络抖动时不丢消息max.in.flight.requests.per.connection55幂等开启后可以大于1compression.typelz4lz4压缩能降低网络带宽对高吞吐友好再看Broker端也就是Kafka服务端配置参数推荐值说明default.replication.factor3主题默认副本数低于3不推荐用于生产关键业务min.insync.replicas2配合acksall保证至少两个副本确认unclean.leader.election.enablefalse禁止非同步副本参与选举宁可短时不可用也不丢数据replica.lag.time.max.ms3000030秒内没跟上Leader的副本会被踢出ISR最后是消费者配置props.put(enable.auto.commit, false); props.put(auto.offset.reset, earliest);配合手动提交try { ListConsumerRecordString, String records consumer.poll(Duration.ofMillis(1000)); process(records); // 核心业务逻辑 consumer.commitSync(); // 处理成功后再提交 } catch (Exception e) { // 记录日志等待重试或者进死信队列 }这里有个小选择commitSync还是commitAsync。我的习惯是“先异步提交失败时同步兜底一次”。因为如果还用同步提交每次poll之后都要等网络往返吞吐会受点影响。写法类似这样consumer.commitAsync((offsets, exception) - { if (exception ! null) { consumer.commitSync(offsets); } });使用这套组合再配合业务侧的幂等表消息在系统层的丢失概率已经压到非常低了。如果连这种偶发的Broker故障带来的抖动都不能接受那就需要上Kafka的镜像集群或者双写方案那就是另一个复杂度话题了。6. 生产环境丢消息了怎么定位和排查讨论了那么多理论实际上线之后你还是可能会遇到“消息丢了”的反馈。这时候不要一上来就怀疑Kafka要按链路一层层排查。第一步确认是“丢”还是“没消费”。打开Kafka消费组工具看下这个Topic的消费位点和生产位点kafka-consumer-groups.sh --bootstrap-server node1:9092 --describe --group your_group看LAG列它表示消费者落后生产者多少条。如果LAG一直增长但业务侧没有收到新数据那不是丢是消费端卡住或者压根没拉取。如果LAG为0说明消息确实被消费过了那就需要看业务日志确认数据处理是否完整。第二步确认生产端有没有发送失败的记录。Java生产者每次发送异常都会记录在回调里。如果发了消息没有回调或者回调里拿到了异常那就是生产端的问题。常见异常有TimeoutException、NotEnoughReplicasException、NetworkException。这些异常出现时数据可能已经落盘也可能没有要结合重试逻辑判断。第三步看Broker端的日志和监控指标。重点看两个指标UnderReplicatedPartitionsISR集合少于副本因子的分区数量和IsrShrinksPerSecISR收缩速率。如果这两个指标异常说明副本同步跟不上有丢数据或者不可用的风险。另一个要看的指标是请求失败率特别是生产请求的错误数量对应了生产者端看到的异常。第四步查一下那个时间段有没有Leader切换。如果Broker端日志里出现了Leader选举的记录结合unclean.leader.election.enable的配置基本能判断出是不是在选举时丢了数据。说实话我遇到过最“冤”的一次排查是费了半天劲看Kafka日志最后发现消息根本没进Kafka。业务方的发送代码里用了异步发送但没有回调发送失败的日志打印到一个没人看的旧文件里去了。数据从源头就没发出来Kafka全程背了黑锅。所以排查的第一原则永远是先确认消息真的到了Kafka。如果确认消息是进了Kafka但消费端缺失可以尝试重置消费位点。工具方式是用kafka-consumer-groups.sh加--to-earliest把位点调回最早或者--shift-by按条数偏移。注意这操作只能在确认下游可以接受重复的情况下做因为重置回更早的位点必然导致重复消费。日常防护上我建议给关键Topic做一个消息数量的对账任务。比如订单Topic生产者每生产1万条消息就发送一条“批次摘要消息”消费者侧统计收到的消息数和批次摘要比对一旦出现不一致立刻报警。这个做法老套但非常实用能在几小时内发现问题而不是等月末对账才发现数据缺了一截。7. 关于丢消息我最后还想说几句从开始用Kafka到现在我对“丢消息”这件事的认知变了三次。第一次觉得Kafka很可靠不可能丢第二次遇到生产故障觉得Kafka怎么这么不给力第三次把这些机制和参数吃透了才明白它就是一套需要正确使用才能发挥威力的系统。如果要用一句话总结我的经验Kafka本身提供了一整套用于保障消息不丢失的工具但没有任何默认配置能让每个场景都达到最佳效果。你需要理解自己业务能接受什么级别的丢失然后针对生产端、Broker端、消费端分别做配置并且在下游业务里做好幂等兜底。把系统层的可靠性压到极限把业务层的容错做到无害才能睡个安稳觉。最后给新入行的人一个建议不妨在自己的测试环境里故意模拟一次换副本改一行重启一台节点然后观察消息是否完好。亲手制造过一次故障、再用工具把数据找回之后你对Kafka可靠性的理解会比看十篇文章都深。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑