为什么使用高级消费者(High Level Consumer)
有时,我们消费Kafka的消息,并不关心偏移量,我们仅仅关心数据能被消费就行。High Level Consumer(高级消费者)提供了消费信息的方法而屏蔽了大量的底层细节。
首先要知道的是,高级消费者在zookeeper的特定分区存储最后的偏离。这个偏移当kafka启动时准备完毕。这一般是指消费者组(Consumer group)。
请小心,对于kafka集群消费群体的名字是全局的,任何的“老”逻辑的消费者应该被关闭,然后运行新的代码。当一个新的进程拥有相同的消费者群的名字,kafka将会增加进程的线程消费topic并且引发的“重新平衡(reblannce)”。在这个重新平衡中,kafka将分配现有分区到所有可用线程,可能移动一个分区到另一个进程的消费分区。如果此时同时拥有旧的的新的代码逻辑,将会有一部分逻辑进入旧得Consumer而另一部分进入新的Consumer中的情况.
设计一个高级消费者(Designing a High Level Consumer)
了解使用高层次消费者的第一件事是,它可以(而且应该!)是一个多线程的应用。线程围绕在你的主题分区的数量,有一些非常具体的规则:
- 如果你提供比在topic分区多的线程数量,一些线程将永远不会看到消息。
- 如果你提供的分区比你拥有的线程多,线程将从多个分区接收数据。
- 如果你每个线程上有多个分区,对于你以何种顺序收到消息是没有保证的。举个例子,你可能从分区10上获取5条消息和分区11上的6条消息,然后你可能一直从10上获取消息,即使11上也拥有数据。
- 添加更多的进程线程将使kafka重新平衡,可能改变一个分区到线程的分配。
这里是一个简单的消费者例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package com.test.groups; import kafka.consumer.ConsumerIterator; import kafka.consumer.KafkaStream; public class ConsumerTest implements Runnable { private KafkaStream m_stream; private int m_threadNumber; public ConsumerTest(KafkaStream a_stream, int a_threadNumber) { m_threadNumber = a_threadNumber; m_stream = a_stream; } public void run() { ConsumerIterator< byte [], byte []> it = m_stream.iterator(); while (it.hasNext()) System.out.println( "Thread " + m_threadNumber + ": " + new String(it.next().message())); System.out.println( "Shutting down Thread: " + m_threadNumber); } } |
这里最有趣的是 while (it.hasNext()) ,基本上这段代码一直向kafka读取消息,直到你停止它。
配置测试应用(Configuring the test application)
不像simpleconsumer高层消费者为你很多的提供需要bookkeeping和错误处理。但是你要告诉kafka这些信息。下面的方法定义了创建高级消费者基础配置:
1 2 3 4 5 6 7 8 9 | private static ConsumerConfig createConsumerConfig(String a_zookeeper, String a_groupId) { Properties props = new Properties(); props.put( "zookeeper.connect" , a_zookeeper); props.put( "group.id" , a_groupId); props.put( "zookeeper.session.timeout.ms" , "400" ); props.put( "zookeeper.sync.time.ms" , "200" ); props.put( "auto.commit.interval.ms" , "1000" ); return new ConsumerConfig(props); } |
zookeeper.connect 指定zookeeper集群中的一个实例,kafka利用zookeeper储存topic的分区偏移值。
Groupid 消费者所属的Consumer Group(消费者群)。
zookeeper.session.timeout.ms zookeeper的超时处理。
auto.commit.interval.ms 属性自动提交的间隔。这将替代消息被消费后提交。如果发生错误,你将从新获得未更新的消息。
创建线程池
这个例子使用了Java java.util.concurrent包的线程管理,因为它使创建一个线程池非常简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public void run( int a_numThreads) { Map<String, Integer> topicCountMap = new HashMap<String, Integer>(); topicCountMap.put(topic, new Integer(a_numThreads)); Map<String, List<KafkaStream< byte [], byte []>>> consumerMap = consumer.createMessageStreams(topicCountMap); List<KafkaStream< byte [], byte []>> streams = consumerMap.get(topic); // now launch all the threads // executor = Executors.newFixedThreadPool(a_numThreads); // now create an object to consume the messages // int threadNumber = 0 ; for ( final KafkaStream stream : streams) { executor.submit( new ConsumerTest(stream, threadNumber)); threadNumber++; } } |
首先我们创建一个map,告诉kafka提供给哪个topic多少线程。consumer.createmessagestreams是我们如何把这个信息传递给卡夫卡。返回的是一个包含kafkastream 的以topic 为键list的map结合。(注意,这里我们只向卡夫卡注册一个话题,但我们可以为map中多添加一个元素的)
最后,我们创建的线程池和通过一项新的consumertest对象,每个线程运转我们的业务逻辑。
清理和异常处理
Kafka在每次处理后不会立即更新zookeeper上的偏移值,她会休息上一段时间后提交。在这段时间内,你的消费者可能已经消费了一些消息,但并没有提交到zookeeper上。这样你可能会重复消费数据。
同时一些时候,broker失败从新选取leader是也可能会导致重复消费消息。
为了避免这种情况应该清理完成后再关闭,而不是直接使用kill -9命令。
这里的例子是休息10秒后再关闭。
1 2 3 4 5 6 | try { Thread.sleep( 10000 ); } catch (InterruptedException ie) { } example.shutdown(); |
运行示例
此处的启动命令需提供:
- ZooKeeper连接的字符串和端口号
- 使用这一过程的消费群的名字
-
Topic的消费消息
- 消费topic的线程数
例如:
1 | server01.myco.com1: 2181 group3 myTopic 4 |
连接server01.myco.com端口2181的zookeeper。并要求从Topic的myTopic所有分区,并通过4个线程消费。这个例子的消费群是group3。
完整源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | package com.test.groups; import kafka.consumer.ConsumerConfig; import kafka.consumer.KafkaStream; import kafka.javaapi.consumer.ConsumerConnector; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ConsumerGroupExample { private final ConsumerConnector consumer; private final String topic; private ExecutorService executor; public ConsumerGroupExample(String a_zookeeper, String a_groupId, String a_topic) { consumer = kafka.consumer.Consumer.createJavaConsumerConnector( createConsumerConfig(a_zookeeper, a_groupId)); this .topic = a_topic; } public void shutdown() { if (consumer != null ) consumer.shutdown(); if (executor != null ) executor.shutdown(); } public void run( int a_numThreads) { Map<String, Integer> topicCountMap = new HashMap<String, Integer>(); topicCountMap.put(topic, new Integer(a_numThreads)); Map<String, List<KafkaStream< byte [], byte []>>> consumerMap = consumer.createMessageStreams(topicCountMap); List<KafkaStream< byte [], byte []>> streams = consumerMap.get(topic); // now launch all the threads // executor = Executors.newFixedThreadPool(a_numThreads); // now create an object to consume the messages // int threadNumber = 0 ; for ( final KafkaStream stream : streams) { executor.submit( new ConsumerTest(stream, threadNumber)); threadNumber++; } } private static ConsumerConfig createConsumerConfig(String a_zookeeper, String a_groupId) { Properties props = new Properties(); props.put( "zookeeper.connect" , a_zookeeper); props.put( "group.id" , a_groupId); props.put( "zookeeper.session.timeout.ms" , "400" ); props.put( "zookeeper.sync.time.ms" , "200" ); props.put( "auto.commit.interval.ms" , "1000" ); return new ConsumerConfig(props); } public static void main(String[] args) { String zooKeeper = args[ 0 ]; String groupId = args[ 1 ]; String topic = args[ 2 ]; int threads = Integer.parseInt(args[ 3 ]); ConsumerGroupExample example = new ConsumerGroupExample(zooKeeper, groupId, topic); example.run(threads); try { Thread.sleep( 10000 ); } catch (InterruptedException ie) { } example.shutdown(); } } |
@半兽人,我的kafka 0.8版本,4个节点的集群,一个分区。使用高级API消费的时候吞吐量只有1000每秒,这个可能是什么原因啊?以下是关键代码:
//group 代表一个消费组
originalProps.put("group.id", "router1");
//zk连接超时时间
originalProps.put("zookeeper.session.timeout.ms", "40000");
//zk同步时间
originalProps.put("zookeeper.sync.time.ms", "200");
//自动提交间隔时间
originalProps.put("auto.commit.interval.ms", "10000");
//消息日志自动偏移量,防止宕机后数据无法读取
originalProps.put("auto.offset.reset", "smallest");
//序列化类
originalProps.put("serializer.class", "kafka.serializer.StringEncoder");
哥们你找到问题了吗?
建议你们在测试的时候,不要带逻辑、