环境要求

(1)已经搭建的kafka集群
(2)Apache Maven
(3)kafka_2.11-2.2.0

一、概念说明

以官方给出的图为例,如下所示

java 监控kafka消费 java kafka 消费_数据


消费者组与主题之间关系

每个Consumer 进程都会划归到一个逻辑的Consumer Group中,逻辑的订阅者是Consumer Group。所以一条message可以被多个订阅message所在的topic的每一个Consumer Group所消费,就好像是这条message被广播到每个Consumer Group一样。而每个Consumer Group中,类似于一个Queue(JMS中的Queue)的概念差不多,即一条消息只会被Consumer Group中的一个Consumer消费。

消费者与分区之间的关系
其实topic中的partition被分配到某个consumer上,也就是某个consumer订阅了某个partition。 consumer订阅的是partition,而不是message。所以在同一时间点上,订阅到同一个partition的consumer必然属于不同的Consumer Group。partition分配的工作其实是在consumer leader中完成的。

消费者与消费者组之间关系
消费者组与消费者之间的关系是动态维护的。当一个消费者进程挂掉或者是卡住时,该消费者所订阅的partition会被重新分配到该group内的其它的consumer上。当一个consumer加入到一个consumer group中时,同样会从其它的consumer中分配出一个或者多个partition 到这个新加入的consumer。当启动一个Consumer时,会指定它要加入的group,使用的是配置项:group.id,为了维持Consumer 与 Consumer Group的关系,Consumer需要周期性的发送heartbeat到coordinator(协调者,在早期版本,以zookeeper作为协调者。后期版本则以某个broker作为协调者)。当Consumer由于某种原因不能发Heartbeat到coordinator时,并且时间超过session.timeout.ms时,就会认为该consumer已退出,它所订阅的partition会分配到同一group 内的其它的consumer上。而这个过程,被称为rebalance。如果一个consumer 进程一直在周期性的发送heartbeat,但是它就是不消费消息,这种状态称为livelock(活锁)状态。那么在这种状态下,就会将该consumer退出consumer group。

Coordinator协调者
Coordinator 协调者,协调consumer、broker。早期版本中Coordinator,使用zookeeper实现,但是这样做,rebalance的负担太重。为了解决scalable的问题,不再使用zookeeper,而是让每个broker来负责一些group的管理,这样consumer就完全不再依赖zookeeper了。
从Consumer的实现来看,在执行poll或者是join group之前,都要保证已连接到Coordinator。连接到coordinator的过程是:
1)连接到最后一次连接的broker(如果是刚启动的consumer,则要根据配置中的borker)。它会响应一个包含coordinator信息(host, port等)的response。
2)连接到coordinator。

消费者组管理流程
Consumer Group 管理中,也是需要coordinator的参与。一个Consumer要join到一个group中,或者一个consumer退出时,都要进行rebalance。进行rebalance的流程是:
1)会给一个coordinator发起Join请求(请求中要包括自己的一些元数据,例如自己感兴趣的topics)
2)Coordinator 根据这些consumer的join请求,选择出一个leader,并通知给各个consumer。这里的leader是 consumer group 内的leader,是由某个consumer担任,不要与partition的leader混淆。
3)Consumer leader 根据这些consumer的metadata,重新为每个consumer member重新分配partition。分配完毕通过coordinator把最新分配情况同步给每个consumer。
4)Consumer拿到最新的分配后,继续工作。

消费者读取消息流程

在Kafka partition中,每个消息有一个唯一标识,即partition内的offset。每个consumer group中的订阅到某个partition的consumer在从partition中读取数据时,是依次读取的。

java 监控kafka消费 java kafka 消费_java 监控kafka消费_02


上图中,Consumer A、B分属于不用的消费者组。Consumer B读取到offset =11,Consumer A读取到offset=9 。这个值表示消费者组中的某个Consumer 在下次读取该partition时会从哪个offset的 message开始读取,即 Consumer Group A 中的Consumer下次会从offset = 9 的message 读取, Consumer Group B 中的Consumer下次会从offset = 11 的message 读取。

这里并没有说是Consumer A 下次会从offset = 9 的message读取,原因是Consumer A可能会退出Group ,然后Group A 进行rebalance,即重新分配分区。

消费方法之-poll 方法(重点)
Consumer读取partition中的数据是通过调用发起一个fetch请求来执行的。而从KafkaConsumer来看,它有一个poll方法。但是这个poll方法只是可能会发起fetch请求。原因是:Consumer每次发起fetch请求时,读取到的数据是有限制的,通过配置项max.partition.fetch.bytes来限制的。而在执行poll方法时,会根据配置项个max.poll.records来限制一次最多pool多少个record。
那么就可能出现这样的情况: 在满足max.partition.fetch.bytes限制的情况下,假如fetch到了100个record,放到本地缓存后,由于max.poll.records限制每次只能poll出15个record。那么KafkaConsumer就需要执行7次才能将这一次通过网络发起的fetch请求所fetch到的这100个record消费完毕。其中前6次是每次pool中15个record,最后一次是poll出10个record。
在consumer中,还有另外一个配置项:max.poll.interval.ms ,它表示最大的poll数据间隔,如果超过这个间隔没有发起pool请求,但heartbeat仍旧在发,就认为该consumer处于 livelock状态。就会将该consumer退出consumer group。所以为了不使Consumer 自己被退出,Consumer 应该不停的发起poll(timeout)操作。而这个动作 KafkaConsumer Client是不会帮我们做的,这就需要自己在程序中不停的调用poll方法了。

偏移量提交(commit offset)
当一个consumer因某种原因退出Group时,进行重新分配partition后,同一group中的另一个consumer在读取该partition时,怎么能够知道上一个consumer该从哪个offset的message读取呢?也是是如何保证同一个group内的consumer不重复消费消息呢?上面说了一次走网络的fetch请求会拉取到一定量的数据,但是这些数据还没有被消息完毕,Consumer就挂掉了,下一次进行数据fetch时,是否会从上次读到的数据开始读取,而导致Consumer消费的数据丢失吗?
为了做到这一点,当使用完poll从本地缓存拉取到数据之后,需要client调用commitSync方法(或者commitAsync方法)去提交下一次该去读取哪一个offset的message。而这个commit方法会通过走网络的commit请求将offset在coordinator中保留,这样就能够保证下一次读取(不论进行了rebalance)时,既不会重复消费消息,也不会遗漏消息。
对于offset的commit,Kafka Consumer Java Client支持两种模式:由KafkaConsumer自动提交,或者是用户通过调用commitSync、commitAsync方法的方式完成offset的提交。

二、实现kafka消费者实例

package com.donwait.consumer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
importorg.apache.kafka.common.TopicPartition;
importorg.apache.kafka.common.serialization.StringDeserializer;
/**
 * @author Administrator
 *
 */
public class ConsumerTest {
     
     public ConsumerTest(){
     }
     
     /*
      * 自动提交偏移量-kafka自动管理offset
      * 存在问题:
      * 获取数据后,offset自动提交到broker,但后续对这些数据处理失败,这样就会出现丢失数据现象
      */
     public void consumer1(String brokerList, String group, String topic){
          
          // kafka消费参数配置
          Properties props = new Properties();
          // kafka服务器地址: IP:PORT形式,多个以逗号隔开
          props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
          // 消费者组名称
          props.put(ConsumerConfig.GROUP_ID_CONFIG, group);
          // 自动提交偏移量
          props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
          // 自动提交时间间隔
          props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
          // 心跳间隔。心跳是在consumer与coordinator之间进行的。心跳用来保持consumer的会话
          // 这个值必须设置的小于session.timeout.ms,因为:当Consumer由于某种原因不能发Heartbeat到coordinator时
          // 并且时间超过session.timeout.ms时,就会认为该consumer已退出
          // 这个值必须设置在broker configuration中的group.min.session.timeout.ms 与 group.max.session.timeout.ms之间
          // 且建议设置在session.timeout.ms值的1/3大小
          //props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "1000");
          
          // 消费者会话超时时间
          props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "10000");
          // 消费者key和value反序列化接口
          props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
          props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
          
          // 指定数据的解码器
          //StringDecoder keyDecoder = new StringDecoder(new VerifiableProperties());
        //StringDecoder valueDecoder = new StringDecoder(new VerifiableProperties());
          //ConsumerConfig config = new ConsumerConfig(props);
          //consumer = new KafkaConsumer<String, String>(config, keyDecoder, valueDecoder);
          
          // Kafka数据消费对象
          KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
          // 预定主题
          // List<String> topics = Arrays.asList("my-topic", "test-topic");
          consumer.subscribe(Arrays.asList(topic));
          
          // 死循环读取
          while(true){
              // Consumer读取partition中的数据是通过调用发起一个fetch请求来执行的
              // 等100毫秒
              ConsumerRecords<String, String> records = consumer.poll(100);
              for(ConsumerRecord<String, String> record : records){
                   System.out.printf("收到消息: offset:%d, key:%s, value:%s%n\n", record.offset(), record.key(), record.value());
              }
          }
     }
     
     /**
      * 手动提交偏移量offset
      * 可能出现问题:
      * 当数据已经被处理,在consumer.commitSync()提交之间出现异常,也就是数据已经被处理,
      * 但offset提交失败,这样下次消费也会获取这条数据,会出现数据重复的现象
      */
     public void consumer2(String brokerList, String group, String topic){
          
          // kafka消费参数配置
          Properties props = new Properties();
          // kafka服务器地址: IP:PORT形式,多个以逗号隔开
          props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
          // 消费者组名称
          props.put(ConsumerConfig.GROUP_ID_CONFIG, group);
          // 手动提交偏移量
          props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
          // 心跳间隔。心跳是在consumer与coordinator之间进行的。心跳用来保持consumer的会话
          // 这个值必须设置的小于session.timeout.ms,因为:当Consumer由于某种原因不能发Heartbeat到coordinator时
          // 并且时间超过session.timeout.ms时,就会认为该consumer已退出
          // 这个值必须设置在broker configuration中的group.min.session.timeout.ms 与 group.max.session.timeout.ms之间
          // 且建议设置在session.timeout.ms值的1/3大小
          //props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "1000");
          
          // 消费者会话超时时间
          props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "10000");
          // 消费者key和value反序列化接口
          props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
          props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
          
          // Kafka数据消费对象
          KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
          // 预定主题
          // List<String> topics = Arrays.asList("my-topic", "test-topic");
          consumer.subscribe(Arrays.asList(topic));
          
          final int minBatchSize = 200;
          List<ConsumerRecord<String, String>> buffer = new ArrayList<ConsumerRecord<String, String>>();
          
          // 死循环读取
          while(true){
              // Consumer读取partition中的数据是通过调用发起一个fetch请求来执行的
              ConsumerRecords<String, String> records = consumer.poll(100);
              for (ConsumerRecord<String, String> record : records) {
                  buffer.add(record);
              }
              if(buffer.size() >= minBatchSize){
                   // 本地批处理消息
                   for(ConsumerRecord<String, String> record : records){
                        System.out.printf("收到消息: offset:%d, key:%s, value:%s%n\n", record.offset(), record.key(), record.value());
                   }
                   
                   // 手动异步提交
                 consumer.commitSync();
                 // 清理本地缓存
                 buffer.clear();
              }             
          }
     }
     
     /**
      * 消费具体分区上的数据
      */
     public void consumer3(String brokerList, String group, String topic){
          
          // kafka消费参数配置
          Properties props = new Properties();
          // kafka服务器地址: IP:PORT形式,多个以逗号隔开
          props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
          // 消费者组名称
          props.put(ConsumerConfig.GROUP_ID_CONFIG, group);
          // 自动提交偏移量
          props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
          // 自动提交时间间隔
          props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
          
          // 心跳间隔。心跳是在consumer与coordinator之间进行的。心跳用来保持consumer的会话
          // 这个值必须设置的小于session.timeout.ms,因为:当Consumer由于某种原因不能发Heartbeat到coordinator时
          // 并且时间超过session.timeout.ms时,就会认为该consumer已退出
          // 这个值必须设置在broker configuration中的group.min.session.timeout.ms 与 group.max.session.timeout.ms之间
          // 且建议设置在session.timeout.ms值的1/3大小
          //props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "1000");
          
          // 消费者会话超时时间
          props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "10000");
          // 消费者key和value反序列化接口
          props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
          props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
          
          // Kafka数据消费对象
          KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
          // 指定主题的指定分区列表消费
          TopicPartition partition0 = new TopicPartition(topic, 0);
          TopicPartition partition1 = new TopicPartition(topic, 1);
          // 分配分区消息
          consumer.assign(Arrays.asList(partition0, partition1));
          
          // 死循环读取
          while(true){
              // Consumer读取partition中的数据是通过调用发起一个fetch请求来执行的
              // 等100毫秒
              ConsumerRecords<String, String> records = consumer.poll(100);
              for(ConsumerRecord<String, String> record : records){
                   System.out.printf("收到消息: offset:%d, key:%s, value:%s%n\n", record.offset(), record.key(), record.value());
              }
          }
     }
}

注意:
(1)自动提交偏移量,可能导致数据丢失
(2)手动提价偏移量,可能导致数据重复消费
(3)API可以自动分配主题的分区进行消费,也可以手动指定特定的分区进行消费
(4)heartbeat.interval.ms心跳时间要介于消费者最小会话超时和最大会话超时之间,最好是当前会话消费时间(session.timeout.ms)的三分之一大小。
(5)生产者API接口是线程安全的,但是消费者API不是线程安全的,需要自己加锁控制,如果需要中断消费者消费可以使用wakeup()方法,但会抛出WakeupException异常。

消费者消费API中poll(long)与poll(Duration)的区别

consumer.poll之后assignment()返回为空的问题,如下面这段代码所示:
consumer.subscribe(Arrays.asList(“test”));
consumer.poll(Duration.ofMillis(0));
// consumer.poll(0);
Set assignment = consumer.assignment(); // empty!

有意思的是,如果是consumer.poll(0);则assignment不为空。之前我以为poll(long)被标记为“Deprecated”之后使用poll(Duration)是相同的效果,现在看来两者还是要有差别的。为什么poll(0)就能获取到consumer分配方案,而使用poll(Duration)就不能呢?
调研了一番之后发现原因如下:
在poll(0)中consumer会一直阻塞直到它成功获取了所需的元数据信息,之后它才会发起fetch请求去获取数据。虽然poll可以指定超时时间,但这个超时时间只适用于后面的消息获取,前面更新元数据信息不计入这个超时时间。poll(Duration)这个版本修改了这样的设计,会把元数据获取也计入整个超时时间。由于本例中使用的是0,即瞬时超时,因此consumer根本无法在这么短的时间内连接上coordinator,所以只能赶在超时前返回一个空集合。这就是为什么使用不同版本的poll命令assignment不同的原因。
poll(0)这种设计的一个问题在于如果远端的broker不可用了, 那么consumer程序会被无限阻塞下去。用户指定了超时时间但却被无限阻塞,显然这样的设计时有欠缺的。特别是对于Kafka Streams而言,这个设计可能导致的问题在于Stream Thread无法正常关闭。目前源代码中依然有一些无限阻塞的场景,比如之前处理的initTransaction,commitTransaction和abortTransaction也是无限等待。看来后面社区还是需要慢慢地将它们都替换掉,毕竟在分布式系统中没有什么场景是需要绝对地等待的。

实现效果,左边为生产,右边有消费:

java 监控kafka消费 java kafka 消费_Group_03


参考资料

https://notafraid.iteye.com/blog/2234386



https://www.jianshu.com/p/c824ac773f4e https://www.jianshu.com/p/8b8206412602 https://www.jianshu.com/p/ec1063a13b53