“Kafka能保证消息不丢吗”这个问题我被人问了不下几十次每次数据对不上账第一反应就怀疑Kafka是不是偷偷吞了消息。先说结论Kafka的可靠性设计在大规模消息系统里确实属于第一梯队但它离“100%不丢”这个绝对承诺永远差那么一口气。这口气不是工程能力不行而是分布式系统在一致性、可用性、性能之间本来就得做选择题。这篇文章从生产者、Broker、消费者三个环节拆开讲讲Kafka到底把消息丢在哪些地方以及我们能用哪些配置把这些洞尽可能堵上。1. 先搞清楚Kafka到底承诺了什么1.1 三个消息语义级别在讨论丢消息之前先对齐一个概念消息系统一般都谈三种投递语义。最多一次at-most-once的意思是消息可能丢但绝不会重复至少一次at-least-once的意思是消息不会丢但可能会重复精确一次exactly-once是最理想的状态不丢也不重复。Kafka的杀手锏之一就是它在 0.11 版本以后通过幂等生产者加事务API能在“单个分区内”做到精确一次。注意这个前提单个分区内。跨分区、跨会话、跨应用的端到端精确一次至今依然是一件极其复杂的事绝大多数团队根本没有走到那一步。很多人以为“Kafka不丢消息”是它的默认属性其实这是个误解。Kafka默认的配置在大多数版本下只能给你“至少一次”的保证而且这个保证还有一串前提条件。换句话说默认配置下消息确实不容易丢但一旦你踩中某些边界条件它丢给你看。1.2 默认配置离100%有多远我们平时聊Kafka的可靠性通常都是“配置达成”的可靠性不是“天生自带”的可靠性。所谓配置达成就是你得同时满足好几个条件缺一个保护网就破一个洞。比如生产者端要设置acksallBroker端的副本数要大于等于某个值min.insync.replicas要给到合理配置消费者端要关掉自动提交要做幂等还要处理Rebalance等等。这一整套下来你才能在实践中达到“消息基本不丢”的效果。但“基本不丢”和“100%不丢”之间依然隔着一条天然的鸿沟。只要你的数据是跑在多台机器、多个进程之上的分布式系统里网络分区、节点宕机、磁盘故障、断电这种物理事件就永远客观存在。配置只能缩小窗口不能消除窗口。搞清楚这一点再看后面所有的细节你就会发现所有的丢消息事故都能归因到某个窗口里。2. 生产者端你说“发成功”和服务器“真的收到”之间隔着什么2.1 从send到ack链路里每一步都可能出问题先看一个典型的生产者发送流程。业务代码调用 producer.send() 的时候消息不是马上飞到服务器上的而是一路经历本地缓冲、网络发送、Broker接收写入最后由Broker应答ack回调告诉业务方“发送成功”。这里每一步都有坑。本地缓冲阶段如果客户端进程在消息还没刷进网络之前崩了这条消息就没了。网络发送阶段消息可能已经到达Broker但Broker在处理过程中宕机了或者客户端没收到ack这条消息到底有没有被成功写入客户端自己也无法确认。之后你只能靠重试。重试本身是一件好事但如果你没开幂等重试会导致消息重复。这时候你面临的两难是重试可能造成重复不重试可能造成丢失。于是Kafka从0.11版本引入了幂等生产者在单分区内通过序列号机制解决了这种因重试导致的重复问题。2.2 acks参数决定你“信”多少生产者端最核心的参数就是acks它直接控制你等待多少确认。acks0 表示发送出去就算完事完全不等服务器回话。这个配置下丢消息是常态飞机会掉网线会抖你根本不知道服务器收到没有。一般在日志采集、指标上报这种可以容忍少量丢失的场景才敢这么用。acks1 表示Leader副本收到并写入页缓存之后就返回ack。注意是页缓存不是落盘而且只等Leader一个副本确认。Leader一旦宕机数据还在页缓存里没刷脏或者还没同步给Follower这条消息就跟着丢了。这是Kafka的默认配置性能好但可靠性只能说“一般够用”。acksall 表示要等所有参与复制的ISR副本都确认接收才返回ack。这个配置能扛住单个副本宕机因为在Leader眼中的有效副本都已经握过手了。但如果你只配了acksall却没有配min.insync.replicas那么ISR列表里只有一个Leader存在时它照样会自己给自己确认实际上退化成acks1的效果。这里有一个我踩过多次的坑很多人把acksall当万能钥匙以为配上就稳了却没关注副本数和min.insync.replicas的联动关系。结果上线的还是单副本Topicacksall形同虚设。2.3 重试和幂等能解决什么不能解决什么生产端第二个关键组合是 retries 和 enable.idempotence。开了幂等并且把retries调到合理值网络抖动时的临时失败就能被自动消化不会出现重试导致的重复。但幂等生产者的保护范围只覆盖“生产者进程与单个分区Leader的会话”范围内。如果生产者本身以事务方式写了多个分区中途进程崩了这个多分区的原子性就需要事务API来保障。不加事务的话一部分分区可能提交了另一部分没提交你就得靠下游去对账。从可靠性角度讲生产端的建议很直接retries 调大到足以应对瞬时故障acksallenable.idempotencetrue。再把max.in.flight.requests.per.connection控制好5个以内配合幂等都是安全的。2.4 生产端丢数据场景小结生产端最容易丢数据的场景按发生频率排个序。第一进程在消息未发送或未确认时崩溃消息在内存缓冲里直接蒸发。第二acks0或1配置下Leader宕机导致已确认消息丢失。第三网络分区把客户端和Broker隔开客户端重试投递到错误Broker或异常没有正确捕获导致消息直接被丢弃。第四业务代码里自己吞了异常。send方法是异步的如果你不在回调里处理失败结果也没设置重试发送失败的消息就悄无声息地没了。不少人排查半天最后发现是自己代码里try...catch空实现把发送失败吞进了肚子。提示生产端的可靠配置必须与Broker端配合单独只看生产端参数是没有意义的。3. Broker端数据明明已经“入队”了到底哪里丢了3.1 刷盘Kafka为什么故意不急着落盘Kafka的性能神话有很大一部分来自它“延迟刷盘”的设计。消息写入Leader的时候其实先落进操作系统页缓存Page CacheKafka并不会立刻调用fsync强制刷到磁盘。这个设计让Kafka能承受极高的写入吞吐因为顺序写内存再异步刷盘比每次同步刷盘快了好几个数量级。但代价就在这里Page Cache里的数据在宕机或者断电时可能来不及落盘。尤其是遇到整机断电内存一失电所有没刷盘的数据全没了。你可以通过配置 log.flush.interval.messages 和 log.flush.interval.ms 来调整刷盘节奏理论上把刷盘间隔调得很小可以缩小丢失窗口但代价是写入延迟上去了吞吐下降。这里没有银弹你只能根据业务对可靠性诉求在性能和风险之间选一个平衡点。大多数生产环境不会走极端因为副本机制已经能覆盖单机故障场景没必要用刷盘来牺牲性能。3.2 ISR机制副本追不上怎么办Kafka的高可用主要靠多副本但多副本之间不是强同步而是基于ISRIn-Sync Replicas的动态同步机制。ISR就是当前与Leader保持同步的副本集合。Follower会周期性拉取Leader的数据只要它落后太多或者长时间没有拉取请求就会被踢出ISR。Leader处理写入时只需要等ISR里的副本确认不需要等所有副本。这里就出现一个窗口如果某个Follower因为网络延迟或负载过高落后了但它还在ISR里这时候Leader把数据写进去了并返回了ack然后Leader挂了那么这个落后的Follower被选为新Leader新Leader上就没有刚刚那条已经ack过的消息。你可能会说把滞后阈值调小一点比如 replica.lag.time.max.ms 调小落后就立刻踢出去窗口不就缩小了吗确实能缩小但踢得太快也有副作用万一网络只是抖了一两秒副本就被踢进非同步列表此时如果恰好Leader宕机甚至触发unclean选举情况会更糟。3.3 未同步副本当选之后丢数据就变成现实Kafka的领导人选举有个参数叫 unclean.leader.election.enable默认是false。这个参数控制的是当Leader挂了ISR里没有其他可用副本时是否允许一个不同步的副本非ISR副本当选为新Leader。如果设为true系统可用性优先——只要还有活的Broker就能继续服务哪怕它上面的数据是旧的。这会直接导致之前写入并被ack过的数据丢失。如果设为false宁可让分区短暂不可用也不允许丢数据。这个选择本质上就是CAP里的可用性和一致性之争。大部分金融、交易类业务会把unclean.leader.election.enable设为false宁可暂停几分钟也不接受静默丢数据。而日志、监控类场景可能更在乎可用性选true也无妨但要清楚代价。注意设置unclean.leader.election.enabletrue等于亲手打开了丢消息的大门这不是Bug是设计选择。3.4 min.insync.replicas 如何兜底acksall之所以能起作用前提是ISR里至少有一定数量的副本。min.insync.replicas 就是定义这个最低数量的参数。比如你设置了 replication.factor3min.insync.replicas2那么只有当ISR里至少有两个副本时分区才能正常接受读写。如果只有一个副本生产者会被拒答宁可不写入也不给你假确认。这个配置配合acksall才能把“已确认消息”和“副本安全落位”绑定在一起。否则当只剩一个Leader副本时acksall照样能自嗨式确认消息就丢得悄无声息。我见过很多团队配置了副本数3min.insync.replicas却保持默认1结果机房某个机架上两台机器同时断电另一台在ISR里写入依然照常等机器起来发现数据落后了一大截。回过头来看只怪当初没把min.insync.replicas抬高。3.5 极端场景整个副本组都没了有了多副本、ISR、acksall、min.insync.replicas是不是就能拍胸脯说100%不丢了还不是。你要考虑一个场景某个分区的所有副本正好同时被拉进同一个故障域里。比如一个机架断电副本刚好都在这一个机架上或者云厂商某个可用区故障而你所有副本都部署在这个可用区那就没有任何副本能救你。还有另一个隐蔽场景Leader宕机后ISR里的Follower接管了分区但旧Leader经过一段时间的隔离后被重新拉回集群如果它上面有一些未被同步的“脏数据”Old Leader的分区数据会被截断覆盖。注意这里不是追加而是对齐——Kafka会强制旧Leader向新Leader看齐那么那些“领先”于新Leader的数据就会被删除。这些数据虽然在旧Leader上存在过但对客户端来言已经不可见了。很多人所说的“Kafka丢数据”有一部分就是这种Leader交替时的数据截断。想对抗这种极端情况只能靠跨机房部署副本、灾备集群或者备份。这个话题第五部分再展开。4. 消费者端优先级最高的“幽灵丢数据”4.1 自动提交20ms后你想回滚都来不及很多业务排查丢消息最后发现消息其实一直在队列里躺着是消费者根本没处理完。消费者端丢数据的头号嫌疑人是自动提交偏移量enable.auto.committrue。自动提交默认每隔5秒提交一次offset。这5秒内如果消费者处理完了一批数据但还没来得及提交offset然后进程崩了再重启就会从旧offset继续消费这部分消息被重复消费一遍。反过来更危险的场景是如果消息拉取之后、还没处理完成自动提交周期到了offset已经往后挪了然后消费者进程崩溃或业务处理抛异常重启后直接跳过了这批消息。这就是真实的“消费侧丢消息”。这种丢法最憋屈因为Broker那边的数据明明完好无损是你的消费进度把数据“跳”过去了。4.2 手动提交的两种姿势都不完美关闭自动提交之后你面临手动提交的设计选择。先处理完业务再提交offset这是标准推荐但它带来的副作用是消息处理完了、offset还没提交的时候一旦消费者崩溃重启后它会重新消费这批消息造成重复。这是“至少一次”语义的正常表现不能算“丢”。反过来如果你图快先提交了offset再做业务处理那业务处理一旦失败或崩溃offset已经前移消息再也不会被这个消费组消费到。这属于自己把自己坑了——消息没丢但你的消费逻辑把它弄丢了。手动提交时还要注意提交的频率和粒度。每次poll一批数据就频繁提交性能差跨批次累积很久再提交重复窗口变大。比较稳妥的做法是每次poll后处理完本批立即同步或异步提交本批最大offset同时在业务侧记录处理进度必要时做幂等。4.3 Rebalance消费者组里的一次“地震”消费者组发生重平衡Rebalance时分区所有权会重新分配。在旧方案里一个消费者正在处理某个分区重平衡来了它被安排处理另一个分区而新接管的消费者从已提交的offset开始消费。如果之前处理的这批消息还没来得及提交offset重平衡之后这些消息就会被重新分配给新的消费者重新消费造成重复。这里不会直接导致丢失但如果你在处理过程中依赖了外部状态并且没有做好幂等重复消费可能造成脏数据最终对账的时候看起来就像丢消息或者错乱了一样。更麻烦的是如果消费者处理消息的耗时比max.poll.interval.ms长消费者会被判定为卡住被踢出消费者组触发重平衡。那种“消费者被踢出去又加回来又踢出去”的反复抖动最容易导致消息处理的空窗期。4.4 消费侧预防丢数据的通用原则消费侧的可靠性本质上不靠Kafka参数靠业务代码和数据设计。接口要做到幂等这是老生常谈但必须做。因为你无论怎么配置至少一次语义意味着重复消费是常态而非偶然。数据库写入时用唯一键约束外部调用前先查重或者给每条消息生成唯一业务ID。其次独立维护消费位点不要完全信任Kafka的offset。你可以把已处理消息的最大offset写进外部存储重启或重平衡后先从外部存储恢复位点尤其是那些极端依赖不丢消息的场景。还有一点常被忽略消费者处理的失败异常一定要重试而不是直接忽略。结合死信队列把实在处理不了的消息单独捞出来人工介入比硬着头皮继续消费要可靠得多。5. 如何用工程手段逼近“100%不丢”5.1 生产环境推荐配置组合如果把可靠性当作系统需求来认真设计以下一套配置组合是很多有经验的团队在生产环境会采用的基线覆盖了“生产者发得稳、Broker存得稳、消费者消费得稳”三个环节环节参数推荐值生产者acksall生产者enable.idempotencetrue生产者retries尽量调大建议不低于5生产者max.in.flight.requests.per.connection5需配合幂等Brokerdefault.replication.factor3Brokermin.insync.replicas2Brokerunclean.leader.election.enablefalseBrokerreplica.lag.time.max.ms30000按网络状况调整消费者enable.auto.commitfalse消费者max.poll.records按单批处理耗时控制消费者max.poll.interval.ms适当放宽避免误踢需要留意的是这套组合不是无脑照搬要根据业务对性能的要求做取舍。min.insync.replicas2意味着在ISR副本不足时写入会拒绝瞬时可用性下降。如果业务无法接受写入被拒绝就得牺牲一部分“确认安全度”。5.2 你能为“不丢”付多少性能税可靠性每提高一档性能总会付出代价。举个例子acks0的时候单分区的写入吞吐可以拉满但故障时几乎必丢。acks1能扛住部分故障性能和可靠性相对均衡。acksall min.insync.replicas2时Leader要等两个副本确认写入延迟会明显上升尤其是跨机架部署时网络往返时间直接加进每次写入的耗时。我自己做过一个简单的压测对比同样的Topic、同样的数据量从acks1切到acksall min.insync2吞吐大约下降20%到30%延迟增加明显具体数字跟网络和副本部署位置有关。这个代价对绝大多数核心交易链路来说是可以接受的但对那种每秒百万级流量的日志场景可能就有点肉疼了。建议大家上线前一定要做一个PIT压测用真实的数据模型和部署拓扑测一测这种可靠性配置下你的业务能不能顶得住峰值流量。我见过有团队上了全可靠配置结果大促时生产者retries疯狂堆积最后反而拖垮了链路。5.3 监控指标清单不要等丢完了才报警配置再全也保不齐遇到那些没法提前预判的故障。所以可靠的运维体系比任何参数都重要。你需要盯几类核心指标。ISR收缩和扩张是副本健康状况的晴雨表。只要ISR里有副本被踢出说明这个分区的冗余能力已经下降此时恰好Leader宕机就极可能丢数据。建议对ISR变化做告警而不是等分区不可用了才看到。生产者端要盯错误重试率和发送失败率。如果重试率持续上升大概率是集群网络或者负载出了问题。消费端要盯堆积量Lag堆积突然归零不一定是好事有可能是offset提交异常堆积暴涨则说明消费能力跟不上或者消费者被踢出了。建议把Kafka的JMX指标接入你的监控系统。重点看UnderReplicatedPartitions、OfflinePartitionsCount、ActiveOffsetManagerCount这些关键项。每个指标背后都值得单独写一篇排查文章但对大多数团队来说先把这几个监控做扎实就已经能把90%的问题暴露在早期。5.4 最后一道保险备份和双写即使你做到上面所有配置依然存在最后一道暗门物理灾难。整个机房的电断了磁盘阵列烧了云厂商把某个可用区撤了这些属于小概率但必须考虑的事件。应对思路就两条。一是跨可用区部署Kafka副本把副本分散到不同的故障域从物理上避免单点。二是搭建灾备集群通过MirrorMaker之类的工具做异步跨集群同步。这里注意异步同步意味着灾备集群的数据天然有一小段延迟窗口极端情况下主集群丢的数据在灾备集群里也可能不存在。所以灾备集群只能兜底“主集群整体不可用”的恢复场景不能兜底“所有副本同时损坏”的数据安全。更狠一点的做法是业务侧双写两个独立集群然后下游消费者做去重和合并。这种方案的成本和创新性都比较高一般只有金融级系统才用得起。但是你可以想一想如果一条消息真的要求100%不能丢那么只靠Kafka本身是没有解的必须靠业务侧的数据冗余来兜底。写在最后的一点体会从业这些年来我最大的感受是Kafka的可靠性问题永远不是某个单一参数能解决的它是生产者、Broker、消费者三个环节的联合工程。不要被“Kafka不丢消息”这种口号迷惑你得清楚每一种配置选择对应的成本和风险窗口。我给团队定的默认原则很简单核心业务链路无条件采用最高等级可靠性配置哪怕付出一些性能和可用性代价非核心的日志、埋点、监控类数据可以放宽配置换取吞吐和成本优势。遇到“消息到底丢没丢”的争议先查消费端有没有做幂等再查生产端是不是真正开了全部保护最后再怀疑集群本身。最后分享一个亲测有效的小技巧每次上线前故意做一次破坏性演练直接杀掉一个Broker观察整个链路的堆积和消费情况再杀掉一个消费者看看重复消费窗口有多大。这种演练比任何文档都更能暴露你系统的真实弱点。尽早把这些窗口找出来别等到大促当天才被现实上一课。