Producer端消息丢失:acks、retries与网络超时

Producer端丢消息,说白了就是数据从你的应用发出去,但还没到Broker就丢了。我排查过不少线上问题,十有八九都跟三个参数有关:acks、retries、还有网络超时。今天咱们一个一个拆开看。

核心结论:Producer端丢消息,90%是因为acks=0或acks=1,加上retries没配好。剩下的10%,是网络超时和缓冲区满了。

Producer端消息丢失三大原因 消息丢失 acks 参数不当 acks=0 / acks=1 retries 配置不足 重试次数少 / 无重试 网络超时与缓冲区 request.timeout.ms / buffer.memory acks=0 发完就丢,不确认 acks=1 Leader挂了就丢 retries=0 失败直接丢弃 超时设置太小 来不及重试 buffer.memory满 阻塞或丢消息 💡 三个原因经常叠加出现:acks=1 + retries=0 + 网络抖动 = 必丢 排查时先看acks,再看retries,最后检查超时和缓冲区

1. acks参数:你的消息到底要等谁确认?

acks这个参数,决定了Producer要等多少个副本确认,才认为消息发送成功。我见过不少新手直接设成0,理由是"这样快"。快是快了,消息丢了都不知道。

acks值 含义 丢消息风险 适用场景
acks=0 发出去就不管了,不等待任何确认 极高 日志、监控等可丢数据
acks=1 等Leader写入成功就返回 中等 一般业务,能接受少量丢失
acks=all(或-1) 等所有ISR副本都写入成功 极低 金融、订单等关键数据

我个人习惯,核心业务必须用acks=all。你想想看,如果Leader刚写完就挂了,Follower还没同步,acks=1的情况下这条消息就丢了。我曾在项目中遇到过,一个支付系统用了acks=1,结果Broker节点重启时丢了十几条订单消息,还好有对账系统发现了。

避坑指南:我曾经以为acks=all就万事大吉了,结果发现如果min.insync.replicas没配好,acks=all也可能丢消息。比如Topic副本数是3,但min.insync.replicas=1,那实际上只要Leader写入就算成功。所以acks=all要配合min.insync.replicas≥2才真正安全。

2. retries配置:失败后你给过机会吗?

网络抖动、Leader选举、磁盘IO打满……这些在分布式系统里太常见了。如果Producer遇到一次失败就直接放弃,那消息必丢。retries就是给消息第二次机会。

默认情况下,Kafka Producer的retries是Integer.MAX_VALUE(2.1+版本),但很多人用的旧版本或者自己改成了0。我见过最离谱的配置:retries=0,还配了acks=all,说"这样最安全"——安全个啥,一次网络波动就全丢了。

我的建议:retries别设成0,也别设太大。一般设3-5次就够了。同时要配合retry.backoff.ms(默认100ms),让每次重试之间有个间隔,别把Broker打死了。

// 推荐配置示例
Properties props = new Properties();
props.put("acks", "all");
props.put("retries", 3);
props.put("retry.backoff.ms", 500);  // 重试间隔500ms
props.put("max.in.flight.requests.per.connection", 1);  // 防止重试导致乱序

这里有个坑:max.in.flight.requests.per.connection。如果这个值大于1,且启用了重试,那消息顺序可能乱掉。比如你发了A、B两条消息,A失败了要重试,B成功了,结果A重试成功后反而排在B后面。我建议关键业务把这个值设成1,保顺序。

3. 网络超时与重试机制

光配了retries还不够,超时时间也得对。request.timeout.ms默认是30000ms(30秒),但如果你把retries设成5次,每次重试间隔500ms,那30秒可能不够用。

为什么会这样?因为一次完整的发送流程是:

  1. Producer发送请求到Broker
  2. Broker处理并返回响应
  3. 如果超时没收到响应,触发重试
  4. 重试又发一次请求

如果request.timeout.ms设得太小,重试还没完成就超时了,那重试机制等于白配。我建议把request.timeout.ms设成:retries × (retry.backoff.ms + 网络往返时间),再留点余量。

实战公式:request.timeout.ms ≥ retries × (retry.backoff.ms + 2000ms)。比如retries=3,retry.backoff.ms=500,那timeout至少设成3×(500+2000)=7500ms。我一般直接设成15000ms,省心。

4. 还有一个容易被忽略的:buffer.memory

Producer端有个发送缓冲区,默认32MB。如果消息发送速度大于Broker处理速度,缓冲区满了怎么办?默认行为是阻塞,但如果你把block.on.buffer.full设成了false(旧版本),或者max.block.ms设得太小,那就会直接抛异常丢消息。

我记得有一次排查线上问题,发现Producer日志里全是"org.apache.kafka.common.errors.TimeoutException",一看buffer.memory才16MB,max.block.ms=1000ms。高峰期流量一上来,缓冲区满了,消息发不出去直接超时丢弃。

我的经验:buffer.memory至少设成64MB,如果流量大可以到128MB或256MB。max.block.ms设成60000ms(60秒),给足等待时间。同时监控"buffer-available-percent"这个指标,如果经常低于20%,就该扩容了。

5. 总结一下排查思路

如果你怀疑Producer端丢消息,按这个顺序查:

  • 第一步:看acks。如果是0或1,先改成all试试。
  • 第二步:看retries。如果是0,改成3-5次。
  • 第三步:看request.timeout.ms。算一下够不够重试用。
  • 第四步:看buffer.memory和max.block.ms。有没有被撑爆。
  • 第五步:看Producer日志。有没有TimeoutException、NetworkException。

嗯,这三个参数配好了,Producer端丢消息的概率能降90%以上。剩下的10%,咱们后面章节再聊——比如幂等性、事务、Broker端丢消息那些事。