1、问题
在kafka中,我们在使用java程序消费者进行消费时,一般都是循环调用poll方法进行拉取数据,在与springboot整合过的消费者也是如此。不知道大家在初学kafka时,会不会有这样的疑问,位移提交设置为手动提交,中间没有生产者生产数据,一直在循环调用poll方法但不进行位移提交,第一次拉取到从上次位移提交未消费的数据,但是再进行循环也是这个poll方法为什么就拉取不到这批数据了呢?重新启动消费者同样也是如此。
java
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "demo02");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
consumer.subscribe(Arrays.asList("test01"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(2000L));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到的消息:partition= %d,offset= %d,key= %s,value=%s %n", record.partition(),
record.offset(), record.key(), record.value());
}
}
根据当前问题场景,每次重新启动消费者后会从上次提交位移处拉取数据,并且拉取后不会重复拉取,基本也能猜到kafka消费者应该本地内存中存了自己当前的消费位移
,消费者每次请求拉取数据都会传当前的消费位移信息,每次消费者重启后本地存的消费位移也就消失了,启动之后更新为从broker获取的提交位移。
2、猜想验证
利用idea调试功能在拉取到数据前后consumer对象进行了对比。
当前主题中有1条消息未消费,已提交位移为33,拉取到数据前 拉取到数据后 发现了一个看上去很能说明问题的变化,拉取到数据后consumer对象的订阅属性subscriptions中有了明显的变化,多了订阅的主题及分区信息"testo01-0"和位移offset=34,(此时已拉取到offset为33的数据
)获取到这些信息之后,大致猜想到offset应该就是消费者本地的消费位移(并非提交位移),并且每次向broker拉取数据时都会携带这个参数
;直接对该属性进行修改验证
java
int i = 0;
boolean key = false;
while (true) {
System.out.println("第" + i++ + "次循环");
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(2000L));
for (ConsumerRecord<String, String> record : records) {
System.err.printf("收到的消息:partition= %d,offset= %d,key= %s,value=%s %n", record.partition(),
record.offset(), record.key(), record.value());
key = true;
}
if (key){
Field subscriptions = consumer.getClass().getDeclaredField("subscriptions");
subscriptions.setAccessible(true);
SubscriptionState o = (SubscriptionState) subscriptions.get(consumer);
System.out.println("subscriptions 对象:" + o);
Field assignment = o.getClass().getDeclaredField("assignment");
assignment.setAccessible(true);
PartitionStates states = (PartitionStates) assignment.get(o);
List list = states.partitionStateValues();
Object o1 = list.get(0);
Field position = o1.getClass().getDeclaredField("position");
position.setAccessible(true);
SubscriptionState.FetchPosition o2 = (SubscriptionState.FetchPosition) position.get(o1);
Field offset = o2.getClass().getDeclaredField("offset");
offset.setAccessible(true);
// 每次拉取到数据后都重新设置为33,即要下次拉取的数据从33开始
offset.set(o2,33L);
}
}
一直在重复拉取offset为33的数据,至此基本验证了之前的猜想;另外kafka消费者客户端对这部分代码封装的挺好的,想从外部进行修改只能通过反射。
3、poll方法探索
kafka消费者在启动后,会先与broker经过一系列的交互再去拉取数据进行消费 注:图片来源于blog.csdn.net/DraGon_HooR...
在经历过以上步骤之后,消费者客户端会向broker发送一个apiKey=OFFSET_FETCH
的请求,大致意思是获取当前消费者在主题分区的上次提交位移,获取到最新的提交位移之后,消费者会将主题分区与位移的对应信息保存在本地内存中。
ini
// 请求
RequestHeader(apiKey=OFFSET_FETCH, apiVersion=8, clientId=consumer-demo02-1, correlationId=7)
// 响应 committedOffset=33 消费者已经成功消费了分区中序号为 0 到 32 的消息
OffsetFetchResponseData(throttleTimeMs=0, topics=[], errorCode=0,
groups=[OffsetFetchResponseGroup(groupId='demo02',
topics=[OffsetFetchResponseTopics(name='test01',
partitions=[OffsetFetchResponsePartitions(partitionIndex=0,
committedOffset=33, committedLeaderEpoch=10,
metadata='', errorCode=0)])], errorCode=0)])
这段代码会在每次消费者拉取到数据后执行
java
private Fetch<K, V> fetchRecords(CompletedFetch completedFetch, int maxRecords) {
// 已省略无关代码。。。
// 本次从borker拉取的数据
List<ConsumerRecord<K, V>> partRecords = completedFetch.fetchRecords(maxRecords);
log.trace("Returning {} fetched records at offset {} for assigned partition {}",
partRecords.size(), position, completedFetch.partition);
boolean positionAdvanced = false;
// position.offset:当前的消费位移 completedFetch.nextFetchOffset:消费过这批数据之后的位移
// 例如:当前消费位移position.offset为33,partRecords中有10条数据是本次要消费的,completedFetch.nextFetchOffset为43
if (completedFetch.nextFetchOffset > position.offset) {
// 重新构建当前消费者对应主题分区的消费位移信息
FetchPosition nextPosition = new FetchPosition(
completedFetch.nextFetchOffset,
completedFetch.lastEpoch,
position.currentLeader);
log.trace("Updating fetch position from {} to {} for partition {} and returning {} records from `poll()`",
position, nextPosition, completedFetch.partition, partRecords.size());
// 重新设置当前消费者对应主题分区的消费位移信息 设置为43
subscriptions.position(completedFetch.partition, nextPosition);
positionAdvanced = true;
}
return Fetch.forPartition(completedFetch.partition, partRecords, positionAdvanced);
}
在上述代码中,每当消费者拉取到要消费的数据时,会先根据主题分区信息获取消费位移,主题分区与当前消费者的位移信息用LinkedHashMap
存储,再次重置当前消费者的消费位移信息。
4、总结
个人名词理解:
提交位移
:保存在kafka broker中,消费者消费消息进行commit提交到broker之后,更新提交位移,消费者重启后会从上次的提交位移处拉取数据。
消费位移
:保存在消费者本地内存中,首次启动会从broker获取提交位移更新为本地的消费位移,后续每次从broker拉取消息后都会更新消费位移(即使一直消费不提交),消费者重启后又随之更新为提交位移。
kafka消费者每次启动时会向broker发送请求同步当前主题分区的提交位移信息offset,并且保存在本地内存,后续再次拉取数据也会携带这个信息,拉取到数据后进行更新。
若有错误,还望批评指正