kafka系列四:consumer端开发

  上一篇介绍了producer端的开发,本篇介绍一下consumer端的开发。

一、consumer端示例程序开发

  和开发producer端程序一样,首先检查pom文件是否有已经有kafka相关jar包依赖,如果没有,则需要在pom.xml中添加如下依赖:

    <!--kafka-->
    <dependency>
      <groupId>org.apache.kafka</groupId>
      <artifactId>kafka_2.12</artifactId>
      <version>2.2.0</version>
    </dependency>

  然后在resouces下面新建 kafka-consumer.properties 配置文件,用于配置consumer端相关参数,配置内容如下:

bootstrap.servers=192.168.184.128:9092,192.168.184.128:9093,192.168.184.128:9094
key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
group.id=testGroup
enable.auto.commit=true
auto.commit.interval.ms=1000
auto.offset.reset=earliest

  上面配置中前4个参数是没有默认值的,所以必须显示配置取值(注:consumer的参数配置可以参考:http://kafka.apache.org/documentation/#consumerconfigs),接下来编写测试代码:

/**
     * consumer接收消息测试
     */
    public void receiveMsg() throws IOException {
        String topicName = "testTopic";
        //1.构造Properties对象
        Properties consumerProps = new Properties();
        FileInputStream fileInputStream = new FileInputStream("F:\\javaCode\\jvmdemo\\src\\main\\resources\\kafka-consumer.properties");
        consumerProps.load(fileInputStream);
        fileInputStream.close();
        //2.使用Properties构造KafkaConsumer对象
        KafkaConsumer consumer = new KafkaConsumer(consumerProps);
        //3.调用KafkaConsumer的subscribe方法订阅consumer group感兴趣的topic列表
        consumer.subscribe(Arrays.asList(topicName));
        try {
            while (true) {
                //4.调用conusmer.poll方法   获取封装在consumerRecord中的消息
                ConsumerRecords<String, String> records = consumer.poll(1000);
                for (ConsumerRecord<String, String> record : records) {
                    //5.处理获取到的consumerRecord对象
                    System.out.printf("offset=%d,key=%s,value=%s%n", record.offset(), record.key(), record.value());
                }
            }
        } finally {
            //6.关闭cnsumer
            consumer.close();
        }
    }

  运行以上代码之后,IEDA会处于一个监听状态,然后再运行上一篇中的producer程序,向testTopic主题中添加消息,此时会在IEDA的控制台上看到consumer端可以接收到producer端发送的消息:

  

二、consumer位移管理

  1.位移的概念

  consumer端需要为每个它要读取的分区保存消费进度,即分区中当前消费消息的位置,该位置称为位移(offset)。kafka中的consumer group中使用一个map来保存其订阅的topic所属分区的offset:

  

   实际上,这里的位移值通常是下一条待消费的消息的位置,因为位移是从0开始的,所以位移为N的消息其实是第N+1条消息。在consumer中有如下位置信息:

  

  上次提交位移:consumer最后一次提交的offset值;

  当前位置:consumer已经读取,但尚未提交时的位置;

  水位:也称为高水位,严格来说它属于分区日志的概念,对于处于水位以下(水位左侧)的所有消息,consumer是可以读取的,水位以上的消息consumer无法读取;

  日志最新位移:也称日志终端位移,严格来说也属于分区日志的概念,表示了某个分区副本当前保存消息对应的最大位移值。

  2.位移提交原理

  consumer需要定期向Kafka提交自己的位置信息,这一过程称为位移提交(offset commit)。consumer会在所有的broker中选择一个broker作为consumer group的coordinator,用于实现组成员管理、消费分配方案制定以及位移提交等。选择该coordinator的依据就是kafka的内置topic(_consumer_offsets)。该topic与普通topic一样,配置多个分区,每个分区有多个副本,它存在的唯一目的就是保存consumer提交的位移。

  当消费者组首次启动的时候,由于没有初始的位移信息,coordinator需要为其确定初始位移值,这就是consumer参数 auto.offset.reset 的作用,通常情况下,consumer要么从最开始位移开始读取,要么从最新的位移开始读取。当cosumer运行一段时间之后,就需要提交自己的位移信息,如果consumer奔溃或者被关闭,它负责的分区就会被分配给其他consumer,因此一定要在其他consumer读取这些分区前就做好位移提交,否则会出现重复消费。

  consumer提交位移的主要机制是通过向所属的coordinator发送位移提交请求来实现的,每个位移提交请求都会向_consumer_offsets对应分区写入一条消息,消息的key是group.id,topic和分区的元组,value是位移值,如果consumer为同一个group的同一个topic分区提交了多次位移,那么只有最新的那次提交的位移值是有效的,其余几次提交的位移值都已经过期,Kafka通过压实(compact)策略来处理这种消息使用模式,

  3.位移的自动提交与手动提交

  默认情况下,consumer是自动提交位移的,可以修改consumer端参数 enable.auto.commit=false 来设置提交方式为手动提交。默认情况下自动提交间隔为5秒,可以通过修改 auto.commit.interval.ms=1000 来配置自动提交的时间间隔。

  自动提交降低了开发成本,但是不能细粒度处理位移提交。例如一个典型的consumer应用场景:用户需要对poll方法返回的消息集合中的消息执行业务级处理,只有消息真正被处理完之后再提交位移,使用自动提交位移显然没法保证这种时序性,因此这种情况下需要使用手动提交位移。手动提交位移时候,设置完 enable.auto.commit=false 之后,在需要提交位移的地方调用 consumer.commitAsync() 方法或者 consumer.commitSync() 方法即可,如以下例子所示:

/**
     * consumer接收消息测试
     */
    public void receiveMsg() throws IOException {
        String topicName = "testTopic";
        //1.构造Properties对象
        Properties consumerProps = new Properties();
        FileInputStream fileInputStream = new FileInputStream("F:\\javaCode\\jvmdemo\\src\\main\\resources\\kafka-consumer.properties");
        consumerProps.load(fileInputStream);
        fileInputStream.close();
        //2.使用Properties构造KafkaConsumer对象
        KafkaConsumer consumer = new KafkaConsumer(consumerProps);
        //3.调用KafkaConsumer的subscribe方法订阅consumer group感兴趣的topic列表
        consumer.subscribe(Arrays.asList(topicName));
        try {
            final int minBatchSize = 500;
            List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
            while (true) {
                //4.调用conusmer.poll方法   获取封装在consumerRecord中的消息
                ConsumerRecords<String, String> records = consumer.poll(1000);
                System.out.println("调用poll");
                for (ConsumerRecord<String, String> record : records) {
                    //5.处理获取到的consumerRecord对象
                    buffer.add(record);
                }
                if (buffer.size() >= minBatchSize) {
                    // 入库操作完之后提交位移
                    insertToDb(buffer);
                    consumer.commitSync();
                    buffer.clear();
                }
            }
        } finally {
            //6.关闭consumer
            consumer.close();
        }
    }

  在手动提交位移的时候, consumer.commitAsync() 方法是一个异步非阻塞提交,consumer在后续poll调用的时候会轮询该位移提交的结果。 consumer.commitSync() 方法是一个同步提交方法,当位移提交结束后才会执行下一条命令。

  以上手动提交位移的时候,使用的 consumer.commitAsync() 及 consumer.commitSync() 是一个无参的提交方法,它会为所有它订阅的分区提交位移,可以选择有参方法只对consumer所拥有的分区进行位移提交,如下代码所示: 

    public void receiveMsg() throws IOException {
        String topicName = "testTopic";
        //1.构造Properties对象
        Properties consumerProps = new Properties();
        FileInputStream fileInputStream = new FileInputStream("F:\\javaCode\\jvmdemo\\src\\main\\resources\\kafka-consumer.properties");
        consumerProps.load(fileInputStream);
        fileInputStream.close();
        //2.使用Properties构造KafkaConsumer对象
        KafkaConsumer consumer = new KafkaConsumer(consumerProps);
        //3.调用KafkaConsumer的subscribe方法订阅consumer group感兴趣的topic列表
        consumer.subscribe(Arrays.asList(topicName));
        try {
            while (true) {
                //4.调用conusmer.poll方法   获取封装在consumerRecord中的消息
                ConsumerRecords<String, String> records = consumer.poll(1000);
                System.out.println("调用poll");
                for (TopicPartition partition : records.partitions()) {
                    // 按照分区获取消息
                    List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
                    for (ConsumerRecord<String, String> partitionRecord : partitionRecords) {
                        System.out.println(partitionRecord.offset()+":"+partitionRecord.value());
                    }
                    long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
                    // 按照分区提交位移
                    consumer.commitSync(Collections.singletonMap(partition,new OffsetAndMetadata(lastOffset+1)));
                }
            }
        } finally {
            //6.关闭cnsumer
            consumer.close();
        }
    }

三、消费者组重平衡(rebalance)

  rebalance只对cnosume group有效,对于standalone consumer,不存在rebalance的概念。rebalance实质上是一种协议,它规定了一个consumer group下所有的consumer如何达成一致性来分配订阅topic的所有分区。例如有个consumer group下有20个consumer,而该consumer group订阅了一个具有100个分区的topic,那么正常情况下,consumer group会为每个consumer分配5个分区,即每个consumer负责读取5个分区的数据,这个分配的过程称为rebalance。

  1.rebalance触发条件

  (1)组成员变更,如新的consumer加入组,或者consumer离开,亦或是consumer发生奔溃;

  (2)组订阅的topic数发生变更;

  (3)组订阅的topic的分区数发生变更。

  2.rebalance分区分配

  rebalance的时候group下面的consumer都会协调在一起共同参与分区分配。consumer默认提供了3种分区分配策略,分别是range策略,round-robin策略和sticky策略。

  range策略主要基于范围的思想,它将单个topic的分区按照顺序排列,然后把这些分区划分为固定大小的分区段并依次分配给每个consumer;

  round-robin策略则会把所有的topic分区顺序摆开,然后轮询式的分配各各个consumer;

  sticky策略有效地避免了上述两种策略完全无视历史分配方案的缺陷,采用了“有黏性”的策略对所有consumer实例进行分配,可以避免极端情况下的数据倾斜并且在两次rebalance间最大限度的维持了之前的分配方案。

  3.rebalance协议

  rebalance本质上是一组协议,group和coordinator共同使用这组协议完成group的rebalance,最新版本kafka提供了如下5个协议来处理rebalance相关事宜:

  JoinGroup请求:consumer请求加入组;

  SyncGroup请求:group leader把分配方案同步更新到组内所有成员中;

  HeartBeat请求:consumer定期向coordinator汇报心跳表明自己依然存活;

  LeaveGroup请求:cosumer主动通知coordinator该consumer即将离组;

  DescripeGroup请求:查看组的所有信息,包括成员信息,协议信息,分配信息以及订阅信息等。

 

posted @ 2019-06-10 19:51  码农大卫  阅读(1229)  评论(0编辑  收藏  举报