手记

kafka消费者

一、消费组和消费者

  1. 每个consumer客户端被创建时,会向zookeeper注册自己的信息;

  2. 此作用主要是为了"负载均衡"。

  3. 同一个Consumer Group中的Consumers,Kafka将相应Topic中的每个消息只发送给其中一个Consumer。

  4. Consumer Group中的每个Consumer读取Topic的一个或多个Partitions,并且是唯一的Consumer;

  5. 一个Consumer group的多个consumer的所有线程依次有序地消费一个topic的所有partitions,如果Consumer group中所有consumer总线程大于partitions数量,则会出现空闲情况;

     举例说明:
     kafka集群中创建一个topic为report-log   4 partitions 索引编号为0,1,2,3
     假如有目前有三个消费者node:注意-->一个consumer中一个消费线程可以消费一个或多个partition.
    如果每个consumer创建一个consumer thread线程,各个node消费情况如下,node1消费索引编号为0,1分区,node2费索引编号为2,node3费索引编号为3
     如果每个consumer创建2个consumer thread线程,各个node消费情况如下(是从consumer node先后启动状态来确定的),node1消费索引编号为0,1分区;node2费索引编号为2,3;node3为空闲状态
     总结:
              从以上可知,Consumer Group中各个consumer是根据先后启动的顺序有序消费一个topic的所有partitions的。

              如果Consumer Group中所有consumer的总线程数大于partitions数量,则可能consumer thread或consumer会出现空闲状态。

     转载地址:https://www.cnblogs.com/qingyunzong/p/9007107.html

二、订阅主题

       在实例化一个消费者之后,我们需要为该消费者订阅主题。一个消费者可以同时订阅多个主题,通常我们可以以集合的形式指定多个主题,或者以正则表达式形式订阅特定模式的主题。

Kafka 定义了以下 3 种订阅主题方法:

  • subscribe(Collection<String> topics)方法,以集合形式指定消费者订阅的主题,通常我们用ArrayList。

  • subscribe(Collection<String> topics, ConsumerRebalanceListener listener)方法,订阅主题时指定一个监听器,用于在消费者发生平衡操作时回调进行相应的业务处理。

  • subscribe(Pattern pattern, ConsumerRebalanceListener listener)方法,以正则表达式形式指定匹配特定模式的主题。

消费端的普通实现:

public void TestKafkaConsumer(){Properties props = new Properties();props.put("bootstrap.servers", "localhost:9092");props.put("group.id", "test-1");props.put("enable.auto.commit", "true");props.put("auto.commit.interval.ms", "1000");props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);consumer.subscribe(Arrays.asList("my-topic", "bar"));while (true) {ConsumerRecords<String, String> records = consumer.poll(100);for (ConsumerRecord<String, String> record : records) {process(record);}}}

 在回调接口ConsumerRebalanceListener中定义了两个回调方法:

  1. onPartitionsRevoked(Collection<TopicPartition> partitions):在这里我们可以提交偏移量操作以避免数据重复消费。

  2. onPartitionsAssigned(Collection<TopicPartition> partitions):这个方法在平衡之后、消费者开始拉去消息之前被调用,一般在该方法中保证各消费者回滚到正确的偏移量,即重置各消费者消费偏移量。

public class JavaKafkaConsumer {static Properties props = new Properties();static {props.put("bootstrap.servers", "192.168.17.64:9092");props.put("group.id", "test");props.put("client.id", "test");props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");}public static void main(String[] args) {// 1.初始化消费者final KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);// 2.订阅主题,指定一个监听器,用于在消费者发生平衡操作时回调响应的业务处理consumer.subscribe(Arrays.asList("java-topic_test"), new ConsumerRebalanceListener() {public void onPartitionsRevoked(Collection<TopicPartition> collection) {consumer.commitAsync(); // 提交偏移量}public void onPartitionsAssigned(Collection<TopicPartition> partitions) {// 获取该分区下已消费的偏移量long commitedOffset = -1;for (TopicPartition topicPartition : partitions) {// 获取该分区下已消费的偏移量commitedOffset = consumer.committed(topicPartition).offset();// 重置偏移量到上一次提交的偏移量下一个位置处开始消费consumer.seek(topicPartition, commitedOffset + 1);}}});try {while (true) {// 长轮询拉取消息ConsumerRecords<String, String> records = consumer.poll(1000);for (ConsumerRecord<String , String> record : records) {System.out.printf("partition = %d, offset = %d,key= %s value = %s%n",record.partition(), record.offset(),record.key(),record.value());}}} catch (Exception e) {e.printStackTrace();} finally {consumer.close();}}}

 

三、订阅指定分区

      可以通过subscribe()方法订阅主题,也可以某些主题的特定分区。Kafka提供了assign(Collection<TopicPartition> partitions)方法来订阅指定分区。

      例如,我们指定消费者只订阅主题“test”的编号为0和1的分区:

consumer.assign(Arrays.asList(new TopicPartition("test", 0),new TopicPartition("test", 1) ));

向特定的分区订阅消息,会失去partion的负载分担。在以下几种场景可以使用: 
1. 只需要获取本机磁盘的分区数据; 
2. 程序自己或者外部程序能够自己实现负载和错误处理。例如YARN/Mesos的介入,当consumer挂掉后,再启动一个consumer。

String topic = "foo";TopicPartition partition0 = new TopicPartition(topic, 0);TopicPartition partition1 = new TopicPartition(topic, 1);consumer.assign(Arrays.asList(partition0, partition1));

说明: 
1. 此种情况用了consumer Group,也不会做负载均衡。 
2. topic的订阅和分区订阅不可以在同一consumer中混用。

 

四、提交和偏移量

1、消费者为什么要提交偏移量

       当消费者崩溃或者有新的消费者加入,那么就会触发再均衡(rebalance),完成再均衡后,每个消费者可能会分配到新的分区,而不是之前处理那个,为了能够继续之前的工作,消费者需要读取每个partition最后一次提交的偏移量,然后从偏移量指定的地方继续处理。

2、提交偏移量可能带来的问题

case1:如果提交的偏移量小于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息就会被重复处理。

case2:如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失。

3、提交偏移量的方式

       Kafka消费者消费位移确认有自动提交与手动提交两种策略。在创建KafkaConsumer对象时,通过参数enable.auto.commit设定,true表示自动提交(默认)。自动提交策略由消费者协调器(ConsumerCoordinator)每隔${auto.commit.interval.ms}毫秒执行一次偏移量的提交。手动提交需要由客户端自己控制偏移量的提交。

      (1) 自动提交offset

        enable.auto.commit设置成true(默认为true),那么每过5s,消费者自动把从poll()方法接收到的最大的偏移量提交。提交的时间间隔由auto.commit.interval.ms控制,默认是5s。

        自动提交的优点是方便,但是可能会重复处理消息

        例如,我们创建一个消费者,该消费者自动提交偏移量,采用显示设置为自动。

Properties props = new Properties();/* 定义kakfa 服务的地址,不需要将所有broker指定上 */props.put("bootstrap.servers", "localhost:9092");/* 指定consumer group */props.put("group.id", "test");/* 显示设置偏移量offset自动提交 */props.put("enable.auto.commit", "true");/* 设置偏移量自动提交时间间隔 */props.put("auto.commit.interval.ms", "1000");props.put("session.timeout.ms", "30000");/* key的序列化类 */props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");/* value的序列化类 */props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");/* 定义consumer */KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);/* 消费者订阅的topic, 可同时订阅多个 */consumer.subscribe(Arrays.asList("foo", "bar"));/* 读取数据,读取超时时间为100ms */while (true) {ConsumerRecords<String, String> records = consumer.poll(100);for (ConsumerRecord<String, String> record : records)System.out.printf("offset = %d, key = %s, value = %s", record.offset(), record.key(), record.value());}

      说明: 
      1. bootstrap.servers 只是代表kafka的连接入口,只需要指定集群中的某一broker; 
      2. 一旦consumer和kakfa集群建立连接,consumer会以心跳的方式来告诉集群自己还活着,如果session.timeout.ms 内心跳未到达服务器,服务器认为心跳丢失,会做rebalence。

    (2)手动提交

      在有些场景我们可能对消费偏移量有更精确的管理,以保证消息不被重复消费以及消息不被丢失。假设我们对拉取到的消息需要进行持久化写入数据库处理,或者用于其他网络访问请求等等复杂的业务处理,在这种场景下,所有的业务处理完成后才认为消息被成功消费,这种场景下,我们必须手动控制偏移量的提交。若是自动提交offset的情况下,如果数据从kafka集群读出,就确认,但是持久化过程失败,就会导致数据丢失。我们就需要控制offset的确认。

      Kafka 提供了异步提交(commitAsync)及同步提交(commitSync)两种手动提交的方式。两者的主要区别在于同步模式下提交失败时一直尝试提交,直到遇到无法重试的情况下才会结束,同时,同步方式下消费者线程在拉取消息时会被阻塞,直到偏移量提交操作成功或者在提交过程中发生错误。而异步方式下消费者线程不会被阻塞,可能在提交偏移量操作的结果还未返回时就开始进行下一次的拉取操作,在提交失败时也不会尝试提交。

     实现手动提交前需要在创建消费者时关闭自动提交,即设置enable.auto.commit=false。然后在业务处理成功后调用commitAsync()或commitSync()方法手动提交偏移量。由于同步提交会阻塞线程直到提交消费偏移量执行结果返回,而异步提交并不会等消费偏移量提交成功后再继续下一次拉取消息的操作,因此异步提交还提供了一个偏移量提交回调的方法commitAsync(OffsetCommitCallback callback)。当提交偏移量完成后会回调OffsetCommitCallback 接口的onComplete()方法,这样客户端根据回调结果执行不同的逻辑处理。

  • 同步提交:

      将enable.auto.commit设置成false,让应用程序决定何时提交偏移量。commitSync()提交由poll()方法返回的最新偏移量,所以在处理完所有消息后要确保调用commitSync,否则会有消息丢失的风险。commitSync在提交成功或碰到无法恢复的错误之前,会一直重试。如果发生了再均衡,从最近一批消息到发生再均衡之间的所有消息都会被重复处理。

不足:broker在对提交请求作出回应之前,应用程序会一直阻塞,会限制应用程序的吞吐量。

Properties props = new Properties();props.put("bootstrap.servers", "localhost:9092");props.put("group.id", "test");/* 关闭自动确认选项 */props.put("enable.auto.commit", "false");props.put("auto.commit.interval.ms", "1000");props.put("session.timeout.ms", "30000");props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);consumer.subscribe(Arrays.asList("foo", "bar"));final int minBatchSize = 200;List<ConsumerRecord<String, String>> buffer = new ArrayList<>();while (true) {// 等待拉取消息ConsumerRecords<String, String> records = consumer.poll(100);for (ConsumerRecord<String, String> record : records) {buffer.add(record);}/* 数据达到批量要求,就写入DB,成功后提交offset */if (buffer.size() >= minBatchSize) {insertIntoDb(buffer);try {//处理完当前批次的消息,在轮询更多的消息之前,调用commitSync方法提交当前批次最新的消息consumer.commitSync();buffer.clear();}catch (CommitFailedException e) {//只要没有发生不可恢复的错误,commitSync方法会一直尝试直至提交成功。如果提交失败,我们也只能把异常记录到错误日志里log.error("commit failed", e);}}}
  • 异步提交:

       异步提交的commitAsync,只管发送提交请求,无需等待broker响应。commitAsync提交之后不进行重试,假设要提交偏移量2000,这时候发生短暂的通信问题,服务器接收不到提交请求,因此也就不会作出响应。与此同时,我们处理了另外一批消息,并成功提交了偏移量3000,。如果commitAsync重新尝试提交2000,那么它有可能在3000之后提交成功,这个时候如果发生再均衡,就会出现重复消息。

Properties props = new Properties();props.put("bootstrap.servers", "localhost:9092");props.put("group.id", "test");props.put("client.id", "test");// 为了便于测试,这里设置一次fetch 请求取得的数据最大值为1KB,默认是5MBprops.put("fetch.max.bytes", 1024);props.put("enable.auto.commit", false);// 设置手动提交偏移量props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);// 订阅主题consumer.subscribe(Arrays.asList("test"));try {int minCommitSize = 10;// 最少处理10 条消息后才进行提交int icount = 0 ;// 消息计算器while (true) {// 等待拉取消息ConsumerRecords<String, String> records = consumer.poll(1000);for (ConsumerRecord<String, String> record : records) {// 简单打印出消息内容,模拟业务处理System.out.printf("partition = %d, offset = %d,key= %s value = %s%n", record. partition(), record.offset(), record.key(),record.value());icount++;}// 在业务逻辑处理成功后提交偏移量if (icount >= minCommitSize){consumer.commitAsync(new OffsetCommitCallback() {@Overridepublic void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {if (null == exception) {// TODO 表示偏移量成功提交System.out.println("提交成功");} else {// TODO 表示提交偏移量发生了异常,根据业务进行相关处理System.out.println("发生了异常");}}});icount=0; // 重置计数器}}} catch(Exception e){// TODO 异常处理e.printStackTrace();} finally {consumer.close();
  •   同步和异步组合提交

        一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大的问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但是如果在关闭消费者或再均衡前的最后一次提交,就要确保提交成功。

       因此,在消费者关闭之前一般会组合使用commitAsync和commitSync提交偏移量。

try {  while (true) {   ConsumerRecords<String, String> records = consumer.poll(100);   for (ConsumerRecord<String, String> record : records) {       System.out.println("topic = %s, partition = %s, offset = %d,                   customer = %s, country = %s\n", record.topic(),     record.partition(), record.offset(), record.key(),    record.value());  }//如果一切正常,我们使用commitAsync来提交,这样速度更快,而且即使这次提交失败,下次提交很可能会成功consumer.commitAsync();} catch (CommitFailedException e) {log.error("commit failed", e);} finally {try {//关闭消费者前,使用commitSync,直到提交成成功或者发生无法恢复的错误    consumer.commitSync();   } finally {    consumer.close();   }}

     (3) 提交特定的偏移量

       消费者API允许调用commitSync()和commitAsync()方法时传入希望提交的partition和offset的map,即提交特定的偏移量。

//用于跟踪偏移量的mapprivate Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();int count = 0;while (true) {ConsumerRecords<String, String> records = consumer.poll(100);for (ConsumerRecord<String, String> record : records) {System.out.println("topic = %s, partition = %s, offset = %d,                 customer = %s, country = %s\n", record.topic(),record.partition(), record.offset(), record.key(),record.value());//模拟对消息的处理//在读取每条消息后,使用期望处理的下一个消息的偏移量更新map里的偏移量。下一次就从这里开始读取消息currentOffsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1, “no matadata”));if (count++ % 1000 == 0) {//每处理1000条消息就提交一次偏移量,在实际应用中,可以根据时间或者消息的内容进行提交consumer.commitAsync(currentOffsets, null);}}}

     (4) 提交具体分区具体offset

try {while(running) {ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);for (TopicPartition partition : records.partitions()) {List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);for (ConsumerRecord<String, String> record : partitionRecords) {System.out.println(record.offset() + ": " + record.value());}/* 同步确认某个分区的特定offset */long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));}}} finally {consumer.close();}

        说明:确认的offset为已接受数据最大offset+1。

     (5) 再均衡监听器

      在为消费者分配新的partition或者移除旧的partition时,可以通过消费者API执行一些应用程序代码,在使用subscribe()方法时传入一个ConsumerRebalanceListener实例。

     ConsumerRebalanceListener需要实现的两个方法:

    1) public void onPartitionRevoked(Collection<TopicPartition> partitions)方法会在再均衡开始之前和消费者停止读取消息之后被调用。如果在这里提交偏移量,下一个接管partition的消费者就知道该从哪里开始读取了。

    2) public void onPartitionAssigned(Collection<TopicPartition> partitions)方法会在重新分配partition之后和消费者开始读取消息之前被调用。

private Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();//用于跟踪偏移量的mapprivate class HandleRebalance implements ConsumerRebalanceListener {@Overridepublic void onPartitionsAssigned(Collection<TopicPartition> partitions) {}@Overridepublic void onPartitionsRevoked(Collection<TopicPartition> partitions) {//如果发生再均衡,要在即将失去partition所有权时提交偏移量。//注意:(1)提交的是最近处理过的偏移量,而不是批次中还在处理的最后一个偏移量。因为partition有可能在我们还在处理消息时被撤回。//(2)我们要提交所有分区的偏移量,而不只是即将市区所有权的分区的偏移量。因为提交的偏移量是已经处理过的,所以不会有什么问题。//(3)调用commitSync方法,确保在再均衡发生之前提交偏移量consumer.commitSync(currentOffsets);}}try{consumer.subscribe(topics, new HandleRebalance());while (true) {ConsumerRecords<String, String> records = consumer.poll(100);for (ConsumerRecord<String, String> record : records) {System.out.println("topic = %s, partition = %s, offset = %d,                           customer = %s, country = %s\n", record.topic(),record.partition(), record.offset(), record.key(),record.value());//模拟对消息的处理//在读取每条消息后,使用期望处理的下一个消息的偏移量更新map里的偏移量。下一次就从这里开始读取消息currentOffsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1, “no matadata”));}consumer.commitAsync(currentOffsets, null);} catch(WakeupException e) {//忽略异常,正在关闭消费者} catch (Exception e) {log.error("unexpected error", e);} finally {try{consumer.commitSync(currentOffsets);} finally {consumer.close();}}}

下面的例子演示如何在失去partition的所有权之前通过onPartitionRevoked()方法来提交偏移量。

     (6) 外部存储offset

       消费者可以自定义kafka的offset存储位置。该设计的主要目的是让消费者将数据和offset进行原子性的存储。这样可以避免上面提到的重复消费问题。举例说明: 
       订阅特定分区。存储所获得的记录时,将每条记录的offset一起存储。保证数据和offset的存储是原子性的。当异步存储被异常打断时,凡已经存储的数据,都有有相应的offset记录。这种方式可以保证不会有数据丢失,也不会重复的从服务端读取。


      如何配置实现: 
      1. 去使能offset自动确认:enable.auto.commit=false; 
      2. 从ConsumerRecord中获取offset,保存下来; 
      3. Consumer重启时,调用seek(TopicPartition, long)重置在服务端的消费记录。

       如果消费分区也是自定义的,这种方式用起来会很爽。如果分区是自动分配的,当分区发生reblance的时候,就要考虑清楚了。如果因为升级等原因,分区漂移到一个不会更新offset的consumer上。 

该情况下: 
1. 原consumer需要监听分区撤销事件,并在撤销时确认好offset。接口:ConsumerRebalanceListener.onPartitionsRevoked(Collection);
2. 新consumer监听分区分配事件,获取当前分区消费的offset。接口:ConsumerRebalanceListener.onPartitionsAssigned(Collection); 
3. consumer监听到 ConsumerRebalance事件,还没有处理或者持久化的缓存数据flush掉。

    (7) 控制消费位置

    大多数情况下,服务端的Consumer的消费位置都是由客户端间歇性的确认。Kafka允许Consumer自己设置消费起点,达到的效果: 
    1. 可以消费已经消费过的数据; 
    2. 可以跳跃性的消费数据; 
    看下这样做的一些场景: 
    1. 对Consumer来说,数据具备时效性,只需要获取最近一段时间内的数据,就可以进行跳跃性的获取数据; 
    2. 上面自己存offset的场景,重启后就需要从指定的位置开始消费。 
    接口上面已经提到过了,用seek(TopicPartition, long)。

 

五、SimpleConsumer查询

import java.nio.ByteBuffer;import java.util.ArrayList;import java.util.Collections;import java.util.HashMap;import java.util.List;import java.util.Map;import kafka.api.FetchRequest;import kafka.api.FetchRequestBuilder;import kafka.api.PartitionOffsetRequestInfo;import kafka.common.ErrorMapping;import kafka.common.TopicAndPartition;import kafka.javaapi.FetchResponse;import kafka.javaapi.OffsetResponse;import kafka.javaapi.PartitionMetadata;import kafka.javaapi.TopicMetadata;import kafka.javaapi.TopicMetadataRequest;import kafka.javaapi.consumer.SimpleConsumer;import kafka.message.MessageAndOffset;public class SimpleExample {	private List<String> m_replicaBrokers = new ArrayList<String>();	public SimpleExample() {		m_replicaBrokers = new ArrayList<String>();	}	public static void main(String args[]) {		SimpleExample example = new SimpleExample();		// 最大读取消息数量		long maxReads = Long.parseLong("3");		// 要订阅的topic		String topic = "mytopic";		// 要查找的分区		int partition = Integer.parseInt("0");		// broker节点的ip		List<String> seeds = new ArrayList<String>();		seeds.add("192.168.4.30");		seeds.add("192.168.4.31");		seeds.add("192.168.4.32");		// 端口		int port = Integer.parseInt("9092");		try {			example.run(maxReads, topic, partition, seeds, port);		} catch (Exception e) {			System.out.println("Oops:" + e);			e.printStackTrace();		}/*  kafka获得partition下标,需要用到kafka的simpleconsumer                TreeMap<Integer,PartitionMetadata> metadatas = kot.findLeader(seeds, port, topic);                            int sum = 0;                            for (Entry<Integer,PartitionMetadata> entry : metadatas.entrySet()) {                      int partition = entry.getKey();                      String leadBroker = entry.getValue().leader().host();                      String clientName = "Client_" + topic + "_" + partition;                      SimpleConsumer consumer = new SimpleConsumer(leadBroker, port, 100000,                      64 * 1024, clientName);                      long readOffset = getLastOffset(consumer, topic, partition,                                  kafka.api.OffsetRequest.LatestTime(), clientName);                                  sum += readOffset;                      System.out.println(partition+":"+readOffset);                      if(consumer!=null)consumer.close();                  }                  System.out.println("总和:"+sum);                */  	}	public void run(long a_maxReads, String a_topic, int a_partition, List<String> a_seedBrokers, int a_port) throws Exception {		// 获取指定Topic partition的元数据		PartitionMetadata metadata = findLeader(a_seedBrokers, a_port, a_topic, a_partition);		if (metadata == null) {			System.out.println("Can't find metadata for Topic and Partition. Exiting");			return;		}		if (metadata.leader() == null) {			System.out.println("Can't find Leader for Topic and Partition. Exiting");			return;		}		String leadBroker = metadata.leader().host();		String clientName = "Client_" + a_topic + "_" + a_partition;		SimpleConsumer consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName);		long readOffset = getLastOffset(consumer, a_topic, a_partition, kafka.api.OffsetRequest.EarliestTime(), clientName);		int numErrors = 0;		while (a_maxReads > 0) {			if (consumer == null) {				consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName);			}			FetchRequest req = new FetchRequestBuilder().clientId(clientName).addFetch(a_topic, a_partition, readOffset, 100000).build();			FetchResponse fetchResponse = consumer.fetch(req);			if (fetchResponse.hasError()) {				numErrors++;				// Something went wrong!				short code = fetchResponse.errorCode(a_topic, a_partition);				System.out.println("Error fetching data from the Broker:" + leadBroker + " Reason: " + code);				if (numErrors > 5)					break;				if (code == ErrorMapping.OffsetOutOfRangeCode()) {					// We asked for an invalid offset. For simple case ask for					// the last element to reset					readOffset = getLastOffset(consumer, a_topic, a_partition, kafka.api.OffsetRequest.LatestTime(), clientName);					continue;				}				consumer.close();				consumer = null;				leadBroker = findNewLeader(leadBroker, a_topic, a_partition, a_port);				continue;			}			numErrors = 0;			long numRead = 0;			for (MessageAndOffset messageAndOffset : fetchResponse.messageSet(a_topic, a_partition)) {				long currentOffset = messageAndOffset.offset();				if (currentOffset < readOffset) {					System.out.println("Found an old offset: " + currentOffset + " Expecting: " + readOffset);					continue;				}				readOffset = messageAndOffset.nextOffset();				ByteBuffer payload = messageAndOffset.message().payload();				byte[] bytes = new byte[payload.limit()];				payload.get(bytes);				System.out.println(String.valueOf(messageAndOffset.offset()) + ": " + new String(bytes, "UTF-8"));				numRead++;				a_maxReads--;			}			if (numRead == 0) {				try {					Thread.sleep(1000);				} catch (InterruptedException ie) {				}			}		}		if (consumer != null)			consumer.close();	}	public static long getLastOffset(SimpleConsumer consumer, String topic, int partition, long whichTime, String clientName) {		TopicAndPartition topicAndPartition = new TopicAndPartition(topic, partition);		Map<TopicAndPartition, PartitionOffsetRequestInfo> requestInfo = new HashMap<TopicAndPartition, PartitionOffsetRequestInfo>();		requestInfo.put(topicAndPartition, new PartitionOffsetRequestInfo(whichTime, 1));		kafka.javaapi.OffsetRequest request = new kafka.javaapi.OffsetRequest(requestInfo, kafka.api.OffsetRequest.CurrentVersion(), clientName);		OffsetResponse response = consumer.getOffsetsBefore(request);		if (response.hasError()) {			System.out.println("Error fetching data Offset Data the Broker. Reason: " + response.errorCode(topic, partition));			return 0;		}		long[] offsets = response.offsets(topic, partition);// long[] offsets2 = response.offsets(topic, 3);  		return offsets[0];	}	/**	 * @param a_oldLeader	 * @param a_topic	 * @param a_partition	 * @param a_port	 * @return String	 * @throws Exception	 *             找一个leader broker	 */	private String findNewLeader(String a_oldLeader, String a_topic, int a_partition, int a_port) throws Exception {		for (int i = 0; i < 3; i++) {			boolean goToSleep = false;			PartitionMetadata metadata = findLeader(m_replicaBrokers, a_port, a_topic, a_partition);			if (metadata == null) {				goToSleep = true;			} else if (metadata.leader() == null) {				goToSleep = true;			} else if (a_oldLeader.equalsIgnoreCase(metadata.leader().host()) && i == 0) {				// first time through if the leader hasn't changed give				// ZooKeeper a second to recover				// second time, assume the broker did recover before failover,				// or it was a non-Broker issue				//				goToSleep = true;			} else {				return metadata.leader().host();			}			if (goToSleep) {				try {					Thread.sleep(1000);				} catch (InterruptedException ie) {				}			}		}		System.out.println("Unable to find new leader after Broker failure. Exiting");		throw new Exception("Unable to find new leader after Broker failure. Exiting");	}	private PartitionMetadata findLeader(List<String> a_seedBrokers, int a_port, String a_topic, int a_partition) {//TreeMap<Integer, PartitionMetadata> map = new TreeMap<Integer, PartitionMetadata>();		PartitionMetadata returnMetaData = null;		loop: for (String seed : a_seedBrokers) {			SimpleConsumer consumer = null;			try {				consumer = new SimpleConsumer(seed, a_port, 100000, 64 * 1024, "leaderLookup");				List<String> topics = Collections.singletonList(a_topic);				TopicMetadataRequest req = new TopicMetadataRequest(topics);				kafka.javaapi.TopicMetadataResponse resp = consumer.send(req);				List<TopicMetadata> metaData = resp.topicsMetadata();				for (TopicMetadata item : metaData) {					for (PartitionMetadata part : item.partitionsMetadata()) {//map.put(part.partitionId(), part);  						if (part.partitionId() == a_partition) {							returnMetaData = part;							break loop;						}					}				}			} catch (Exception e) {				System.out.println("Error communicating with Broker [" + seed + "] to find Leader for [" + a_topic + ", " + a_partition + "] Reason: " + e);			} finally {				if (consumer != null)					consumer.close();			}		}		if (returnMetaData != null) {			m_replicaBrokers.clear();			for (kafka.cluster.Broker replica : returnMetaData.replicas()) {				m_replicaBrokers.add(replica.host());			}		}		return returnMetaData;// return map;	}}

      67行调用的getLastOffset方法,方法中有一个kafka.api.OffsetRequest.EarliestTime()这样的参数,用这个参数的时候不能及时的返回offset,总是返回0。换成kafka.api.OffsetRequest.LatestTime(),就可以及时的取到offset值了!(https://blog.csdn.net/zhouyu7373/article/details/39025691http://blackproof.iteye.com/blog/2217388

六、以时间戳查询消息

       Kafka 在0.10.1.1 版本增加了时间戳索引文件,因此我们除了直接根据偏移量索引文件查询消息之外,还可以根据时间戳来访问消息。consumer-API 提供了一个offsetsForTimes(Map<TopicPartition, Long> timestampsToSearch)方法,该方法入参为一个Map 对象,Key 为待查询的分区,Value 为待查询的时间戳,该方法会返回时间戳大于等于待查询时间的第一条消息对应的偏移量和时间戳。需要注意的是,若待查询的分区不存在,则该方法会被一直阻塞。

        假设我们希望从某个时间段开始消费,那们就可以用offsetsForTimes()方法定位到离这个时间最近的第一条消息的偏移量,在查到偏移量之后调用seek(TopicPartition partition, long offset)方法将消费偏移量重置到所查询的偏移量位置,然后调用poll()方法长轮询拉取消息。例如,我们希望从主题“stock-quotation”第0 分区距离当前时间相差12 小时之前的位置开始拉取消息。

Properties props = new Properties();props.put("bootstrap.servers", "localhost:9092");props.put("group.id", "test");props.put("client.id", "test");props.put("enable.auto.commit", true);// 显示设置偏移量自动提交props.put("auto.commit.interval.ms", 1000);// 设置偏移量提交时间间隔props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);// 订阅主题consumer.assign(Arrays.asList(new TopicPartition("test", 0)));try {Map<TopicPartition, Long> timestampsToSearch = new HashMap<TopicPartition,Long>();// 构造待查询的分区TopicPartition partition = new TopicPartition("stock-quotation", 0);// 设置查询12 小时之前消息的偏移量timestampsToSearch.put(partition, (System.currentTimeMillis() - 12 * 3600 * 1000));// 会返回时间大于等于查找时间的第一个偏移量Map<TopicPartition, OffsetAndTimestamp> offsetMap = consumer.offsetsForTimes (timestampsToSearch);OffsetAndTimestamp offsetTimestamp = null;// 这里依然用for 轮询,当然由于本例是查询的一个分区,因此也可以用if 处理for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : offsetMap.entrySet()) {// 若查询时间大于时间戳索引文件中最大记录索引时间,// 此时value 为空,即待查询时间点之后没有新消息生成offsetTimestamp = entry.getValue();if (null != offsetTimestamp) {// 重置消费起始偏移量consumer.seek(partition, entry.getValue().offset());}}while (true) {// 等待拉取消息ConsumerRecords<String, String> records = consumer.poll(1000);for (ConsumerRecord<String, String> record : records){// 简单打印出消息内容System.out.printf("partition = %d, offset = %d,key= %s value = %s%n", record.partition(), record.offset(), record.key(),record.value());}}} catch (Exception e) {e.printStackTrace();} finally {consumer.close();}

 

七、控制消费流 Consumption Flow Control

       如果一个consumer同时消费多个分区,默认情况下,这多个分区的优先级是一样的,同时消费。Kafka提供机制,可以让暂停某些分区的消费,先获取其他分区的内容。场景举栗: 
      1. 流式计算,consumer同时消费两个Topic,然后对两个Topic的数据做Join操作。但是这两个Topic里面的数据产生速率差距较大。Consumer就需要控制下获取逻辑,先获取慢的Topic,慢的读到数据后再去读快的。 
       2. 同样多个Topic同时消费,但是Consumer启动是,本地已经存有了大量某些Topic数据。此时就可以优先去消费下其他的Topic。

      调控的手段:让某个分区消费先暂停,时机到了再恢复,然后接着poll。提供 pause(Collection<TopicPartition> partitions)和resume(Collection<TopicPartition>
partitions)方法,分别用来暂停某些分区在拉取操作时返回数据给客户端和恢复某些分区向客户端返回数据操作。通过这两个方法可以对消费速度加以控制,结合业务使用。

 

八、多线程处理模型 Multi-threaded Processing

Kafka的Consumer的接口为非线程安全的。多线程共用IO,Consumer线程需要自己做好线程同步。 
如果想立即终止consumer,唯一办法是用调用接口:wakeup(),使处理线程产生WakeupException。

public class KafkaConsumerRunner implements Runnable {/* 注意,这俩货是类成员变量 */private final AtomicBoolean closed = new AtomicBoolean(false);private final KafkaConsumer consumer;public void run() {try {consumer.subscribe(Arrays.asList("topic"));while (!closed.get()) {ConsumerRecords records = consumer.poll(10000);// Handle new records}} catch (WakeupException e) {// Ignore exception if closingif (!closed.get()) throw e;} finally {consumer.close();}}// Shutdown hook which can be called from a separate threadpublic void shutdown() {closed.set(true);consumer.wakeup();}}

说明: 
1. KafkaConsumerRunner是runnable的,请自觉补脑多线程运行; 
2. 外部线程控制KafkaConsumerRunner线程的停止; 
3. 主要说的是多线程消费同一topic,而不是消费同一分区;

比较一下两种模型:

Consumer单线程模型

优点:实现容易; 
优点:没有线程之间的协作。通常比下面的那种更快; 
优点:单分区数据的顺序处理; 
缺点:多个TCP连接,但是关系不大,kafka对自己的server自信满满; 
缺点:太多的Request可能导致server的吞吐降低一丢丢; 
缺点:consumer数量受到分区数量限制,一个consumer一个分区;

Consumer多线程模型

优点:一个consumer任意多的线程,线程数不用受到分区数限制; 
缺点:如果有保序需求,自己要加控制逻辑; 
缺点:该模型中如果手动offset,自己要加控制逻辑; 
一种可行的解决办法:为每个分区分配独立的存储,获取的数据根据数据所在分区进行hash存储。这样可以解决顺序消费,和offset的确认问题。

疑问:

对比两种线程模型时,应该是有隐藏地图的。 
1. 单线程模型中,多分区情况下,应该说的是每个Consumer独立去消费一个分区; 
2. 多线程模型中,单Consumer消费一个Topic。如果多个线程同时消费同一分区,也就是要公用连接了,各个线程之间要做好同步; 
3. 对于多线程模型下提出的客户端分区数据分开存储,各个分区之间是如何保序的? 

(转载地址:https://blog.csdn.net/xianzhen376/article/details/51167742

 

九、从zookeeper中动态读取kafka集群中brokers

      kafka集群中producer和consumer都依赖于zookeeper来保证系统可用性集群保存一些meta信息。

public class KafkaBrokerInfoFetcher {public static void main(String[] args) throws Exception {ZooKeeper zk = new ZooKeeper("localhost:2181", 10000, null);List<String> ids = zk.getChildren("/brokers/ids", false);for (String id : ids) {String brokerInfo = new String(zk.getData("/brokers/ids/" + id, false, null));System.out.println(id + ": " + brokerInfo);}}}

结果如下:

1: {"jmx_port":-1,"timestamp":"1428512949385","host":"192.168.0.11","version":1,"port":9093}2: {"jmx_port":-1,"timestamp":"1428512955512","host":"192.168.0.11","version":1,"port":9094}3: {"jmx_port":-1,"timestamp":"1428512961043","host":"192.168.0.11","version":1,"port":9095}

转载地址:https://blog.csdn.net/qq_25827845/article/details/56049022

原文出处


0人推荐
随时随地看视频
慕课网APP