别网上找个demo就以为掌握了Kafka消费者

点击上方蓝色“胖滚猪学编程”,选择“设为星标”

跟着胖滚猪学编程!好玩!有趣!

前面两篇文章带你打入了kafka生产者的内部花园,现在是不是该轮到消费者接招了呢?

消息怎么消费呢?你会告诉我,so easy。网上随便找段consumer demo就知道了,最多10分钟就可以看到成果。

然而事实可并不这么简单,我再问你:

  • 消费者消费速率跟不上怎么办?

  • 消费组是什么?重平衡是什么?

  • 消费者数据丢失了怎么办?重复消费了怎么办?

  • 怎么指定位置消费?比如我铁了心要2020-05-20 13:14开始消费。

如果有任一问题回答不出来,那你就不能说自己掌握了Kafka Consumer。老老实实看完这篇文章吧骚年。

1

从简单Demo说起

首先还是从简单的demo说起,如果你完全没使用过kafka消费者,那么先运行一下这个demo吧:

public void consume() {
    Properties properties = PropertiesConfig.getConsumerProperties();
    properties.put("group.id", "my_group");
    properties.put("bootstrap.server","192.168.1.9:9092");     
    properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");   
    properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");

    //先创建一个 KafkaConsumer 对象。
    Consumer<String, String> consumer = new KafkaConsumer<>(properties);
   //订阅主题列表 可以匹配正则表达式 如subscribe("test.*");
    consumer.subscribe(Collections.singletonList("customerTopic"));

    try {
     //消费者是一个长期运行的应用程序,它通过轮询的方式向 Kafka 请求数据。
        while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(KafkaConfig.pollTimeoutOfMinutes));
            if (!consumerRecords.isEmpty()) {
                for (ConsumerRecord<String, String> record : consumerRecords) {
              // 省略业务逻辑处理
             
                }

            }
        }
    } catch (Exception e) {
        
    }
}

流程不复杂,首先是配置一些参数然后创建消费者实例,然后进行主题订阅。循环那一部分简单说下,消费者是如何知道生产者发送了数据呢?其实生产者产生的数据消费者是不知道的,所以KafkaConsumer 是采用轮询的方式定期去 Kafka Broker 中进行数据的检索,如果有数据就用来消费,如果没有就再继续轮询等待。如果用刨根问底的思想去看待这个简单的demo,你还会问参数group.id是什么?顾名思义是组的概念。

2

消费组


如果有1000个面包,让一个人解决,他需要至少一年的时间,而让1000个人解决,只需要一分钟的时间。同理,100w条消息如果只让一个消费者来消费,可能需要一个小时,而如果你让50个消费者来消费,只需要5分钟。这50个消费者实例组成的集合就是消费组。

ConsumerGroup:消费者组,指的是多个消费者实例组成一个组来消费一组主题。为什么要引入消费者组呢?从面包的例子不难得出是效率问题。再看下这两张图你就明白了:

2beca3a90249f1f684d409789c62160f.pngad2dfc240f4679937160474aa2a6f6b7.png

主要是为了提升消费者端的吞吐量。多个消费者实例同时消费,加速整个消费端的吞吐量(TPS)。所以我们得出:在用户产生消息特别多的时候,消费者吃不消,可以继续增加消费者(无非就是单机多线程或者分布式)。

可是事实往往没有你想象的简单,Kafka的分区只能被消费者组中的其中一个消费者去消费,组员之间不能重复消费。 也就是分区:消费者是N:1的关系。

比如你看下面这张图,就有消费者闲置了。11b827acad3bb6d7e80cb0c168fc977e.png

这个知识点非常重要!告诉我们分区和消费者的对应关系。所以我们应该提前规划好topic的分区数,否则可能导致消费速率无法提高。另外也告诉我们如何设计消费组?理想情况下,Consumer 实例的数量应该等于该 Group 订阅主题的分区总数。

现在你应该可以回答开头的问题了:消费者消费速率跟不上怎么办?首先你可以使用多进程或者多线程消费,但是有个度,这个度就是分区。我们将在下一篇文章重点说一下kafka多线程消费如何实现

Kafka另外一个很重要的特性就是,只需写入一次消息,可以支持任意多的应用读取这个消息。任意多的应用其实就是任意多的group.id。如下图所示:9219bd98af2b56add0cb8a296622554d.png

消费组是个双刃剑,给我们带来利好的同时、也给我们带来了"重平衡"这个臭名昭著的破玩意,生产很多问题都是由于它引起的。我们将在下一篇文章重点说一下重平衡这个鬼东西。

到现在,你彻底入门了Kafka消费者,基础知识其实就那么点。但是很多问题我们不得不考虑,比如怎么保证消息不丢失不重复呢?

3

保证消息不丢失

回忆一下我们在聊生产者的时候说到保证消息不丢失的一个方法,一定要使用带回调函数的send方法,这里的回调其实就是一个通知作用,通知我们消息有没有发送成功。那么消费者也是如此呀,我们需要知道消费有没有成功,如果没有成功,我们需要进行补处理。

Consumer 程序有个“位移”的概念,表示的是这个 Consumer 当前消费到的 Topic 分区的位置。就好像书签一样,需要书签你才可以快速找到你上次读书的位置。Kafka默认是自动提交位移的(enable.auto.commit=true)。

自动提交可能会有问题,因为自动提交是发生在消费者poll方法调用后每隔5秒(由auto.commit.interval.ms指定)提交一次位移。

假如你poll了0-30条消息,处理到第20条时kafka就自动提交了offset,但是在处理21条的时候出现了异常,当你再次拉取数据时,由于之前是自动提交的offset,所以是从30条之后开始拉取数据,这也就意味着21-30条的数据发生了丢失。

消费端保证不丢数据,最重要就是保证offset的准确性。我们能做的,就是确保消息消费完成再手动提交offset。Consumer 端有个参数 ,设置enable.auto.commit= false,并且采用手动提交位移的方式,我们用手动提交位移的方式改造一下代码:

ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
if (!consumerRecords.isEmpty()) {
    boolean batchHandleResult = true;
    for (ConsumerRecord<String, String> record : consumerRecords) {
        KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
  //处理业务逻辑 返回成功或者失败
        boolean handleResult = handle(kafkaMessage);
        if (!handleResult) {batchHandleResult = false;} 
    }
    //手动提交位移
    if (batchHandleResult) {
        consumer.commitSync(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
    } else {
  //消费失败处理
    }
}

这样做,即使处理消息的过程中发生了异常,由于没有提交位移,下次消费时还会从上次的位移处重新拉取消息,不会发生消息丢失的情况。如果在处理数据时发生了异常,可以进行重试,比如重试3次。还是失败的话,就把这条数据给缓存起来,可以是redis、DB、file等,也可以把这条消息存入专门用于存储失败消息的topic中,让其它的consumer专门处理失败的消息。

消费位移是一个需要深究的东西,比如既可以手动同步提交也可以手动异步提交、而两者结合才是最完美的方式;另外一般可以把消费位移存储在数据库中,并且指定位移消费。。我们将在下一篇文章重点说一下消费位移的那些事。

4

保证消息不重复

即使采用手动提交位移也不能保证消息不重复的问题。假设你处理完业务逻辑,准备提交offset的时候,程序挂了,导致消费位移没有提交,重启之后就会重新消费了。所以消息队列其实是提供了at least once(至少一次) + 幂等性,而这个幂等性交给我们去处理。

我觉得最好的方式就是:从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。 借助业务消息本身及下游组件的幂等性来做。比如有些组件,mysql、hbase、elasticsearch 天然就支持幂等操作。利用mysql数据库的唯一约束(主键)实现幂等,我相信不用多说你也知道了。所以我还是举个HBase的例子吧:

在华泰证券中Kafka的幂等性是如何保证的?在接收端,启动专门的消费者拉取 kafka 数据存入 hbase。hbase 的 rowkey 的设计主要包括 security_id(股票id)和 timestamp(行情数据时间)。消费线程从 kafka 拉取数据后反序列化,然后批量插入 hbase,只有插入成功后才往 kafka 中持久化 offset。这样的好处是,如果在中间任意一个阶段发生报错,程序恢复后都会从上一次持久化 offset 的位置开始消费数据,而不会造成数据丢失。如果中途有重复消费的数据,则插入 hbase 的 rowkey 是相同的,数据只会覆盖不会重复,最终达到数据一致。

但是,不是所有的业务都能设计成天然幂等的。这时候你还可以考虑一种通用性最强,适用范围最广的实现幂等性方法:记录并检查操作,也称为“Token 机制或者 GUID(全局唯一 ID)机制”,实现的思路特别简单:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。

具体的实现方法是,在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。

想法很简单,但是在分布式系统中,这个方法其实是非常难实现的。首先,给每个消息指定一个全局唯一的 ID 就是一件不那么简单的事儿,方法有很多,但都不太好同时满足简单、高可用和高性能,或多或少都要有些牺牲。更加麻烦的是,在“检查消费状态,然后更新数据并且设置消费状态”中,三个操作必须作为一组操作保证原子性,才能真正实现幂等,否则就会出现 Bug。

比如说,对于同一条消息:“全局 ID 为 8,操作为:给 ID 为 666 账户增加 100 元”,有可能出现这样的情况:

  • t0 时刻:Consumer A 收到条消息,检查消息执行状态,发现消息未处理过,开始执行“账户增加 100 元”;

  • t1 时刻:Consumer B 收到条消息,检查消息执行状态,发现消息未处理过,因为这个时刻,Consumer A 还未来得及更新消息执行状态。

这样就会导致账户被错误地增加了两次 100 元,这是一个在分布式系统中非常容易犯的错误,一定要引以为戒。

对于这个问题,当然我们可以用事务来实现,也可以用锁来实现,但是在分布式系统中,无论是分布式事务还是分布式锁都是比较难解决问题。因此一般不推荐使用。

5

总结

使用Kafka消费消息虽然简单,可是考虑的问题可就不是这么简单了。消费组和重平衡、位移提交和位移管理、不重复不丢失的保障、以及参数调优都是非常重要的章节。

在本篇文章已经带你把这些内容都过了一遍,你应该能做到心中有数,但是接下来还会有几篇文章单独讲解一些核心内容:

  • 大名鼎鼎又臭名昭著的消费组和重平衡

  • Kafka消费位移的那些事

  • Kafka多线程消费的思考和实例

今天早点睡!敬请期待后续文章吧!

END

6755ff55d81056ea7f8cb510a03a901d.png

点击查看往期内容回顾

别网上找个demo就以为掌握了Kafka生产者

kafka入门必知必会的术语概念

Kafka消息分区机制的原理及分区策略

原创声明:本文为公众号【胖滚猪学编程】原创博文,转载注明出处!

点个“在看”表示朕

已阅

本人只是kafka小学生,如果有任何不对的地方,请各位大学生一定要指正我,留言写起来!

写留言

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值