Kafka生产者异步发送消息产生堵塞
问题描述
使用KafkaTemplate作为生产者发送消息时为了不影响主流业务会采用异步发送的方式,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public void producerSendFuture(String topic, String data) { logger.info( "kafka异步发送topic:" + topic + "|requestMsg:" + data); ListenableFuture<SendResult<String, String>> future = this .kafkaTemplate.send(topic, data); future.addCallback( new ListenableFutureCallback<Object>() { public void onSuccess(Object o) { SendResult<String, String> sendResult = (SendResult)o; logger.info( "成功发送消息," offset: " + sendResult.getRecordMetadata().offset()); } public void onFailure(Throwable throwable) { throwable.printStackTrace(); logger.info( "发送kafka消息失败,异常原因:" + throwable.getMessage()); } }); logger.info( "return" ); } |
但是在实际使用时发现会出现如下日志
1 | kafka异步发送... |
1 | 发送kafka消息失败,异常原因... |
1 | return |
即说明在kafka发送时发生了同步堵塞,但是我们用的不是异步方法吗?
原因分析
Metadata
什么是 Metadata
Metadata 是指 Kafka 集群的元数据,包含了 Kafka 集群的各种信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | public class Metadata implements Closeable { private final Logger log; // retry.backoff.ms: 默认值为100ms,它用来设定两次重试之间的时间间隔,避免无效的频繁重试。 private final long refreshBackoffMs; // metadata.max.age.ms: 默认值为300000,如果在这个时间内元数据没有更新的话会被强制更新。 private final long metadataExpireMs; // 更新版本号,每更新成功1次,version自增1,主要是用于判断metadata是否更新 private int updateVersion; // 请求版本号,每发送一次请求,version自增1 private int requestVersion; // 上一次更新的时间(包含更新失败) private long lastRefreshMs; // 上一次更新成功的时间 private long lastSuccessfulRefreshMs; private KafkaException fatalException; // 非法的topics private Set<String> invalidTopics; // 未认证的topics private Set<String> unauthorizedTopics; // 元数据信息的Cache缓存 private MetadataCache cache = MetadataCache.empty(); private boolean needFullUpdate; private boolean needPartialUpdate; // 会收到metadata updates的Listener列表 private final ClusterResourceListeners clusterResourceListeners; private boolean isClosed; // 存储Partition最近一次的leaderEpoch private final Map<TopicPartition, Integer> lastSeenLeaderEpochs; } //MetadataCache:Kafka 集群中关于 node、topic 和 partition 的信息 public class MetadataCache { private final String clusterId; private final Map<Integer, Node> nodes; private final Set<String> unauthorizedTopics; private final Set<String> invalidTopics; private final Set<String> internalTopics; private final Node controller; private final Map<TopicPartition, PartitionMetadata> metadataByPartition; private Cluster clusterInstance; } //关于 topic 的详细信息(leader 所在节点、replica 所在节点、isr 列表)都是在 Cluster 实例中保存的 public final class Cluster { private final boolean isBootstrapConfigured; // node 列表 private final List<Node> nodes; // 未认证的topics private final Set<String> unauthorizedTopics; // 非法的topics private final Set<String> invalidTopics; // kafka内置的topics private final Set<String> internalTopics; private final Node controller; // partition对应的信息,如:leader所在节点、所有的副本、ISR中的副本、offline的副本 private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition; // topic和partition信息的对应关系 private final Map<String, List<PartitionInfo>> partitionsByTopic; // topic和可用partition(leader不为null)的对应关系 private final Map<String, List<PartitionInfo>> availablePartitionsByTopic; // node和partition信息的对应关系 private final Map<Integer, List<PartitionInfo>> partitionsByNode; // 节点id与节点的对应关系 private final Map<Integer, Node> nodesById; // 集群信息,里面只有一个clusterId private final ClusterResource clusterResource; } |
根据 Metadata 的主要数据结构,我们大概总结下包含哪些信息:
-
集群中有哪些节点;
-
集群中有哪些 topic,这些 topic 有哪些 partition;
-
每个 partition 的 leader 副本分配在哪个节点上,follower 副本分配在哪些节点上;
-
每个 partition 的 AR 有哪些副本,ISR 有哪些副本;
Metadata 的应用场景
Metadata 在 Kafka 中非常重要,很多场景中都需要从 Metadata 中获取数据或更新数据,例如:
-
KafkaProducer 发送一条消息到指定的 topic 中,需要知道分区的数量,要发送的目标分区,目标分区的 leader,leader 所在的节点地址等,这些信息都要从 Metadata 中获取
-
当 Kafka 集群中发生了 leader 选举,节点中 partition 或副本发生了变化等,这些场景都需要更新Metadata 中的数据
Producer 的 Metadata 更新流程
Producer 在调用 doSend() 方法时,第一步就是通过 waitOnMetadata 方法获取该 topic 的 metadata 信息
首先会从缓存中获取 cluster 信息,并从中获取 partition 信息,如果可以取到则返回当前的 cluster 信息,如果不含有所需要的 partition 信息时就会更新 metadata;
更新 metadata 的操作会在一个 do ....while 循环中进行,直到 metadata 中含有所需 partition 的信息,该循环中主要做了以下事情:
-
调用 metadata.requestUpdateForTopic() 方法来获取 updateVersion,即上一次更新成功时的 version,并将 needUpdate 设为 true,强制更新;
-
调用 sender.wakeup() 方法来唤醒 Sender 线程,Sender 线程中又会唤醒 NetworkClient 线程,在 NetworkClient 中会对 UpdateMetadataRequest 请求进行操作,待会下面会详细介绍;
-
调用 metadata.awaitUpdate(version, remainingWaitMs) 方法来等待 metadata 的更新,通过比较当前的 updateVersion 与步骤 1 中获取的 updateVersion 来判断是否更新成功
-
更新成功后唤醒主线程返回
在kafkaProducer初始化的时候,对metadata数据进行过update,不过这次更新只是将我们初始传入的集群节点更新到cluster字段中,在新建的clauster中添加了bootstrap的配置信息,并无任何原始参数信息
真正第一次获取metadata数据实在第一次发送数据的时候
Producer 的 Metadata 更新机制
强制更新
initConnect 方法调用时,初始化连接;
poll() 方法中对 handleDisconnections() 方法调用来处理连接断开的情况,这时会触发强制更新;
poll() 方法中对 handleTimedOutRequests() 来处理请求超时时;
发送消息时,如果无法找到 partition 的 leader;
处理 Producer 响应(handleProduceResponse),如果返回关于 Metadata 过期的异常,比如:没有 topic-partition 的相关 meta 或者 client 没有权限获取其 metadata
强制更新主要是用于处理各种异常情况
周期性更新
通过 Metadata 的 lastSuccessfulRefreshMs 和 metadataExpireMs 来实现,一般情况下,默认周期时间就是 metadataExpireMs,5 分钟时长
结论
Kafka生产者在发送消息前,要先获取到Metadata。对于异步发送,虽然消息发送的过程是非阻塞的,但获取Metadata的过程是阻塞的。如果因为Broker连接失败、topic未创建、kafka服务器不可达等原因而一直获取不到Metadata,主线程将长时间阻塞,循环获取直至超时
解决办法
1、指定max.block.ms,来限制获取Metadata的最大阻塞时间(默认60000ms)
1 |
1 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效