KafkaConsumer多线程优化

优化目的:
KafkaConsumer是以单线程模式运行,为了提升consumer的消费能力,多线程是一个很好的选择。KafkaConsumer和KafkaProducer不同,后者是线程安全的,因此我们鼓励用户在多个线程中共享一个KafkaProducer实例,这样通常都要比每个线程维护一个KafkaProducer实例效率要高。但对于KafkaConsumer而言,它不是线程安全的,所以实现多线程时通常有两种实现方法​

方案1 每个线程内部维护自己的consumer对象​

缺点:​

更多的TCP连接开销(每个线程都要维护若干个TCP连接)​
consumer数受限于topic分区数,扩展性差​
频繁请求导致吞吐量下降​
线程自己处理消费到的消息可能会导致超时,从而造成rebalance

方案2 Consumer内部维护自己的worker线程池​

缺点:​

实现较复杂​
线程池容量规划​
限流策略​
优雅关闭
作者采用第二种方式进行实现,在实现过程中发了一些需要注意的细节,所以写下本文做记录

Consumer V1.0

    private ExecutorService scheduler;​
    
    {​
        ThreadFactory threadFactory = new ThreadFactoryBuilder()​
                .setNameFormat("base-consumer-pool-%d").build();​
                
        scheduler = new ThreadPoolExecutor(4, 8, 0L, TimeUnit.MILLISECONDS,​
                new LinkedBlockingQueue<>(MAX_QUEUE_SIZE),  threadFactory, ​
                new ThreadPoolExecutor.AbortPolicy());​
    }​
    
    @KafkaListener(topics = "test")​
    public void listen(ConsumerRecord<?, ?> record) {​
        Optional<?> kafkaMessage = Optional.ofNullable(record.value());​
        if (kafkaMessage.isPresent()) {​
            scheduler.execute(() -> {​
                //do something​
            });​
        }​
    }​


problem
当下游业务吞吐量,低于consumer的吞吐量时,消息会在线程池中积压,最终导致线程池满,抛出RejectedException​

​当我们设计线程池时,初衷是为了提升consumer的消费能力​,当系统处理能力低于kafkaConsumer消费能力时,我们希望积压的​消息保留在kafka中,而不是被worker线程池rejected​

解决方案:自旋限流
这里借鉴了自旋锁的思路

自旋锁(spin lock)​
是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁,从而减少线程切换开销​

自旋限流 (spin limit)​
在消费线程中,若线程队列大于限流阈值,则挂起当前线程,休眠后,再次验证队列长度,直到队列长度小于阈值Consumer V1.1

private ExecutorService scheduler;​
    private BlockingQueue<Runnable> workQueue;​
    private static final int MAX_QUEUE_SIZE = 1024;​

    {​
        workQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);​
        scheduler = new ThreadPoolExecutor(4, 8, 0L, TimeUnit.MILLISECONDS,​
                workQueue, threadFactory, new ThreadPoolExecutor.AbortPolicy());​
    }​

    @KafkaListener(topics = "test")​
    public void listen(ConsumerRecord<?, ?> record) {​
        Optional<?> kafkaMessage = Optional.ofNullable(record.value());​
        if (kafkaMessage.isPresent()) {​
            //自旋阻塞consumer, 让消费不了的消息保存在kafka中,​ 而不是被worker线程池rejected​
            while (workQueue.size() >= MAX_QUEUE_SIZE) {​
                    Thread.sleep(100);​
            }​
            scheduler.execute(() -> {​
                 //do something​
            });​
        }​
    }​



problem
当线程队列到达阈值后,自旋阻塞了consumer的消费过程​,线程池没有用上maximumPoolSize属性​
解决这个问题需要清楚CorePoolSize、MaximumPoolSize和workQueue的含义​

线程池处理策略​如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;​
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;​
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;​
Consumer V1.2

private ExecutorService scheduler;​
    private BlockingQueue<Runnable> workQueue;​
    private static final int MAX_QUEUE_SIZE = 1024;​
    private static final int MAX_POOL_SIZE = 8;​

    {​
        workQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);​
        scheduler = new ThreadPoolExecutor(4, MAX_POOL_SIZE, 0L, TimeUnit.MILLISECONDS,​
                workQueue, threadFactory, new ThreadPoolExecutor.AbortPolicy());​
    }​

    @KafkaListener(topics = "test")​
    public void listen(ConsumerRecord<?, ?> record) {​
        Optional<?> kafkaMessage = Optional.ofNullable(record.value());​
        if (kafkaMessage.isPresent()) {​
            //验证队列长队的同时校验活跃线程数​
            while (workQueue.size() >= MAX_QUEUE_SIZE && 
                    MAX_POOL_SIZE - ((ThreadPoolExecutor) scheduler).getActiveCount() <= 0 ){​
                Thread.sleep(100);​
            }​
            scheduler.execute(() -> { //do something });​
        }​
    }​



线程池优雅关闭

Runtime.getRuntime().addShutdownHook()​
implements DisposableBean ​
进程需要使用kill -15进行关闭​
    @Override​
    public void destroy() throws Exception {​
         this.scheduler.shutdown();​
         awaitTermination(this.scheduler);​
    }

    private void awaitTermination(ExecutorService executor) {​
        try{​
           executor.awaitTermination(30, TimeUnit.SECONDS)​
        } catch (InterruptedException ex) {​
            Thread.currentThread().interrupt();​
        }​
    }

 

多线程分区消费Kafka是一种通过多个线程同时消费Kafka消息的方式。在Kafka中,一个主题可以被分成多个分区,每个分区可以由一个或多个消费者线程来消费。以下是一种实现多线程分区消费Kafka的方式: 1. 创建一个Kafka消费者实例:使用适当的配置参数创建一个Kafka消费者实例,指定要消费的主题和消费者组ID。 2. 获取主题的分区列表:通过调用`consumer.partitionsFor(topic)`方法,获取指定主题的所有分区。 3. 创建消费者线程:根据分区列表创建相应数量的消费者线程。每个线程负责消费一个或多个分区。 4. 启动消费者线程:启动所有的消费者线程,使它们开始消费消息。 5. 消费消息:每个消费者线程在一个无限循环中执行以下操作: - 通过`consumer.poll()`方法获取一批待消费的消息。 - 遍历消息列表,处理每条消息。 - 提交偏移量:在消息处理完成后,通过`consumer.commitSync()`方法提交消费偏移量,确保下次启动时可以继续从上次的位置消费。 6. 处理异常情况:在消费过程中可能会出现异常,如网络故障或处理消息时的错误。你可以根据需要添加异常处理逻辑,比如重新连接Kafka、重试消息处理等。 需要注意的是,多线程消费Kafka消息时,需要确保线程之间的同步和并发访问控制,以避免数据竞争和重复消费的问题。可以使用线程安全的数据结构和同步机制来实现这一点。 此外,还可以通过调整消费者线程数量和分区分配策略等参数来优化消费性能。例如,可以根据主题的分区数和消费者线程数来进行分区分配,以实现负载均衡和最大化吞吐量。 希望以上信息对你有所帮助!如果你还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值