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

若有错误,还望批评指正

相关推荐
Piper蛋窝5 分钟前
我所理解的 Go 的 `panic` / `defer` / `recover` 异常处理机制
后端·go
clk660742 分钟前
Spring Boot
java·spring boot·后端
皮皮高1 小时前
itvbox绿豆影视tvbox手机版影视APP源码分享搭建教程
android·前端·后端·开源·tv
弱冠少年1 小时前
golang入门
开发语言·后端·golang
Humbunklung2 小时前
Rust 函数
开发语言·后端·rust
喜欢踢足球的老罗2 小时前
在Spring Boot 3.3中使用Druid数据源及其监控功能
java·spring boot·后端·druid
jakeswang2 小时前
StarRocks
后端·架构
Dnui_King2 小时前
Kafka 入门指南与一键部署
分布式·kafka
龙云飞谷2 小时前
从原理到调参,小白也能读懂的大模型微调算法Lora
后端
荣江2 小时前
【实战】基于 Tauri 和 Rust 实现基于无头浏览器的高可用网页抓取
后端·rust