簡體   English   中英

每個主題是否可以有一個Kafka使用者線程?

[英]Is it possible to have one Kafka consumer thread per topic?

我們有使用Spring-Kafka(2.1.7)的Springboot應用程序。 我們啟用了並發,因此每個分區可以有一個使用者線程。 因此,當前,如果我們有3個主題,每個主題都有2個分區,那么將有2個使用者線程,如下所示:

ConsumerThread1-[topic1-0,topic2-0,topic3-0]
ConsumerThread2-[topic1-1,topic2-1,topic3-1]

但是,我們希望每個主題一個分區而不是每個分區一個KafkaListener(或消費者線程)。 例如:

ConsumerThread1-[topic1-0,topic1-1]
ConsumerThread2-[topic2-0,topic2-1]
ConsumerThread3-[topic3-0,topic3-1]

如果無法做到這一點,那么即使進行以下設置也可以:

ConsumerThread1-[topic1-0]
ConsumerThread2-[topic1-1]
ConsumerThread3-[topic2-0]
ConsumerThread4-[topic2-1]
ConsumerThread5-[topic3-0]
ConsumerThread6-[topic3-1]

需要注意的是, 我們事先不知道主題的完整列表 (我們使用通配符主題模式)。 可以隨時添加新主題,並且應在運行時為該新主題動態創建一個新的使用者線程。

有什么辦法可以實現?

您可以從spring-kafka:2.2為每個主題創建單獨的容器,並設置並發1,以便每個容器都可以從每個主題中消費

從2.2版開始,您可以使用同一工廠創建任何ConcurrentMessageListenerContainer。 如果您想創建多個具有相似屬性的容器,或者希望使用一些外部配置的工廠,例如Spring Boot自動配置提供的工廠,這可能很有用。 創建容器后,您可以進一步修改其屬性,其中許多屬性是使用container.getContainerProperties()設置的。 下面的示例配置一個ConcurrentMessageListenerContainer:

@Bean
public ConcurrentMessageListenerContainer<String, String>(
    ConcurrentKafkaListenerContainerFactory<String, String> factory) {

ConcurrentMessageListenerContainer<String, String> container =
    factory.createContainer("topic1", "topic2");
container.setMessageListener(m -> { ... } );
return container;
}

注意:用這種方法創建的容器不會添加到端點注冊表中。 應該將它們創建為@Bean定義,以便在應用程序上下文中注冊它們。

您可以使用自定義分區程序隨意分配分區。 這是卡夫卡的消費財產。

編輯

看到這個答案

它適用於@JmsListener但是相同的技術也可以應用於kafka。

感謝@Gary Russel的建議,我能夠提出以下解決方案,該解決方案@KafkaListener每個Kafka主題創建一個@KafkaListener bean實例(或使用者線程)。 這樣,如果屬於特定主題的消息存在問題,則不會影響其他主題的處理。

-以下代碼在啟動過程中引發InstanceAlreadyExistsException異常。 但是,這似乎並不影響功能。 使用日志輸出,我可以驗證每個主題有一個bean實例(或線程),並且它們能夠處理消息。

@SpringBootApplication
@EnableScheduling
@Slf4j
public class KafkaConsumerApp {

    public static void main(String[] args) {
        log.info("Starting spring boot KafkaConsumerApp..");
        SpringApplication.run(KafkaConsumerApp.class, args);
    }

}


@EnableKafka
@Configuration
public class KafkaConfiguration {

    private final KafkaProperties kafkaProperties;

    @Value("${kafka.brokers:localhost:9092}")
    private String bootstrapServer;

    @Value("${kafka.consumerClientId}")
    private String consumerClientId;

    @Value("${kafka.consumerGroupId}")
    private String consumerGroupId;

    @Value("${kafka.topicMonitorClientId}")
    private String topicMonitorClientId;

    @Value("${kafka.topicMonitorGroupId}")
    private String topicMonitorGroupId;

    @Autowired
    private ConfigurableApplicationContext context;

    @Autowired
    public KafkaConfiguration( KafkaProperties kafkaProperties ) {
        this.kafkaProperties = kafkaProperties;
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory( consumerFactory( consumerClientId, consumerGroupId ) );
        factory.getContainerProperties().setAckMode( ContainerProperties.AckMode.MANUAL );
        return factory;
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> topicMonitorContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory( consumerFactory( topicMonitorClientId, topicMonitorGroupId ) );
        factory.getContainerProperties().setAckMode( ContainerProperties.AckMode.MANUAL );
        factory.getContainerProperties().setConsumerRebalanceListener( new KafkaRebalanceListener( context ) );
        return factory;
    }

    private ConsumerFactory<String, String> consumerFactory( String clientId, String groupId ) {
        Map<String, Object> config = new HashMap<>();
        config.putAll( kafkaProperties.buildConsumerProperties() );
        config.put( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServer );
        config.put( ConsumerConfig.CLIENT_ID_CONFIG, clientId );
        config.put( ConsumerConfig.GROUP_ID_CONFIG, groupId );
        config.put( ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false ); // needs to be turned off for rebalancing during topic addition and deletion
                                                                    // check -> https://stackoverflow.com/questions/56264681/is-it-possible-to-have-one-kafka-consumer-thread-per-topic/56274988?noredirect=1#comment99401765_56274988
        return new DefaultKafkaConsumerFactory<>( config, new StringDeserializer(), new StringDeserializer() );
    }
}


@Configuration
public class KafkaListenerConfiguration {

    @Bean
    @Scope("prototype")
    public KafkaMessageListener kafkaMessageListener() {
        return new KafkaMessageListener();
    }

}


@Slf4j
public class KafkaMessageListener {

    /*
     * This is the actual message listener that will process messages. It will be instantiated per topic.
     */
    @KafkaListener( topics = "${topic}", containerFactory = "kafkaListenerContainerFactory" )
    public void receiveHyperscalerMessage( ConsumerRecord<String, String> record, Acknowledgment acknowledgment, Consumer<String, String> consumer ) {

        log.debug("Kafka message - ThreadName={}, Hashcode={}, Partition={}, Topic={}, Value={}", 
                Thread.currentThread().getName(), Thread.currentThread().hashCode(), record.partition(), record.topic(), record.value() );

        // do processing

        // this is just a sample acknowledgment. it can be optimized to acknowledge after processing a batch of messages. 
        acknowledgment.acknowledge();
    }

}


@Service
public class KafkaTopicMonitor {

    /*
     * The main purpose of this listener is to detect the rebalance events on our topic pattern, so that 
     * we can create a listener bean instance (consumer thread) per topic. 
     *
     * Note that we use the wildcard topic pattern here.
     */
    @KafkaListener( topicPattern = ".*abc.def.ghi", containerFactory = "topicMonitorContainerFactory" )
    public void monitorTopics( ConsumerRecord<String, String> record ) {
        // do nothing
    }

}


@Slf4j
public class KafkaRebalanceListener implements ConsumerAwareRebalanceListener {

    private static final ConcurrentMap<String, KafkaMessageListener> listenerMap = new ConcurrentHashMap<>();
    private final ConfigurableApplicationContext context;

    public KafkaRebalanceListener( ConfigurableApplicationContext context ) {
        this.context = context;
    }

    public void onPartitionsRevokedBeforeCommit(Consumer<?, ?> consumer, Collection<TopicPartition> partitions) {
        // do nothing
    }

    public void onPartitionsRevokedAfterCommit(Consumer<?, ?> consumer, Collection<TopicPartition> partitions) {
        // do nothing
    }

    public void onPartitionsAssigned(Consumer<?, ?> consumer, Collection<TopicPartition> partitions) {

        log.info("OnPartitionsAssigned - partitions={} - {}", partitions.size(), partitions);
        Properties props = new Properties();
        context.getEnvironment().getPropertySources().addLast( new PropertiesPropertySource("topics", props) );

        for( TopicPartition tp: partitions ) {

            listenerMap.computeIfAbsent( tp.topic(), key -> {
                log.info("Creating messageListener bean instance for topic - {}", key );
                props.put( "topic", key );
                // create new KafkaMessageListener bean instance
                return context.getBean( "kafkaMessageListener", KafkaMessageListener.class );
            });
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM