Distribuido - Message Queue Kafka: método de envío del desplazamiento del consumo del consumidor de Kafka

1. Enviar automáticamente el desplazamiento de consumo.

El método de envío más simple es permitir que el consumidor envíe automáticamente la compensación y envíe automáticamente los parámetros relevantes de la compensación:

  • enable.auto.commit: si se habilita la función de envío de compensación automática, el valor predeterminado es verdadero;
  • auto.commit.interval.ms: el intervalo de tiempo para enviar compensaciones automáticamente, el valor predeterminado es 5 segundos;

Si enable.auto.commit se establece en verdadero, cada 5 segundos, el consumidor enviará automáticamente el desplazamiento máximo devuelto por poll(), que es el desplazamiento máximo de mensaje en cada partición extraída. El intervalo de confirmación lo establece auto.commit.interval.ms y el valor predeterminado es 5 segundos. Al igual que otros procesos en el consumidor, el envío automático se produce en un ciclo de sondeo. El consumidor comprueba en cada encuesta si es momento de confirmar una compensación y, de ser así, confirma la compensación devuelta por la última encuesta.

① Inicie el programa de consumo del consumidor y configúrelo para que envíe automáticamente el desplazamiento del consumidor:

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());
            }
        }
    }
}

② Inicie el programa productor y envíe 3 mensajes, el contenido de los mensajes es hola y kafka.

③ Ver los registros de mensajes consumidos por los consumidores:

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

Se puede ver que el desplazamiento del último mensaje en la partición de consumo del consumidor es offset = 2, es decir, el desplazamiento del mensaje del consumidor es offset = 2;

④ Ver el desplazamiento presentado por el consumidor:

[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)

Se puede ver que el desplazamiento del mensaje del consumidor tiene un desplazamiento = 2, pero el desplazamiento del envío del consumidor tiene un desplazamiento = 3;

2. ¿Cuáles son los problemas al presentar automáticamente el desplazamiento del consumo?

Supongamos que acaba de enviar un desplazamiento de consumo y luego extrajo un lote de mensajes para el consumo. Antes de que se envíe automáticamente el siguiente desplazamiento de consumo, el consumidor falla. Luego debe reiniciar el consumo desde el lugar donde se envió el último desplazamiento. De esta manera , se produce un consumo repetido (lo mismo se aplica al reequilibrio. Una vez completado el reequilibrio, el consumidor que se hace cargo de la partición leerá los mensajes a partir del último desplazamiento confirmado). Puede modificar el intervalo de confirmación para confirmar las compensaciones con mayor frecuencia y reducir la ventana de tiempo que puede generar mensajes duplicados, pero no puede evitarlo por completo.

Cuando se utiliza la confirmación automática, cuando llega el momento de confirmar el desplazamiento, el método de sondeo confirmará el desplazamiento devuelto por la encuesta anterior, pero no sabe qué mensajes se han procesado. Por lo tanto, antes de volver a llamar a poll(), asegúrese de que todos los mensajes devueltos por el último poll() hayan sido procesados ​​(llamar al método close() también confirmará automáticamente el desplazamiento). Normalmente esto no es un problema, pero debe tener cuidado al manejar excepciones o salir temprano del ciclo de sondeo.

Aunque el envío automático es conveniente, no deja margen para evitar que los desarrolladores dupliquen mensajes.

3. Enviar manualmente el desplazamiento de consumo.

Kafka también proporciona un método de envío de desplazamiento manual, que permite a los desarrolladores tener una gestión y control más flexibles sobre el desplazamiento del consumo. En muchos casos, esto no significa que el consumo se complete después de extraer el mensaje, sino que el mensaje debe escribirse en la base de datos, en el caché local o en un procesamiento comercial más complejo. En estos escenarios, el mensaje debe considerarse consumido exitosamente solo después de que se complete todo el procesamiento comercial. El método de envío manual permite a los desarrolladores realizar el envío por desplazamiento en el lugar apropiado de acuerdo con la lógica del programa.

El requisito previo para habilitar la función de confirmación manual es que el parámetro del cliente consumidor enable.auto.commit esté configurado como falso, lo que permite a la aplicación decidir cuándo confirmar la compensación. El envío manual se puede subdividir en envío sincrónico y envío asincrónico, correspondientes a los dos tipos de métodos commitSync () y commitAsync () en KafkaConsumer.

① Envío sincrónico de desplazamiento significa que el consumidor bloqueará el envío del desplazamiento hasta que se complete el envío y se reciba la confirmación. Confirmará el último desplazamiento devuelto por poll(), regresará inmediatamente después de que la confirmación sea exitosa y generará una excepción si la confirmación falla por algún motivo. El método commitAsync() tiene cuatro métodos sobrecargados diferentes, que se definen específicamente de la siguiente manera:

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

② El hilo del consumidor no se bloqueará cuando se ejecute el envío asincrónico del desplazamiento, y puede comenzar una nueva operación de extracción antes de que se devuelva el resultado del envío del desplazamiento del consumo. El envío asincrónico puede mejorar el rendimiento del consumidor hasta cierto punto. El método commitAsync tiene tres métodos sobrecargados diferentes, que se definen específicamente de la siguiente manera:

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

1. Enviar desplazamiento de consumo de forma sincrónica.

En el ciclo de consumo de mensajes, después de procesar el lote actual de mensajes, antes de sondear para obtener más mensajes, llame al método commitSync() para enviar el último desplazamiento del lote actual. Esto bloqueará el hilo actual hasta que se complete el envío del desplazamiento. recibir confirmación. Mientras no se produzcan errores irrecuperables, el método commitSync() seguirá intentándolo hasta que la confirmación se realice correctamente. Si el envío falla, la excepción se registra en el registro de errores.

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....");
            }
        }
    }
}

También puede modificar el programa del consumidor para procesamiento por lotes + envío por lotes:

@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....");
            }
        }
    }
}

En el ejemplo anterior, los mensajes extraídos se almacenan en el búfer de caché. Cuando se acumulan suficientes mensajes, es decir, cuando hay más de 200 mensajes, se realiza el procesamiento por lotes correspondiente y luego se realiza el envío por lotes.

El método commitSync() se confirmará en función del último desplazamiento obtenido por el método poll(). Siempre que no se produzca un error irrecuperable, bloqueará el hilo del consumidor hasta que se complete el envío del desplazamiento. Para errores irrecuperables, como CommitFailedException, WakeupException, InterruptException, AuthenticationException, AuthorizationException, etc., podemos capturarlos y manejarlos en consecuencia.

Cabe señalar que al enviar compensaciones sincrónicamente, debe asegurarse de confirmar después de procesar el mensaje, porque commitSync() confirmará la última compensación devuelta por poll(). Si llama a commitSync() antes de procesar todos los registros), habrá Existe el riesgo de perder mensajes (los mensajes se enviaron pero no se procesaron) en caso de que la aplicación falle. Si la aplicación falla mientras procesa registros, pero aún no se ha llamado a commitSync(), todos los mensajes desde el inicio del lote más reciente hasta el momento en que se produce el reequilibrio se procesarán nuevamente; esto probablemente sea mejor que perder mensajes, tal vez peor.

2. Enviar asincrónicamente el desplazamiento del consumo.

Una desventaja del envío sincrónico es que la aplicación se bloqueará hasta que el corredor responda a la solicitud, lo que limitará el rendimiento de la aplicación. El rendimiento se puede mejorar reduciendo la frecuencia de confirmación, pero esto aumenta el potencial de duplicación de mensajes si se produce un reequilibrio. En este momento, puede utilizar la API de envío asincrónico. Simplemente envíe la solicitud sin esperar a que responda el corredor.

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() seguirá intentándolo hasta que la confirmación tenga éxito o encuentre un error irrecuperable, pero commitAsync() no lo hará. Esta es una deficiencia de commitAsync(). La razón por la que no se realizan reintentos es porque cuando commitAsync() recibe la respuesta del servidor, es posible que haya habido un desplazamiento mayor y el envío fue exitoso. Supongamos que enviamos una solicitud para enviar un desplazamiento de 2000. En este momento, hay un problema de comunicación a corto plazo: el servidor no puede recibir la solicitud y, naturalmente, no responderá. Al mismo tiempo, procesamos otro lote de mensajes y enviamos con éxito el desplazamiento 3000. Si commitAsync() vuelve a intentar enviar con un desplazamiento de 2000 en este momento, es posible que el envío se realice correctamente después de un desplazamiento de 3000. Si se produce un reequilibrio en este momento, se producirá la duplicación de mensajes.

La razón por la que menciono este problema y enfatizo la importancia de la orden de confirmación es que commitAsync() también admite devoluciones de llamada, que se ejecutarán cuando el corredor devuelva una respuesta. Las devoluciones de llamada se utilizan a menudo para registrar errores de envío de desplazamientos o generar indicadores. Si desea utilizarlas para volver a intentar enviar desplazamientos, debe prestar atención al orden de envío.

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);
                    }
                }
            });
        }
    }
}

Cómo implementar el reintento en el envío asincrónico: podemos establecer un número de secuencia creciente para mantener el orden del envío asincrónico y aumentar el valor correspondiente al número de secuencia después de cada envío de desplazamiento. Cuando encuentre una falla en el envío de desplazamiento y necesite volver a intentarlo, puede verificar el tamaño del desplazamiento enviado y el número de secuencia. Si el primero es más pequeño que el segundo, significa que se ha enviado un desplazamiento mayor y no es necesario Vuelva a intentarlo esta vez.; Si los dos son iguales, significa que se puede volver a intentar el envío.

3. Presentación combinada síncrona y asincrónica del desplazamiento del consumo.

En circunstancias normales, no es un gran problema fallar ocasionalmente en una confirmación sin volver a intentarlo, porque si la falla de la confirmación se debe a un problema temporal, las confirmaciones posteriores siempre tendrán éxito. Si el consumidor sale de manera anormal, entonces este problema de consumo repetido es difícil de evitar, porque en este caso el desplazamiento del consumo no se puede enviar a tiempo; pero si este es el último envío antes de que el consumidor se apague o reequilibre, asegúrese de que Si el envío tiene éxito, puede utilizar el envío sincrónico como verificación final antes de salir o reequilibrar la ejecución.

@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. Presentar desplazamiento de consumo específico

Para el método sin parámetros que utiliza commitSync(), la frecuencia de envío de desplazamientos de consumo es la misma que la frecuencia de extracción y procesamiento de mensajes por lotes. ¿Pero qué pasa si quieres cometer desplazamientos con mayor frecuencia? Si poll() devuelve un gran lote de datos, ¿qué debemos hacer si queremos confirmar desplazamientos durante el procesamiento por lotes para evitar la duplicación de mensajes que puede ser causada por el reequilibrio? No puedes simplemente llamar a commitSync() o commitAsync() en este momento, porque solo confirmarán el último desplazamiento en el lote de mensajes.

Afortunadamente, la API del consumidor permite que las llamadas a commitSync() y commitAsync() pasen la partición y el desplazamiento que desean confirmar:

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

Insertar descripción de la imagen aquí

Como se muestra en la figura: desplazamiento de envío del consumidor = desplazamiento máximo del mensaje de partición extraído por la encuesta actual + 1, este desplazamiento de envío es la próxima vez

@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. Presentar desplazamiento de consumo por partición

@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. ¿Qué deberían hacer los consumidores cuando no pueden encontrar un desplazamiento del consumo?

Cuando se establece un nuevo grupo de consumidores, no tiene que buscar ningún desplazamiento de consumo. O un nuevo consumidor en el grupo de consumidores se suscribe a un nuevo tema y no tiene ningún desplazamiento de consumo que pueda buscarse. Cuando la información de compensación sobre este grupo de consumidores en el tema __consumer_offsets caduca y se elimina, no hay compensación de consumo para buscar. ¿Qué hacer cuando no hay un desplazamiento inicial en Kafka o el desplazamiento actual ya no existe en el servidor?

En este momento, la ubicación para iniciar el consumo se determinará en función de la configuración del parámetro del cliente consumidor auto.offset.reset, cuyo valor es el siguiente:

  • último (valor predeterminado): indica que los mensajes se consumen a partir del final de la partición.
  • más temprano: indica que el consumidor comenzará a consumir desde el principio, que es 0.
  • none: cuando no se encuentra ningún desplazamiento de consumo, el consumo no comenzará desde la posición del último mensaje ni desde la posición del mensaje más antiguo. En este caso, se informará una excepción NoOffsetForPartitionException. Si se puede encontrar el desplazamiento del consumo, configurarlo en "ninguno" no provocará ninguna excepción.

Si la configuración no es "más reciente", "más antigua" y "ninguna", se informará una ConfigException.

El parámetro auto.offset.reset se utiliza para especificar dónde debe comenzar el consumidor a consumir mensajes si no puede encontrar el desplazamiento del consumo al comenzar. Si se puede encontrar el desplazamiento de consumo, entonces el consumidor comenzará a consumir mensajes de ese desplazamiento, por lo que el parámetro auto.offset.reset no tendrá efecto y solo tendrá efecto cuando no se pueda encontrar el desplazamiento de consumo. Si el desplazamiento excede los límites, es decir, el desplazamiento de consumo excede el número o rango de posición de mensajes en la cola de mensajes, el parámetro auto.offset.reset también tendrá efecto.

5. ¿Cómo leer mensajes de un desplazamiento de partición específico?

Si el consumidor puede encontrar el desplazamiento de consumo, se puede usar poll() para leer mensajes del último desplazamiento de cada partición, y el parámetro auto.offset.reset proporcionado también puede ser de grano grueso si no se puede encontrar el desplazamiento de consumo o El desplazamiento está fuera de límites, empieza a consumir desde el principio o el final. Pero a veces necesitamos un control más detallado que nos permita comenzar a extraer mensajes de un desplazamiento específico, y el método seek() en KafkaConsumer proporciona exactamente esta función, permitiéndonos consumir hacia adelante o hacia atrás.

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

① El parámetro partición en el método seek () representa la partición, y el parámetro de desplazamiento se usa para especificar la ubicación en la partición para comenzar a consumir. El método seek () solo puede restablecer la posición de consumo de la partición asignada por el consumidor, y la asignación de la partición se implementa durante la llamada del método poll (). Es decir, el método poll () debe ejecutarse una vez antes de ejecutar el método seek (), y la posición de consumo se puede restablecer solo después de asignar la partición:

@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));
            // ...
        }
    }
}

② Si el parámetro en el método poll() es 0 y este método regresa inmediatamente, entonces la lógica de asignación de particiones dentro del método poll() será demasiado tarde para implementarse. Es decir, el consumidor no está asignado a ningún partición en este momento, entonces topicPartitions Es una lista vacía. Entonces, ¿cuál es la configuración adecuada para el parámetro de tiempo de espera aquí? Si es demasiado corto, la operación de asignación de partición fallará, si es demasiado largo, puede provocar esperas innecesarias. Podemos usar el método de asignación () de KafkaConsumer para determinar si la partición correspondiente está asignada:

@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));
            // 消费消息
        }
    }
}

③ Si el método seek() se ejecuta en una partición no asignada, se informará una excepción IllegalStateException. Similar a llamar al método seek() directamente después de llamar al método 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));
            // 消费消息
        }
    }
}

Error reportado:

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

④ Si el consumidor del grupo de consumidores puede encontrar el desplazamiento de consumo al comenzar, entonces el consumidor comenzará a consumir mensajes de ese desplazamiento. A menos que el desplazamiento exceda los límites, es decir, el desplazamiento del consumo exceda el número o rango de posición de los mensajes en la cola de mensajes, el parámetro auto.offset.reset no funcionará. En este momento, si desea especificar el consumo desde el principio o al final, necesitas el método seek(). Para ayudar, especifica el consumo desde el final de la partición:

El método endOffsets () se utiliza para obtener la posición del mensaje al final de la partición especificada. El método específico de endOffsets se define de la siguiente manera:

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

El parámetro de particiones representa el conjunto de particiones y el parámetro de tiempo de espera se utiliza para establecer el período de tiempo de espera para la adquisición. Si no se especifica el valor del parámetro de tiempo de espera, el tiempo de espera del método endOffsets() lo establece el parámetro del cliente request.timeout.ms y el valor predeterminado es 30000. Correspondiente a endOffsets es el método StartingOffset(). La posición inicial de una partición es 0 al principio, pero eso no significa que sea 0 en todo momento. Debido a que la acción de limpieza de registros limpiará los datos antiguos, la posición inicial de la partición será Naturalmente, la definición específica del método StartingOffsets() es la siguiente:

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

El contenido y significado de los parámetros del método StartingOffsets() son los mismos que los del método endOffsets(), con estos dos métodos podemos empezar a consumir desde el principio o el final de la partición. De hecho, KafkaConsumer proporciona directamente el método seekToBeginning () y el método seekToEnd () para implementar estas dos funciones. Las definiciones específicas de estos dos métodos son las siguientes:

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

⑤ A veces no conocemos el lugar de consumo específico, pero conocemos un momento relevante. Por ejemplo, queremos consumir noticias después de las 8 en punto de ayer. Esta demanda está más en línea con la lógica del pensamiento normal. En este momento no podemos utilizar directamente el método seek() para rastrear hasta la ubicación correspondiente. KafkaConsumer también tiene en cuenta esta situación y proporciona un método offsetsForTimes() para consultar la ubicación de la partición correspondiente a través de la marca de tiempo:

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

El parámetro timestampsToSearch del método offsetsForTimes() es de tipo Map, la clave es la partición a consultar y el valor es la marca de tiempo a consultar, este método devolverá la posición y marca de tiempo correspondiente al primer mensaje con una marca de tiempo mayor. mayor o igual a la hora a consultar. , correspondiente a los campos de desplazamiento y marca de tiempo en OffsetAndTimestamp. El siguiente ejemplo demuestra el uso entre offsetsForTimes() y seek(). Primero, el método offsetsForTimes() se usa para obtener la posición del mensaje hace un día, y luego el método seek() se usa para rastrear hasta la posición correspondiente y empezar a consumir:

@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());
            }
        }
    }
}

⑥ El desplazamiento fuera de límites también activará la ejecución del parámetro auto.offset.reset. El desplazamiento fuera de límites significa que la posición de consumo se conoce pero no se puede encontrar en la partición real. Por ejemplo, la posición de recuperación original es 101 (compensación de búsqueda 101), pero ha salido de los límites (fuera de rango), por lo que en este momento la posición de extracción se restablecerá (restableciendo la compensación) a 100 de acuerdo con el valor predeterminado del parámetro auto.offset.reset También podemos saber que el desplazamiento máximo de mensajes en la partición en este momento es 99.

6. ¿Cómo salir con gracia del consumo del ciclo de sondeo?

¿Cómo salir elegantemente del ciclo de sondeo? Si está seguro de que desea cerrar al consumidor pronto (incluso si el consumidor todavía está esperando que regrese una encuesta()), puede llamar a consumer.wakeup() en otro hilo. Si el ciclo de sondeo se ejecuta en el hilo principal, este método se puede llamar en ShutdownHook. Cabe señalar que consumer.wakeup() es el único método que un consumidor puede llamar de forma segura desde otros subprocesos. Llamar a consumer.wakeup() hará que poll() arroje una WakeupException. Si el hilo no está sondeando cuando se llama a consumer.wakeup(), la excepción se lanzará la próxima vez que se llame a poll(). No es necesario manejar WakeupException, pero se debe llamar a consumer.close() antes de salir del hilo. Cuando se cierra el consumidor, confirmará las compensaciones no comprometidas y enviará un mensaje al coordinador del consumidor para informarle que abandonará el grupo. El coordinador activará el reequilibrio inmediatamente y las particiones propiedad del consumidor cerrado se reasignarán a otros consumidores del grupo sin esperar a que expire el tiempo de espera de la sesión.

Supongo que te gusta

Origin blog.csdn.net/qq_42764468/article/details/132358750
Recomendado
Clasificación