}
// poll for new data until the timeout expires
do { // @4
client.maybeTriggerWakeup(); //@5
if (includeMetadataInTimeout) { // @6
if (!updateAssignmentMetadataIfNeeded(timer)) {
return ConsumerRecords.empty();
}
} else {
while (!updateAssignmentMetadataIfNeeded(time.timer(Long.MAX_VALUE))) {
log.warn(“Still waiting for metadata”);
}
}
final Map<TopicPartition, List<ConsumerRecord<K, V>>> records = pollForFetches(timer); // @7
if (!records.isEmpty()) {
if (fetcher.sendFetches() > 0 || client.hasPendingRequests()) { // @8
client.pollNoWakeup();
}
return this.interceptors.onConsume(new ConsumerRecords<>(records)); // @9
}
} while (timer.notExpired());
return ConsumerRecords.empty();
} finally {
release();
}
}
代码@1:首先先对其参数含义进行讲解。
- boolean includeMetadataInTimeout
拉取消息的超时时间是否包含更新元数据的时间,默认为true,即包含。
代码@2:检查是否可以拉取消息,其主要判断依据如下:
-
KafkaConsumer 是否有其他线程再执行,如果有,则抛出异常,因为 - KafkaConsumer 是线程不安全的,同一时间只能一个线程执行。
-
KafkaConsumer 没有被关闭。
代码@3:如果当前消费者未订阅任何主题或者没有指定队列,则抛出错误,结束本次消息拉取。
代码@4:使用 do while 结构循环拉取消息,直到超时或拉取到消息。
代码@5:避免在禁止禁用wakeup时,有请求想唤醒时则抛出异常,例如在下面的@8时,会禁用wakeup。
代码@6:更新相关元数据,为真正向 broker 发送消息拉取请求做好准备,该方法将在下面详细介绍,现在先简单介绍其核心实现点:
-
如有必要,先向 broker 端拉取最新的订阅信息(包含消费组内的在线的消费客户端)。
-
执行已完成(异步提交)的 offset 提交请求的回调函数。
-
维护与 broker 端的心跳请求,确保不会被“踢出”消费组。
-
更新元信息。
-
如果是自动提交消费偏移量,则自动提交偏移量。
-
更新各个分区下次待拉取的偏移量。
这里会有一个更新元数据是否占用消息拉取的超时时间,默认为 true。
代码@7:调用 pollForFetches 向broker拉取消息,该方法将在下文详细介绍。
代码@8:如果拉取到的消息集合不为空,再返回该批消息之前,如果还有挤压的拉取请求,可以继续发送拉取请求,但此时会禁用warkup,主要的目的是用户在处理消息时,KafkaConsumer 还可以继续向broker 拉取消息。
代码@9:执行消费拦截器。
接下来对上文提到的代码@6、@7进行详细介绍。
1.1 KafkaConsumer updateAssignmentMetadataIfNeeded 详解
KafkaConsumer#updateAssignmentMetadataIfNeeded
boolean updateAssignmentMetadataIfNeeded(final Timer timer) {
if (coordinator != null && !coordinator.poll(timer)) { // @1
return false;
}
return updateFetchPositions(timer); // @2
}
要理解这个方法实现的用途,我们就必须依次对 coordinator.poll 方法与 updateFetchPositions 方法。
1.1.1 ConsumerCoordinator#poll
public boolean poll(Timer timer) {
invokeCompletedOffsetCommitCallbacks(); // @1
if (subscriptions.partitionsAutoAssigned()) { // @2
pollHeartbeat(timer.currentTimeMs()); // @21
if (coordinatorUnknown() && !ensureCoordinatorReady(timer)) { //@22
return false;
}
if (rejoinNeededOrPending()) { // @23
if (subscriptions.hasPatternSubscription()) { // @231
if (this.metadata.timeToAllowUpdate(time.milliseconds()) == 0) {
this.metadata.requestUpdate();
}
if (!client.ensureFreshMetadata(timer)) {
return false;
}
}
if (!ensureActiveGroup(timer)) { // @232
return false;
}
}
} else { // @3
if (metadata.updateRequested() && !client.hasReadyNodes(timer.currentTimeMs())) {
client.awaitMetadataUpdate(timer);
}
}
maybeAutoCommitOffsetsAsync(timer.currentTimeMs()); // @4
return true;
}
代码@1:执行已完成的 offset (消费进度)提交请求的回调函数。
代码@2:队列负载算法为自动分配(即 Kafka 根据消费者个数与分区书动态负载分区)的相关的处理逻辑。其实现关键点如下:
-
代码@21:更新发送心跳相关的时间,例如heartbeatTimer、sessionTimer、pollTimer 分别代表发送最新发送心跳的时间、会话最新活跃时间、最新拉取消息。
-
代码@22:如果不存在协调器或协调器已断开连接,则返回 false,结束本次拉取。如果协调器就绪,则继续往下走。
-
代码@23:判断是否需要触发重平衡,即消费组内的所有消费者重新分配topic中的分区信息,例如元数据发送变化,判断是否需要重新重平衡的关键点如下:
-
如果队列负载是通过用户指定的,则返回 false,表示无需重平衡。
-
如果队列是自动负载,topic 队列元数据发生了变化,则需要重平衡。
-
如果队列是自动负载,订阅关系发生了变化,则需要重平衡。
如果需要重重平衡,则同步更新元数据,此过程会阻塞。详细的重平衡将单独重点介绍,这里暂时不深入展开。
代码@3:用户手动为消费组指定负载的队列的相关处理逻辑,其实现关键如下:
- 如果需要更新元数据,并且还没有分区准备好,则同步阻塞等待元数据更新完毕。
代码@4:如果开启了自动提交消费进度,并且已到下一次提交时间,则提交。Kafka 消费者可以通过设置属性 enable.auto.commit 来开启自动提交,该参数默认为 true,则默认会每隔 5s 提交一次消费进度,提交间隔可以通过参数 auto.commit.interval.ms 设置。
接下来继续探讨 updateAssignmentMetadataIfNeeded (更新元数据)的第二个步骤,更新拉取位移。