Распределенное — очередь сообщений Kafka: метод представления смещения потребительского потребления Kafka

1. Автоматически отправлять данные о смещении потребления.

Самый простой метод отправки — позволить потребителю автоматически отправлять смещение и автоматически отправлять соответствующие параметры смещения:

  • Enable.auto.commit: включить ли функцию автоматического представления смещения, значение по умолчанию — true;
  • auto.commit.interval.ms: интервал времени для автоматической отправки смещений, значение по умолчанию — 5 секунд;

Если для параметра Enable.auto.commit установлено значение true, то каждые 5 секунд потребитель будет автоматически отправлять максимальное смещение, возвращаемое функцией poll(), которое представляет собой максимальное смещение сообщения в каждом извлеченном разделе. Интервал фиксации устанавливается в файле auto.commit.interval.ms, значение по умолчанию составляет 5 секунд. Как и другая обработка у потребителя, автоматическая отправка происходит в цикле опроса. Потребитель проверяет при каждом опросе, пришло ли время зафиксировать смещение, и если да, фиксирует смещение, возвращенное последним опросом.

① Запустите программу потребительского потребления и настройте ее на автоматическое представление данных о смещении потребителей:

public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-ni");

        // 显式配置消费者自动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);

        // 显式配置消费者自动提交位移的事件间隔
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,4);

        // 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);

        // 订阅主题
        consumer.subscribe(Arrays.asList("ni"));

        // 消费数据
        while (true){
    
    
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> record : consumerRecords) {
    
    
                System.out.printf("主题 = %s, 分区 = %d, 位移 = %d, " + "消息键 = %s, 消息值 = %s\n",
                        record.topic(), record.partition(), record.offset(), record.key(), record.value());
            }
        }
    }
}

② Запустите программу-производитель и отправьте 3 сообщения. Содержимое сообщений — hello и kafka.

③ Просмотр записей сообщений, использованных потребителями:

主题 = ni, 分区 = 0, 位移 = 0, 消息键 = null, 消息值 = hello,kafka
主题 = ni, 分区 = 0, 位移 = 1, 消息键 = null, 消息值 = hello,kafka
主题 = ni, 分区 = 0, 位移 = 2, 消息键 = null, 消息值 = hello,kafka

Можно видеть, что смещение последнего сообщения в потребительском разделе потребителя равно смещению = 2, то есть смещение сообщения потребителя равно смещению = 2;

④ Просмотрите объем, указанный потребителем:

[root@master01 kafka01]# bin/kafka-console-consumer.sh --bootstrap-server 10.65.132.2:9093 --topic __consumer_offsets --consumer.config config/consumer.properties --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning

[group-ni,ni,0]::OffsetAndMetadata(offset=3, leaderEpoch=Optional[0], metadata=, commitTimestamp=1692168114999, expireTimestamp=None)

Можно видеть, что смещение сообщения потребителя равно смещению = 2, но смещение отправки потребителя равно смещению = 3;

2. Каковы проблемы с автоматическим представлением данных о смещении потребления?

Предположим, вы только что отправили смещение потребления, а затем получили пакет сообщений для потребления. Прежде чем следующее смещение потребления будет автоматически отправлено, потребитель выйдет из строя. Затем вам придется перезапустить потребление с того места, где было отправлено последнее смещение. Таким образом, вы только что отправили смещение потребления, а затем получили пакет сообщений для потребления. , происходит явление повторного потребления (то же самое относится и к ребалансировке. После завершения ребалансировки потребитель, который принимает на себя управление разделом, будет читать сообщения, начиная с последнего зафиксированного смещения). Вы можете изменить интервал фиксации, чтобы чаще фиксировать смещения, и сузить временной интервал, что может привести к дублированию сообщений, но полностью избежать этого невозможно.

При использовании автоматической фиксации, когда пришло время зафиксировать смещение, метод опроса зафиксирует смещение, возвращенное предыдущим опросом, но он не знает, какие сообщения были обработаны. Поэтому, прежде чем снова вызывать poll(), убедитесь, что все сообщения, возвращенные последним poll(), были обработаны (вызов метода close() также автоматически зафиксирует смещение). Обычно это не проблема, но нужно быть осторожным при обработке исключений или досрочном выходе из цикла опроса.

Хотя автоматическая отправка удобна, она не позволяет разработчикам дублировать сообщения.

3. Вручную отправьте данные о смещении потребления.

Kafka также предоставляет метод ручного представления смещения, который позволяет разработчикам более гибко управлять и контролировать перемещение потребления. Во многих случаях это не означает, что потребление завершается после получения сообщения, а означает, что сообщение необходимо записать в базу данных, локальный кэш или выполнить более сложную бизнес-обработку. В этих сценариях сообщение должно считаться успешно использованным только после завершения всей бизнес-обработки.Метод отправки вручную позволяет разработчикам выполнить отправку смещения в соответствующем месте в соответствии с логикой программы.

Предварительным условием для включения функции ручной фиксации является то, что параметр клиентского клиента Enable.auto.commit настроен как false, что позволяет приложению решить, когда фиксировать смещение. Отправка вручную может быть разделена на синхронную отправку и асинхронную отправку, что соответствует двум типам методов commitSync() и commitAsync() в KafkaConsumer.

① Синхронная подача смещения означает, что потребитель будет блокировать отправку смещения до тех пор, пока подача не будет завершена и не будет получено подтверждение. Он зафиксирует последнее смещение, возвращенное функцией poll(), вернется сразу после успешного завершения фиксации и выдаст исключение, если фиксация по какой-либо причине не удалась. Метод commitAsync() имеет четыре различных перегруженных метода, которые определены следующим образом:

public void commitSync()
public void commitSync(Duration timeout)
public void commitSync(Map<TopicPartition, OffsetAndMetadata> offsets) 
public void commitSync(Map<TopicPartition, OffsetAndMetadata> offsets, Duration timeout) 

② Поток потребителя не будет заблокирован при выполнении асинхронной отправки смещения, и новая операция извлечения может начаться до того, как будет возвращен результат отправки смещения потребления. Асинхронная отправка может в определенной степени повысить производительность потребителей. Метод commitAsync имеет три различных перегруженных метода, которые определены следующим образом:

public void commitAsync() 
public void commitAsync(OffsetCommitCallback callback) 
public void commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback) 

1. Синхронное представление смещения потребления

В цикле потребления сообщений после обработки текущего пакета сообщений, прежде чем запрашивать дополнительные сообщения, вызовите метод commitSync(), чтобы отправить последнее смещение текущего пакета. Это заблокирует текущий поток до тех пор, пока отправка смещения не будет завершена. получить подтверждение. Пока не возникает неисправимых ошибок, метод commitSync() будет продолжать попытки до тех пор, пока фиксация не завершится успешно. Если отправка не удалась, исключение записывается в журнал ошибок.

public void commitSync()
@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

        // 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        // 订阅主题
        consumer.subscribe(Arrays.asList("topic-01"));
        // 消费数据
        while (true){
    
    
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> record : consumerRecords) {
    
    
                // 业务处理拉取的消息
            }
            try{
    
    
                // 消费者手动提交消费位移:同步提交方式
                consumer.commitSync();
            }catch (CommitFailedException exception){
    
    
                log.error("commit failed....");
            }
        }
    }
}

Вы также можете изменить потребительскую программу для пакетной обработки + пакетной отправки:

@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

        // 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        // 订阅主题
        consumer.subscribe(Arrays.asList("topic-01"));
        // 消费数据
        while (true){
    
    
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            int minSize = 200;
            List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
            for (ConsumerRecord<String, String> record : consumerRecords) {
    
    
                buffer.add(record);
            }
            try{
    
    
                // 消费者手动提交消费位移:同步提交方式
                if(buffer.size()>minSize){
    
    
                    // 批量处理消息
                    // ...
                }
                // 手动提交位移:同步方式
                consumer.commitSync();
            }catch (CommitFailedException exception){
    
    
                log.error("commit failed....");
            }
        }
    }
}

В приведенном выше примере извлеченные сообщения сохраняются в буфере кэша.Когда накапливается достаточно сообщений, то есть когда их больше или равно 200 сообщениям, выполняется соответствующая пакетная обработка, а затем выполняется пакетная отправка.

Метод commitSync() выполнит фиксацию на основе последнего смещения, полученного методом poll(). Пока не произойдет неисправимой ошибки, он заблокирует потребительский поток до завершения отправки смещения. Неустранимые ошибки, такие как CommitFailedException, WakeupException, InterruptException, AuthenticationException, AuthorizationException и т. д., мы можем перехватить и обработать соответствующим образом.

Следует отметить, что при синхронной отправке смещений вам необходимо обязательно выполнить фиксацию после обработки сообщения, поскольку commitSync() зафиксирует последнее смещение, возвращенное опросом(). Если вы вызываете commitSync() перед обработкой всех записей), там — это риск потери сообщений (сообщения были отправлены, но не обработаны) в случае сбоя приложения. Если приложение вылетает при обработке записей, но commitSync() еще не был вызван, все сообщения от начала самого последнего пакета до момента, когда произойдет ребалансировка, будут обработаны заново — это, наверное, лучше, чем потерять сообщения, а может и хуже.

2. Асинхронное представление смещения потребления

Недостатком синхронной отправки является то, что приложение будет блокироваться до тех пор, пока брокер не ответит на запрос, что ограничит пропускную способность приложения. Пропускную способность можно повысить за счет уменьшения частоты фиксации, но это увеличивает вероятность дублирования сообщений в случае повторной балансировки. В настоящее время вы можете использовать API асинхронной отправки. Просто отправьте запрос, не дожидаясь ответа брокера.

public void commitAsync() 
@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

        // 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        // 订阅主题
        consumer.subscribe(Arrays.asList("topic-01"));
        // 消费数据
        while (true){
    
    
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
    
    
                // 业务逻辑处理
            }
            // 异步提交消费位移
            consumer.commitAsync();
        }
    }
}

CommitSync() будет продолжать повторять попытки до тех пор, пока фиксация не будет успешной или не возникнет неисправимая ошибка, но commitAsync() этого не сделает. Это недостаток commitAsync(). Причина, по которой повторные попытки не выполняются, заключается в том, что когда commitAsync() получает ответ от сервера, возможно, произошло большее смещение и отправка прошла успешно. Предположим, мы выдаем запрос на отправку смещения 2000. В настоящее время возникла кратковременная проблема со связью. Сервер не может получить запрос и, естественно, не ответит. В то же время мы обработали еще одну партию сообщений и успешно отправили водоизмещение 3000. Если в это время commitAsync() попытается отправить сообщение со смещением 2000, возможно, что отправка будет успешной после смещения 3000. Если в это время произойдет ребалансировка, это приведет к дублированию сообщений.

Причина, по которой я упоминаю эту проблему и подчеркиваю важность порядка фиксации, заключается в том, что commitAsync() также поддерживает обратные вызовы, которые будут выполнены, когда брокер вернет ответ. Обратные вызовы часто используются для записи ошибок отправки смещений или создания индикаторов. Если вы хотите использовать их для повторной отправки смещений, вы должны обратить внимание на порядок отправки.

public void commitAsync(OffsetCommitCallback callback)
@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

        // 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        // 订阅主题
        consumer.subscribe(Arrays.asList("topic-01"));
        // 消费数据
        while (true){
    
    
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
    
    
                // 业务逻辑处理
            }
            // 异步提交消费位移
            consumer.commitAsync(new OffsetCommitCallback() {
    
    
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsetAndMetadataMap, Exception exception) {
    
    
                    if(exception!=null){
    
    
                        log.info("fail to commit offsets:{}",offsetAndMetadataMap,exception);
                    }
                }
            });
        }
    }
}

Как реализовать повторную попытку при асинхронной отправке: мы можем установить возрастающий порядковый номер, чтобы сохранить порядок асинхронной отправки, и увеличивать значение, соответствующее порядковому номеру, после каждой отправки смещения. Если вы столкнулись с ошибкой отправки смещения и вам необходимо повторить попытку, вы можете проверить размер отправленного смещения и порядковый номер. Если первый меньше второго, это означает, что было отправлено большее перемещение, и нет необходимости повторите попытку на этот раз. ; Если они совпадают, это означает, что отправку можно повторить.

3. Синхронная и асинхронная комбинированная подача смещения потребления.

В обычных обстоятельствах не составляет большой проблемы случайный сбой фиксации без повторной попытки, потому что, если неудача фиксации вызвана временной проблемой, последующие фиксации всегда будут успешными. Если потребитель выходит ненормально, то этой проблемы повторного потребления трудно избежать, потому что в этом случае смещение потребления не может быть отправлено вовремя; но если это последнее представление перед выключением или повторной балансировкой потребителя, убедитесь, что если подача успешно, вы можете использовать синхронную отправку в качестве окончательной проверки перед выходом или повторной балансировкой выполнения.

@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        consumer.subscribe(Arrays.asList("topic-01"));

        try {
    
    
            while (true) {
    
    
                ConsumerRecords<String, String> records = consumer.poll( Duration.ofMillis(100));
                for (ConsumerRecord<String, String> record : records) {
    
    
					// 业务逻辑处理
                }
                // 异步提交位移
                consumer.commitAsync();
            }
        } catch (Exception e) {
    
    
            log.error("Unexpected error", e);
        } finally {
    
    
            try {
    
    
                // 同步提交位移
                consumer.commitSync();
            }finally{
    
    
                consumer.close();
            }
        }
    }
}

4. Укажите конкретное смещение потребления.

Для метода без параметров с использованием commitSync() частота отправки смещений потребления такая же, как частота получения и обработки пакетных сообщений. Но что, если вы хотите совершать смещения чаще? Если poll() возвращает большой пакет данных, что нам следует делать, если мы хотим зафиксировать смещения во время пакетной обработки, чтобы избежать дублирования сообщений, которое может быть вызвано ребалансировкой? В настоящее время вы не можете просто вызвать commitSync() или commitAsync(), поскольку они фиксируют только последнее смещение в пакете сообщений.

К счастью, потребительский API позволяет вызовам commitSync() и commitAsync() передавать раздел и смещение, которые они хотят зафиксировать:

public void commitSync(Map<TopicPartition, OffsetAndMetadata> offsets)
public void commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback)

Вставьте сюда описание изображения

Как показано на рисунке: смещение представления потребителя = максимальное смещение смещения сообщения раздела, полученного текущим опросом + 1, это смещение представления будет в следующий раз.

@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        consumer.subscribe(Arrays.asList("topic-01"));

        ConcurrentHashMap<TopicPartition,OffsetAndMetadata> offsets = new ConcurrentHashMap<>();
        int count = 0;
        while (true) {
    
    
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
            for (ConsumerRecord<String, String> record : records) {
    
    
                // 消息所属的主题和分区
                TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition());
                // 消费者提交的消费位移=当前消费消息的位移+1
                OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(record.offset() + 1);
                offsets.put(topicPartition, offsetAndMetadata);
                if(count % 1000 == 0){
    
    
                    consumer.commitAsync(offsets,null);
                }
                count++;
            }
        }
    }
}

5. Укажите смещение потребления по разделам.

@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        consumer.subscribe(Arrays.asList("topic-01"));

        while (true){
    
    
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            // 获取拉取的消息包含的所有分区列表
            Set<TopicPartition> partitions = consumerRecords.partitions();
            for (TopicPartition partition : partitions) {
    
    
                // 获取当前分区要消费的消息
                List<ConsumerRecord<String, String>> partitionRecords = consumerRecords.records(partition);
                // 获取当前分区消息的最大位移
                long lastConsumerOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
                // 当前分区的消费位移提交 = 当前分区消息的最大位移 + 1
                Map<TopicPartition, OffsetAndMetadata> topicPartitionOffsetAndMetadataMap = Collections.singletonMap(partition, new OffsetAndMetadata(lastConsumerOffset + 1));
                consumer.commitSync(topicPartitionOffsetAndMetadataMap);
            }
        }
    }
}

4. Что следует делать потребителям, если они не могут найти замену потреблению?

Когда создается новая группа потребителей, ей не приходится искать смещения в потреблении. Или новый потребитель в группе потребителей подписывается на новую тему, и у него нет смещения потребления, которое можно было бы найти. Когда срок действия информации о смещении об этой группе потребителей в теме __consumer_offsets истекает и она удаляется, у нее нет смещения потребления, которое можно было бы искать. Что делать, если в Kafka нет начального смещения или текущего смещения больше нет на сервере?

В настоящее время место начала потребления будет определяться на основе конфигурации параметра клиента-потребителя auto.offset.reset. Значение параметра auto.offset.reset следующее:

  • последнее (значение по умолчанию): указывает, что сообщения обрабатываются, начиная с конца раздела.
  • самый ранний: указывает, что потребитель начнет потреблять с самого начала, равного 0.
  • none: если смещение потребления не обнаружено, потребление не начнется ни с позиции последнего сообщения, ни с позиции самого раннего сообщения. В этом случае будет сообщено об исключении NoOffsetForPartitionException. Если смещение потребления можно найти, настройка его на «нет» не вызовет каких-либо исключений.

Если конфигурация не является «самой последней», «самой ранней» и «нет», будет сообщено об исключении ConfigException.

Параметр auto.offset.reset используется для указания того, где потребитель должен начать получать сообщения, если он не может найти смещение потребления при запуске. Если смещение потребления можно найти, то потребитель начнет получать сообщения из этого смещения, поэтому параметр auto.offset.reset не вступит в силу и вступит в силу только тогда, когда смещение потребления не может быть найдено. Если смещение выходит за пределы, то есть смещение потребления превышает количество или диапазон позиций сообщений в очереди сообщений, параметр auto.offset.reset также вступит в силу.

5. Как читать сообщения с определенного смещения раздела?

Если потребитель может найти смещение потребления, poll() может использоваться для чтения сообщений из последнего смещения каждого раздела, а предоставленный параметр auto.offset.reset также может быть грубым, если смещение потребления не может быть найдено или смещение выходит за пределы. Начинайте потреблять с начала или с конца. Но иногда нам нужен более детальный элемент управления, который позволяет нам начинать извлекать сообщения с определенного смещения, и метод search() в KafkaConsumer предоставляет именно эту функцию, позволяя нам использовать вперед или назад.

public void seek(TopicPartition partition, long offset)
public void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata)

① Параметр раздела в методе Seek() представляет раздел, а параметр смещения используется для указания местоположения в разделе, с которого нужно начать использование. Метод search() может только сбрасывать позицию потребления раздела, назначенного потребителем, а выделение раздела реализуется во время вызова метода poll(). То есть метод poll() необходимо выполнить один раз перед выполнением метода search(), а позиция потребления может быть сброшена только после выделения раздела:

@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        consumer.subscribe(Arrays.asList("topic-01"));

        // 执行一次poll() 方法完成分区分配的逻辑
        //  ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(0));
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
        Set<TopicPartition> topicPartitions = consumer.assignment();
        for (TopicPartition topicPartition : topicPartitions) {
    
    
            consumer.seek(topicPartition,10);
        }

        while (true) {
    
    
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(1000));
            // ...
        }
    }
}

② Если параметр в методе poll() равен 0 и этот метод возвращает значение немедленно, то логика распределения разделов внутри метода poll() будет реализована слишком поздно. раздел в настоящее время, поэтому раздел Разделы Это пустой список. Итак, какова здесь подходящая настройка параметра тайм-аута? Если оно слишком короткое, операция выделения раздела завершится неудачей. Если оно слишком длинное, это может привести к ненужному ожиданию. Мы можем использовать метод присваивания() KafkaConsumer, чтобы определить, выделен ли соответствующий раздел:

@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        consumer.subscribe(Arrays.asList("topic-01"));

        Set<TopicPartition> topicPartitions = consumer.assignment();
        // 此时说明还未完成分区分配
        while (topicPartitions.size()==0){
    
    
            consumer.poll(Duration.ofMillis(100));
            topicPartitions = consumer.assignment();
        }
        for (TopicPartition topicPartition : topicPartitions) {
    
    
            // 重置每个分区的消费位置为10
            consumer.seek(topicPartition,10);
        }

        while (true) {
    
    
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(1000));
            // 消费消息
        }
    }
}

③ Если метод search() выполняется в нераспределенном разделе, будет сообщено об исключении IllegalStateException. Аналогично вызову метода seek() непосредственно после вызова метода subscribe():

@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        consumer.subscribe(Arrays.asList("topic-01"));


        // 未完成分区分配,直接调用seek方法,重置分区1的消费位置为10
        consumer.seek(new TopicPartition("topic-01",1),10);

        while (true) {
    
    
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(1000));
            // 消费消息
        }
    }
}

Сообщается об ошибке:

Exception in thread "main" java.lang.IllegalStateException: No current assignment for partition topic-01-1

④ Если потребитель в группе потребителей может найти смещение потребления при запуске, то потребитель начнет потреблять сообщения из этого смещения. Если смещение не выходит за пределы, то есть смещение потребления не превышает количество или диапазон позиций сообщений в очереди сообщений, параметр auto.offset.reset не будет работать. В настоящее время, если вы хотите указать потребление с самого начала или конец, вам нужен метод seek(). В помощь укажите потребление с конца раздела:

Метод endOffsets() используется для получения позиции сообщения в конце указанного раздела. Конкретный метод endOffsets определяется следующим образом:

public Map<TopicPartition, Long> endOffsets(Collection<TopicPartition> partitions)
public Map<TopicPartition, Long> endOffsets(Collection<TopicPartition> partitions, Duration timeout)

Параметр разделов представляет набор разделов, а параметр таймаута используется для установки периода ожидания ожидания получения данных. Если значение параметра timeout не указано, время ожидания метода endOffsets() задается клиентским параметром request.timeout.ms, а значение по умолчанию — 30000. Соответствующим методу endOffsets является метод BeginOffset(). Начальная позиция раздела сначала равна 0, но это не означает, что она всегда равна 0. Поскольку действие очистки журнала очищает старые данные, начальная позиция раздела будет Естественно, конкретное определение метода BeginOffsets() выглядит следующим образом:

public Map<TopicPartition, Long> beginningOffsets(Collection<TopicPartition> partitions) 
public Map<TopicPartition, Long> beginningOffsets(Collection<TopicPartition> partitions, Duration timeout)

Содержимое и значение параметров в методе BeginOffsets() такое же, как и в методе endOffsets(). С помощью этих двух методов мы можем начать использование с начала или конца раздела. Фактически, KafkaConsumer напрямую предоставляет методы SeeToBeginning() и SeeToEnd() для реализации этих двух функций. Конкретные определения этих двух методов следующие:

public void seekToBeginning(Collection<TopicPartition> partitions)
public void seekToEnd(Collection<TopicPartition> partitions)

⑤ Иногда мы не знаем конкретного места потребления, но знаем соответствующий момент времени. Например, мы хотим получать новости после 8 часов вчера. Это требование больше соответствует нормальной логике мышления. В настоящее время мы не можем напрямую использовать метод search() для отслеживания соответствующего местоположения. KafkaConsumer также учитывает эту ситуацию и предоставляет метод offsetsForTimes() для запроса местоположения соответствующего раздела через временную метку:

public Map<TopicPartition, OffsetAndTimestamp> offsetsForTimes(Map<TopicPartition, Long> timestampsToSearch)
public Map<TopicPartition, OffsetAndTimestamp> offsetsForTimes(Map<TopicPartition, Long> timestampsToSearch, Duration timeout)

Параметр timestampsToSearch метода offsetsForTimes() имеет тип карты, ключ — это запрашиваемый раздел, а значение — запрашиваемая метка времени. Этот метод возвращает позицию и метку времени, соответствующие первому сообщению с меткой времени, большей чем или равно запрашиваемому времени. , что соответствует полям смещения и метки времени в OffsetAndTimestamp. В следующем примере показано использование offsetsForTimes() и search(). Сначала метод offsetsForTimes() используется для получения позиции сообщения день назад, а затем метод Seek() используется для возврата к соответствующей позиции и начать потреблять:

@Slf4j
public class CustomConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.65.132.2:9093");
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"group-topic-01");
        // 显式配置消费者手动提交位移
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        consumer.subscribe(Arrays.asList("topic-01"));

        Map<TopicPartition,Long> timestampToSearch = new HashMap<>();
        Set<TopicPartition> topicPartitionSet = consumer.assignment();
        // 查询的分区以及查询的时间戳
        for (TopicPartition topicPartition : topicPartitionSet) {
    
    
            timestampToSearch.put(topicPartition,System.currentTimeMillis()-1*24*3600*1000);
        }

        // 获取时间戳大于等于待查询时间的第一条消息对应的位置和时间戳
        Map<TopicPartition, OffsetAndTimestamp> topicPartitionOffsetAndTimestampMap = consumer.offsetsForTimes(timestampToSearch);
        for (TopicPartition topicPartition : topicPartitionSet) {
    
    
            OffsetAndTimestamp offsetAndTimestamp = topicPartitionOffsetAndTimestampMap.get(topicPartition);
            // seek 方法重置消费的位移
            if(offsetAndTimestamp != null){
    
    
                consumer.seek(topicPartition,offsetAndTimestamp.offset());
            }
        }
    }
}

⑥ Смещение за пределы границ также приводит к выполнению параметра auto.offset.reset. Смещение за пределы границ означает, что позиция потребления известна, но не может быть найдена в реальном разделе. Например, исходная позиция выборки. равно 101 (смещение выборки 101), но оно вышло за пределы (вне диапазона), поэтому в этот момент позиция извлечения будет сброшена (сброс смещения) на 100 в соответствии со значением по умолчанию параметра auto.offset.reset. Мы также можем знать, что максимальное смещение сообщения в разделе в данный момент равно 99.

6. Как корректно выйти из цикла опроса?

Как корректно выйти из цикла опроса Если вы уверены, что хотите закрыть потребителя в ближайшее время (даже если потребитель все еще ждет возврата poll()), вы можете вызвать Consumer.wakeup() в другом потоке. Если цикл опроса выполняется в основном потоке, этот метод можно вызвать в ShutdownHook. Следует отметить, что Consumer.wakeup() — единственный метод, который потребитель может безопасно вызывать из других потоков. Вызов Consumer.wakeup() приведет к тому, что poll() выдаст исключение WakeupException. Если поток не выполняет опрос при вызове Consumer.wakeup(), исключение будет выброшено при следующем вызове poll(). WakeupException не обязательно обрабатывать, но необходимо вызвать метод Consumer.close() перед выходом из потока. Когда потребитель отключается, он фиксирует незафиксированные смещения и отправляет сообщение координатору потребителя, чтобы сообщить ему, что он покидает группу. Координатор немедленно инициирует перебалансировку, а разделы, принадлежащие выключенному потребителю, будут перераспределены другим потребителям в группе, не дожидаясь истечения времени ожидания сеанса.

おすすめ

転載: blog.csdn.net/qq_42764468/article/details/132358750
おすすめ