Kafka Consumer多线程开发

1,651 阅读2分钟

image.png 如图创建了一个名字为foo分区数为3的topic,在以上情况下,探讨多线程开发Consumer的方案.

worker线程池模式

image.png 该种模式一个ConsumerGroup有一个或多个Consumer实例,不断的从订阅的partitionpoll消息,将获取到的消息投递到workerThreadPool线程池。

优点

  1. 将任务切分成了消息获取消息处理两个部分,分别由不同的线程处理它们。最大优势就在于它的高伸缩性,就是说我们可以独立地调节消息获取的线程数,以及消息处理的线程数,而不必考虑两者之间是否相互影响。如果你的消费获取速度慢,那么增加消费获取的线程数即可;如果是消息的处理速度慢,那么增加 Worker 线程池线程数即可。

缺点

  1. 实现起来比较困难相当于有消息获取消息处理两个线程组。
  2. 该方案将消息获取和消息处理分开了,也就是说获取某条消息的线程不是处理该消息的线程,因此无法保证分区内的消费顺序。这也是他致命的缺点
  3. 由于缺点2,可能导致消息重复消费问题以及消息丢失问题。

多Consumer模式

image.png

消费者程序启动多个线程,每个线程维护专属的 KafkaConsumer 实例,负责完整的消息获取、消息处理流程。

优点

  1. 实现起来很简单,只需要创建多个Consumer实例数即可
  2. 线程间是隔离的,不需要去额外的处理线程间交互问题
  3. 因为每个线程消费的分区是固定的,所以可以很好保证唯一提交的正确性,只要处理的到位,不会出现消息重复,消息丢失问题

缺点

  1. 因为每一个Consumer实例都相当于一个TCP链接,所以对应的资源消耗比较大
  2. Consumer的实例数受限于partition数,Consumer的实例数只能小于等于partition数。

因为worker线程池模式不能保证消息被成功消费,所以这里强烈建议使用多Consumer模式


编码实现

  1. Worker线程池模式
...

private KafkaConsumer<String, Object> kafkaConsumer;

private final ExecutorService workers = Executors.newFixedThreadPool(4);

public void run() {
    kafkaConsumer.subscribe(Arrays.asList("foo","bar"));
    ConsumerRecords<String, Object> records = kafkaConsumer.poll(Duration.ofMillis(100));
    records.forEach(record->{
        workers.submit(()->handlerRecord(record));
    });
}
  1. 多Consumer实例模式
public class KafkaConsumerWorker implements Runnable {

    private final KafkaConsumer<String, Object> consumer;

    private final AtomicBoolean closed = new AtomicBoolean(false);

    public KafkaConsumerWorker(KafkaConsumer<String, Object> consumer) {
        this.consumer = consumer;
    }

    @Override
    public void run() {
        while (!closed.get()) {
            try {
                ConsumerRecords<String, Object> records = consumer.poll(Duration.ofMillis(100));
                // todo handler records
            } catch (WakeupException e) {
                // Ignore exception if closing
                if (!closed.get()) {
                    throw e;
                }
            } finally {
                consumer.close();
            }
        }
    }

    public void shutdown() {
        closed.set(true);
        consumer.wakeup();
    }
}