Kafka之Consumer

版权声明:本文是作者在学习与工作中的总结与笔记,如有内容是您的原创,请评论留下链接地址,我会在文章开头声明。 https://blog.csdn.net/usagoole/article/details/82812896

kafka版本声明

  1. 使用的是kafka 0.10.0.1版本
  2. 参考文献
    • 《kafka权威指南》

基础

  1. Push(推送)方式是broker接收到消息后,主动把消息推送到Consumer(消费者)
    • 优点:实时性高
    • 缺点:
      • 加大broker的工作量,影响broker性能.
      • Consumer(消费者)的处理能力各不相同,Consumer(消费者)的状态不受broker控制,如果Consumer(消费者)不能及时处理,broker推送过来的消息,可能会造成各种问题,比如缓冲区溢出、内存溢出等
  2. Pull(拉取)方式是Consumer(消费者)循环地从broker拉取消息,拉取多少消息,什么时候拉取都是由Consumer(消费者)决定,处理完毕再继续拉取,这样可以达到限流的目的,不会出现处理不过来的情况。
    • 优点:Consumer(消费者)自己控制流量
    • 缺点: 拉取消息的时间间隔不好控制,间隔太短就处在一个忙等的状态,浪费资源,时间间隔太长,broker的消息不能及时处理
  3. kafka采用pull(拉取)模型,由Consumer自己记录消费状态,每个Consumer互相独立地顺序读取每个Partition(分区)的消息。kafka服务端有一个__consumer_offsets的内部Topic(主题),用来记录消费者已经提交的offset(偏移量)(如果消费者消费了但是并未提交,是不记录的)

消费组

  1. 为每一个需要获取一个或者多个Topic(主题)全部消息的应用程序创建一个ConsumerGroup(消费组),然后往ConsumerGroup(消费组)里添加Consumer(消费者)来伸缩读取能力和处理能力,ConsumerGroup(消费组)里的每个Consumer(消费者)只处理一部分消息。不要让Consumer(消费者)的数量超过Topic(主题)分区的数量,多余的Consumer(消费者)只会闲置

  2. 线程安全

    • 在同一个ConsumerGroup(消费组)里,一个Consumer(消费者)使用一个线程。无法让一个线程运行多个Consumer(消费者),也无法让多个线程安全地共享一个Consumer(消费者)
  3. 程序中使用group.id唯一标识一个 ConsumerGroup(消费组)group.id是一个自定义的字符串。ConsumerGroup(消费组)可以有一个或多个Consumer实例,Consumer实例可以是一个进程,也可以是一个线程

rebalance

  1. ConsumerGroup(消费组)里的Consumer(消费者)共同读取topic(主题)partition(分区),一个新的Consumer(消费者)加入ConsumerGroup(消费组)时,读取的是原本由其他Consumer(消费者)读取的消息。当一个Consumer(消费者)被关闭或发生奔溃时,它就离开ConsumerGroup(消费组),原本由它读取的分区将有ConsumerGroup(消费组)的其他Consumer(消费者)来读取。在topic发生变化时(比如添加了新的分区),会发生Partition重分配,Partition的所有权从一个Consumer(消费者)转移到另一个Consumer(消费者)的行为被称为rebalance(再均衡)rebalance(再均衡)本质上是一种协议,规定了ConsumerGroup(消费组)中所有Consumer(消费者)如何达成一致来消费topic(主题)下的partition(分区)
  2. rebalance(再均衡)ConsumerGroup(消费组)带来了高可用性和伸缩性(可以安全的添加或移除消费者),在rebalance(再均衡)期间,Consumer(消费者)无法读取消息,造成整个Consumer(消费者)一段时间的不可用
  3. Consumer(消费者)通过向GroupCoordinator(群组协调器)(不同的ConsumerGroup(消费组)可以有不同的)发送心跳来维持它们与群组的从属关系以及它们对分区的所有权关系。Consumer(消费者)会在轮询消息或者提交偏移量时发送心跳(kafka0.10.1之前的版本),在kafka0.10.1版本里,心跳线程是独立的
  4. 分配分区的过程
    • Consumer(消费者)加入ConsumerGroup(消费组)时,会向GroupCoordinator(群组协调器)发送一个JoinGroup请求,第一个加入群组的Consumer(消费者)将会成为群主,群主从GroupCoordinator(群组协调器)获得ConsumerGroup(消费组)的成员列表(此列表包含所有最新正常发送心跳的活跃的Consumer(消费者)),并负责给每一个Consumer(消费者)分配分区(PartitionAssignor的实现类来决定哪个分区被分配给哪个Consumer(消费者))
    • 群主把分配情况列表发送给GroupCoordinator(群组协调器)GroupCoordinator(群组协调器)再把这些信息发送给ConsumerGroup(消费组)里所有的Consumer(消费者)。每个Consumer(消费者)只能看到自己的分配信息,只有群主知道ConsumerGroup(消费组)里所有消费者的分配信息。
  5. rebalance(再均衡)触发条件
    • ConsumerGroup(消费组)里的Consumer(消费者)发生变更(主动加入、主动离开、崩溃)
    • 订阅topic(主题)的数量发生变更(比如使用正则表达式的方式订阅)
    • 订阅topic(主题)partition(分区)数量发生变更

提交偏移量

  1. 更新Partition当前位置操作叫做commit(提交)。当Consumer(消费者)发生崩溃或者有新的Consumer(消费者)加入ConsumerGroup(消费组),就会触发rebalance(再均衡),完成rebalance(再均衡)之后每个Consumer(消费者)可能分配到新的Partition(分区),为了能够继续处理之前的消息,Consumer(消费者)需要读取每个Partition(分区)最后一次提交的Offset(偏移量),然后从指定的Offset(偏移量)位置继续处理

  2. Consumer(消费者)提交的OffsetConsumer(消费者)处理的Offset

    • 提交偏移量小于客户端处理的偏移量: 两个偏移量之间的消息就会被重复处理
    • 提交偏移量大于客户端处理的偏移量: 处于两个偏移量之间的消息将会丢失
  3. 在当前使用的kafka 0.10.0.1版本中,心跳不是独立的,消息处理时间过长, poll的时间间隔很长, 导致不能及时在poll发送心跳, 且offset也不能提交, 客户端被超时被判断为挂掉, 未提交offset的消息会被其他Consumer(消费者)重新消费.

测试不提交情况

  1. 不提交

    	static Properties props = new Properties();
    
        @BeforeClass
        public static void testBefore() {
            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
            props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
            props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
            props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000");
            props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
            props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        }
        @Test
        public void testConsumerNoCommit() {
            props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
            props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_group");
            KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(props);
            String topic = "testTopic";
    
            consumer.subscribe(Collections.singletonList(topic));
            while (true) {
                //如果缓冲区中没有数据会阻塞
                ConsumerRecords<Integer, String> records = consumer.poll(1000);
                for (ConsumerRecord<Integer, String> record : records) {
                    System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset());
                }
                //consumer.commitSync(); 注释手动提交
    
            }
        }
    
  2. 查看服务端的test_group的状态,可以看到服务存储的test_group已经提交的偏移量是unknown,即还是开始状态

    bash-4.3# bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --new-consumer --describe --group test_group
    
    GROUP          TOPIC      PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             OWNER
    test_group     testTopic   0          unknown         0               unknown       consumer-1_/172.17.0.1
    test_group     testTopic   1          unknown         0               unknown       consumer-1_/172.17.0.1
    test_group     testTopic   2          unknown         100             unknown       consumer-1_/172.17.0.1
    test_group     testTopic   3          unknown         0               unknown       consumer-1_/172.17.0.1
    test_group     testTopic   4          unknown         0               unknown       consumer-1_/172.17.0.1
    
  3. 重启再次运行Consumer(不更改消费组的名称),此时会重复消费,即从开始位置消费。 为什么要重启Consumer呢?因为kafkaoffset的记录会有两份,服务端会记录一份,本地的Consumer也会记录一份,已经提交的offset会告诉服务端,但是本地的offset,无论提交与否都会记录,所以如果不重启不会重复消费。

  4. 提交一部分,commitSync()commitAsync()只会提交最后一个offset,如果想提交一部分,则不能直接使用commitSync(),需要使用commitSync(final Map<TopicPartition, OffsetAndMetadata> offsets)

    	 //错误的写法
    	 @Test
        public void testConsumerNoCommit() {
            props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
            props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_group_2");
            KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(props);
            String topic = "testTopic";
    
            consumer.subscribe(Collections.singletonList(topic));
            int count = 0;
            while (true) {
                ConsumerRecords<Integer, String> records = consumer.poll(1000);
                for (ConsumerRecord<Integer, String> record : records) {
                    System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset());
                    count++;
    
                    if (count == 50) {
                        System.out.println(count);
                        //commitSync只会提交最后一个offset,所以提交的偏移量是100,而不是50,因为服务端有100条消息
                        consumer.commitSync();
                    }
                }
    
            }
        }
        # bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --new-consumer --describe --group test_group_2
    GROUP         TOPIC      PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG   OWNER
    test_group_2  testTopic   0          unknown         0               unknown         consumer-1_/172.17.0.1
    test_group_2  testTopic   1          unknown         0               unknown         consumer-1_/172.17.0.1
    test_group_2  testTopic   2          100             100             0               consumer-1_/172.17.0.1
    test_group_2  testTopic   3          unknown         0               unknown         consumer-1_/172.17.0.1
    test_group_2  testTopic   4          unknown         0               unknown         consumer-1_/172.17.0.1
    
        
        //正确的写法
         @Test
        public void testConsumerOffset() {
            props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
            props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_group_2");
            KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(props);
            String topic = "testTopic";
            consumer.subscribe(Collections.singletonList(topic));
            //跟踪已经正确处理的偏移量
            Map<TopicPartition, OffsetAndMetadata> offsetAndMetadataMap = new HashMap<>();
            int count = 0;
            while (true) {
                ConsumerRecords<Integer, String> records = consumer.poll(1000);
                for (ConsumerRecord<Integer, String> record : records) {
                    System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset());
    
                    offsetAndMetadataMap.put(
                            new TopicPartition(record.topic(), record.partition()),
                            new OffsetAndMetadata(record.offset() + 1, "")
                    );
                    //当为50时提交map中的偏移量
                    if (count == 50) {
                        consumer.commitSync(offsetAndMetadataMap);
                    }
                    count++;
                }
    
            }
        }
       
        
    	# bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --new-consumer --describe --group test_group_2
    GROUP         TOPIC      PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG   OWNER
    test_group_2  testTopic   0          unknown         0               unknown         consumer-1_/172.17.0.1
    test_group_2  testTopic   1          unknown         0               unknown         consumer-1_/172.17.0.1
    test_group_2  testTopic   2          51              100             49              consumer-1_/172.17.0.1
    test_group_2  testTopic   3          unknown         0               unknown         consumer-1_/172.17.0.1
    test_group_2  testTopic   4          unknown         0               unknown         consumer-1_/172.17.0.1
    
    

猜你喜欢

转载自blog.csdn.net/usagoole/article/details/82812896