初学kafka:一次kafka消费者poll方法的疑问探索,提交位移?消费位移?

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,并且保存在本地内存,后续再次拉取数据也会携带这个信息,拉取到数据后进行更新。

若有错误,还望批评指正

相关推荐
计算机学姐1 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis
程序员陆通2 小时前
Spring Boot RESTful API开发教程
spring boot·后端·restful
无理 Java2 小时前
【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)
java·后端·spring·面试·mvc·框架·springmvc
cyz1410013 小时前
vue3+vite@4+ts+elementplus创建项目详解
开发语言·后端·rust
liuxin334455663 小时前
大学生就业招聘:Spring Boot系统的高效实现
spring boot·后端·mfc
向上的车轮4 小时前
ASP.NET Zero 多租户介绍
后端·asp.net·saas·多租户
灰色孤星A4 小时前
Kafka学习笔记(三)Kafka分区和副本机制、自定义分区、消费者指定分区
zookeeper·kafka·kafka分区机制·kafka副本机制·kafka自定义分区
yz_518 Nemo4 小时前
django的路由分发
后端·python·django
肖哥弹架构4 小时前
Spring 全家桶使用教程
java·后端·程序员
AIRust编程之星5 小时前
Rust中的远程过程调用实现与实践
后端