跟我学Kafka源码之Consumer分析

在上一章,我们跟踪了Producer源码的整体流程和一些细节,本章我们将重点跟踪Consumer的源码细节。

Consumer的配置文件如下:

Html代码   收藏代码
  1. Kafka Consumer配置:  
  2.   
  3. group.id:                                指定consumer所属的consumer group  
  4. consumer.id:                             如果不指定会自动生成  
  5. socket.timeout.ms:                      网络请求的超时设定  
  6. socket.receive.buffer.bytes:            Socket的接收缓存大小  
  7. fetch.message.max.bytes:                试图获取的消息大小之和(bytes)  
  8. num.consumer.fetchers:                  该消费去获取data的总线程数  
  9. auto.commit.enable:                      如果是true, 定期向zk中更新Consumer已经获取的last message offset(所获取的最后一个batch的first message offset)  
  10. auto.commit.interval.ms:                Consumer向ZK中更新offset的时间间隔  
  11. queued.max.message.chunks:              默认为2  
  12. rebalance.max.retries:                   在rebalance时retry的最大次数,默认为4  
  13. fetch.min.bytes:                         对于一个fetch request, Broker Server应该返回的最小数据大小,达不到该值request会被block, 默认是1字节。  
  14. fetch.wait.max.ms:                        Server在回答一个fetch request之前能block的最大时间(可能的block原因是返回数据大小还没达到fetch.min.bytes规定);  
  15. rebalance.backoff.ms:                    当rebalance发生时,两个相邻retry操作之间需要间隔的时间。  
  16. refresh.leader.backoff.ms:               如 果一个Consumer发现一个partition暂时没有leader, 那么Consumer会继续等待的最大时间窗口(这段时间内会 refresh partition leader);  
  17. auto.offset.reset:                       当发现offset超出合理范围(out of range)时,应该设成的大小(默认是设成offsetRequest中指定的值):  
  18.                                              smallest: 自动把该consumer的offset设为最小的offset;  
  19.                                              largest: 自动把该consumer的offset设为最大的offset;  
  20.                                              anything else: throw exception to the consumer;  
  21. consumer.timeout.ms:                     如果在该规定时间内没有消息可供消费,则向Consumer抛出timeout exception;  
  22.                                              该参数默认为-1, 即不指定Consumer timeout;  
  23. client.id:                               区分不同consumer的ID,默认是group.id  

 

先从一个消费者的demo开始:

 

Java代码   收藏代码
  1. public class ConsumerDemo {  
  2.     private final ConsumerConnector consumer;  
  3.     private final String topic;  
  4.     private ExecutorService executor;  
  5.    
  6.     public ConsumerDemo(String a_zookeeper, String a_groupId, String a_topic) {  
  7.         consumer = Consumer.createJavaConsumerConnector(createConsumerConfig(a_zookeeper,a_groupId));  
  8.         this.topic = a_topic;  
  9.     }  
  10.    
  11.     public void shutdown() {  
  12.         if (consumer != null)  
  13.             consumer.shutdown();  
  14.         if (executor != null)  
  15.             executor.shutdown();  
  16.     }  
  17.    
  18.     public void run(int numThreads) {  
  19.         Map<String, Integer> topicCountMap = new HashMap<String, Integer>();  
  20.         topicCountMap.put(topic, new Integer(numThreads));  
  21.         Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer  
  22.                 .createMessageStreams(topicCountMap);  
  23.         List<KafkaStream<byte[], byte[]>> streams = consumerMap.get(topic);  
  24.    
  25.         // now launch all the threads  
  26.         executor = Executors.newFixedThreadPool(numThreads);  
  27.    
  28.         // now create an object to consume the messages  
  29.         //  
  30.         int threadNumber = 0;  
  31.         for (final KafkaStream stream : streams) {  
  32.             executor.submit(new ConsumerMsgTask(stream, threadNumber));  
  33.             threadNumber++;  
  34.         }  
  35.     }  
  36.    
  37.     private static ConsumerConfig createConsumerConfig(String a_zookeeper,  
  38.             String a_groupId) {  
  39.         Properties props = new Properties();  
  40.         props.put("zookeeper.connect", a_zookeeper);  
  41.         props.put("group.id", a_groupId);  
  42.         props.put("zookeeper.session.timeout.ms""400");  
  43.         props.put("zookeeper.sync.time.ms""200");  
  44.         props.put("auto.commit.interval.ms""1000");  
  45.    
  46.         return new ConsumerConfig(props);  
  47.     }  
  48.    
  49.     public static void main(String[] arg) {  
  50.         String[] args = { "172.168.63.221:2188""group-1""page_visits""12" };  
  51.         String zooKeeper = args[0];  
  52.         String groupId = args[1];  
  53.         String topic = args[2];  
  54.         int threads = Integer.parseInt(args[3]);  
  55.    
  56.         ConsumerDemo demo = new ConsumerDemo(zooKeeper, groupId, topic);  
  57.         demo.run(threads);  
  58.    
  59.         try {  
  60.             Thread.sleep(10000);  
  61.         } catch (InterruptedException ie) {  
  62.    
  63.         }  
  64.         demo.shutdown();  
  65.     }  
  66. }  

 

   上面的例子是用java编写的消费者的例子,也是官网提供的例子,那么我们的源码分析就从下面这一行开始:

 

 

Java代码   收藏代码
  1. Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer  
  2.                 .createMessageStreams(topicCountMap);  
 

 

 

 从createMessagesStreams方法进入后直接到kafka.javaapi.consumer.ZookeeperConsumerConnector类。

 

Java代码   收藏代码
  1. private[kafka] class ZookeeperConsumerConnector(val config: ConsumerConfig,  
  2.                                                 val enableFetcher: Boolean) // for testing only  
  3.     extends ConsumerConnector {  
  4.   //初始化伴生对象  
  5.   private val underlying = new kafka.consumer.ZookeeperConsumerConnector(config, enableFetcher)  
  6.   private val messageStreamCreated = new AtomicBoolean(false)  
  7.   
  8.   def this(config: ConsumerConfig) = this(config, true)  
  9.   
  10.  // for java client  
  11.   def createMessageStreams[K,V](  
  12.         topicCountMap: java.util.Map[String,java.lang.Integer],  
  13.         keyDecoder: Decoder[K],  
  14.         valueDecoder: Decoder[V])  
  15.       : java.util.Map[String,java.util.List[KafkaStream[K,V]]] = {  
  16.   
  17.     if (messageStreamCreated.getAndSet(true))  
  18.       throw new MessageStreamsExistException(this.getClass.getSimpleName +  
  19.                                    " can create message streams at most once",null)  
  20.     val scalaTopicCountMap: Map[String, Int] = {  
  21.       import JavaConversions._  
  22.       Map.empty[String, Int] ++ (topicCountMap.asInstanceOf[java.util.Map[String, Int]]: mutable.Map[String, Int])  
  23.     }  
  24.     val scalaReturn = underlying.consume(scalaTopicCountMap, keyDecoder, valueDecoder)  
  25.     val ret = new java.util.HashMap[String,java.util.List[KafkaStream[K,V]]]  
  26.     for ((topic, streams) <- scalaReturn) {  
  27.       var javaStreamList = new java.util.ArrayList[KafkaStream[K,V]]  
  28.       for (stream <- streams)  
  29.         javaStreamList.add(stream)  
  30.       ret.put(topic, javaStreamList)  
  31.     }  
  32.     ret  
  33.   }  
 

 

这个类是整体Consumer的核心类,首先要初始化 ZookeeperConsumerConnector的伴生对象(关于伴生对象请大家查看scala语法,实际就是一个静态对象,每一个class都要 有一个伴生对象,像我们的静态方法都要定义在这里面),在createMessageStreams中,topicCountMap主要是消费线程数,这 个参数和partition的数量有直接有关系。

 

  通过val scalaReturn = underlying.consume(scalaTopicCountMap, keyDecoder, valueDecoder)这行代码,将进入到伴生对象中,直接可以跟踪消费的内部逻辑。

 

 

Java代码   收藏代码
  1. def consume[K, V](topicCountMap: scala.collection.Map[String,Int], keyDecoder: Decoder[K], valueDecoder: Decoder[V])  
  2.       : Map[String,List[KafkaStream[K,V]]] = {  
  3.     debug("entering consume ")  
  4.     if (topicCountMap == null)  
  5.       throw new RuntimeException("topicCountMap is null")  
  6.      //封装成一个TopicCount对象,参数分别是消费者的ids字符串和线程数map  
  7.     val topicCount = TopicCount.constructTopicCount(consumerIdString, topicCountMap)  
  8.     //解析出每个topic对应多少个消费者线程,topicThreadsIds是一个map结构  
  9.     val topicThreadIds = topicCount.getConsumerThreadIdsPerTopic  
  10.   
  11.     //针对每一个消费者线程创建一个BlockingQueue队列,队列中存储的是FetchedDataChunk数据块,每一个数据块中包括多条记录。  
  12.     val queuesAndStreams = topicThreadIds.values.map(threadIdSet =>  
  13.       threadIdSet.map(_ => {  
  14.         val queue =  new LinkedBlockingQueue[FetchedDataChunk](config.queuedMaxMessages)  
  15.         val stream = new KafkaStream[K,V](  
  16.           queue, config.consumerTimeoutMs, keyDecoder, valueDecoder, config.clientId)  
  17.         (queue, stream)  
  18.       })  
  19.     ).flatten.toList  
  20.   
  21.     val dirs = new ZKGroupDirs(config.groupId)  
  22.     //将consumer的topic信息注册到zookeeper中,格式如下:  
  23.     //Consumer id registry:/consumers/[group_id]/ids[consumer_id] -> topic1,...topicN  
  24.     registerConsumerInZK(dirs, consumerIdString, topicCount)  
  25.     reinitializeConsumer(topicCount, queuesAndStreams)  
  26.   
  27.     loadBalancerListener.kafkaMessageAndMetadataStreams.asInstanceOf[Map[String, List[KafkaStream[K,V]]]]  
  28.   }  
 

 

 结合代码中的注释请看下面的图:

   

说明: 

创建consumer thread

consumer thread数量与BlockingQueue一一对应。

a.当consumer thread count=1时

此 时有一个blockingQueue1,三个fetch thread线程,该topic分布在几个node上就有几个fetch thread,每个fetch thread会于kafka broker建立一个连接。3个fetch thread线程去拉取消息数据,最终放到blockingQueue1中,等待consumer thread来消费。

 

接着看上面代码中的这个方法:

 

Java代码   收藏代码
  1. registerConsumerInZK(dirs, consumerIdString, topicCount)  

 

这个方法是将consumer的topic信息注册到zookeeper中,格式如下:

 

    Consumer id registry:

    /consumers/[group_id]/ids[consumer_id] -> topic1,...topicN

  

进入重新初始化Consumer方法:

 

Java代码   收藏代码
  1. registerConsumerInZK(dirs, consumerIdString, topicCount)  

 

 这个方法会建立一系列的侦听器:

1、负载平衡器侦听器:ZKRebalancerListener。

2、会话超时侦听器:ZKSessionExpireListener。

3、监控topic和partition变化侦听器:ZKTopicPartitionChangeListener。

客 户端启动后会在消费者注册目录上添加子节点变化的监听ZKRebalancerListener,ZKRebalancerListener实例会在内部 创建一个线程,这个线程定时检查监听的事件有没有执行(消费者发生变化),如果没有变化则wait1秒钟,当发生了变化就调用 syncedRebalance 方法,去rebalance消费者,代码如下:

 

Java代码   收藏代码
  1. private val watcherExecutorThread = new Thread(consumerIdString + "_watcher_executor") {  
  2.      override def run() {  
  3.        info("starting watcher executor thread for consumer " + consumerIdString)  
  4.        var doRebalance = false  
  5.        while (!isShuttingDown.get) {  
  6.          try {  
  7.            lock.lock()  
  8.            try {  
  9.              if (!isWatcherTriggered)  
  10.                cond.await(1000, TimeUnit.MILLISECONDS) // wake up periodically so that it can check the shutdown flag  
  11.            } finally {  
  12.              doRebalance = isWatcherTriggered  
  13.              isWatcherTriggered = false  
  14.              lock.unlock()  
  15.            }  
  16.            if (doRebalance)  
  17.              syncedRebalance  
  18.          } catch {  
  19.            case t: Throwable => error("error during syncedRebalance", t)  
  20.          }  
  21.        }  
  22.        info("stopping watcher executor thread for consumer " + consumerIdString)  
  23.      }  
  24.    }  
  25.    watcherExecutorThread.start()  
  26.   
  27.    @throws(classOf[Exception])  
  28.    def handleChildChange(parentPath : String, curChilds : java.util.List[String]) {  
  29.      rebalanceEventTriggered()  
  30.    }  
  31.   
  32.    def rebalanceEventTriggered() {  
  33.      inLock(lock) {  
  34.        isWatcherTriggered = true  
  35.        cond.signalAll()  
  36.      }  
  37.    }  

 syncedRebalance方法在内部会调用def rebalance(cluster: Cluster): Boolean方法,去真正执行操作。

 在这个方法中,获取者必须停止,避免重复的数据,重新平衡尝试失败,被释放的分区被另一个consumers拥有。如果我们不首先停止获取数据,消费者将继续并发的返回数据,所以要先停止之前的获取者,再更新当前的消费者信息,重新更新启动获取者。代码如下:

Java代码   收藏代码
  1. private def rebalance(cluster: Cluster): Boolean = {  
  2.       val myTopicThreadIdsMap = TopicCount.constructTopicCount(  
  3.         group, consumerIdString, zkClient, config.excludeInternalTopics).getConsumerThreadIdsPerTopic  
  4.       val brokers = getAllBrokersInCluster(zkClient)  
  5.       if (brokers.size == 0) {  
  6.         // This can happen in a rare case when there are no brokers available in the cluster when the consumer is started.  
  7.         // We log an warning and register for child changes on brokers/id so that rebalance can be triggered when the brokers  
  8.         // are up.  
  9.         warn("no brokers found when trying to rebalance.")  
  10.         zkClient.subscribeChildChanges(ZkUtils.BrokerIdsPath, loadBalancerListener)  
  11.         true  
  12.       }  
  13.       else {  
  14.         /** 
  15.          * fetchers must be stopped to avoid data duplication, since if the current 
  16.          * rebalancing attempt fails, the partitions that are released could be owned by another consumer. 
  17.          * But if we don't stop the fetchers first, this consumer would continue returning data for released 
  18.          * partitions in parallel. So, not stopping the fetchers leads to duplicate data. 
  19.          */  
  20.          //在这行要先停止之前的获取者线程,再更新启动当前最新消费者的。  
  21.         closeFetchers(cluster, kafkaMessageAndMetadataStreams, myTopicThreadIdsMap)  
  22.   
  23.         releasePartitionOwnership(topicRegistry)  
  24.   
  25.         val assignmentContext = new AssignmentContext(group, consumerIdString, config.excludeInternalTopics, zkClient)  
  26.         val partitionOwnershipDecision = partitionAssignor.assign(assignmentContext)  
  27.         val currentTopicRegistry = new Pool[String, Pool[Int, PartitionTopicInfo]](  
  28.           valueFactory = Some((topic: String) => new Pool[Int, PartitionTopicInfo]))  
  29.   
  30.         // fetch current offsets for all topic-partitions  
  31.         val topicPartitions = partitionOwnershipDecision.keySet.toSeq  
  32.   
  33.         val offsetFetchResponseOpt = fetchOffsets(topicPartitions)  
  34.   
  35.         if (isShuttingDown.get || !offsetFetchResponseOpt.isDefined)  
  36.           false  
  37.         else {  
  38.           val offsetFetchResponse = offsetFetchResponseOpt.get  
  39.           topicPartitions.foreach(topicAndPartition => {  
  40.             val (topic, partition) = topicAndPartition.asTuple  
  41.             val offset = offsetFetchResponse.requestInfo(topicAndPartition).offset  
  42.             val threadId = partitionOwnershipDecision(topicAndPartition)  
  43.             addPartitionTopicInfo(currentTopicRegistry, partition, topic, offset, threadId)  
  44.           })  
  45.   
  46.           /** 
  47.            * move the partition ownership here, since that can be used to indicate a truly successful rebalancing attempt 
  48.            * A rebalancing attempt is completed successfully only after the fetchers have been started correctly 
  49.            */  
  50.           if(reflectPartitionOwnershipDecision(partitionOwnershipDecision)) {  
  51.             allTopicsOwnedPartitionsCount = partitionOwnershipDecision.size  
  52.   
  53.             partitionOwnershipDecision.view.groupBy { case(topicPartition, consumerThreadId) => topicPartition.topic }  
  54.                                       .foreach { case (topic, partitionThreadPairs) =>  
  55.               newGauge("OwnedPartitionsCount",  
  56.                 new Gauge[Int] {  
  57.                   def value() = partitionThreadPairs.size  
  58.                 },  
  59.                 ownedPartitionsCountMetricTags(topic))  
  60.             }  
  61.   
  62.             topicRegistry = currentTopicRegistry  
  63.             updateFetcher(cluster)  
  64.             true  
  65.           } else {  
  66.             false  
  67.           }  
  68.         }  
  69.       }  
  70.     }  

   上面代码的流程图如下:

   

 我们要了解Rebalance如何动作,看下updateFetcher怎么实现的。

Java代码   收藏代码
  1. private def updateFetcher(cluster: Cluster) {  
  2.       // 遍历topicRegistry中保存的当前消费者的分区信息,修改Fetcher的partitions信息   
  3.       var allPartitionInfos : List[PartitionTopicInfo] = Nil  
  4.       for (partitionInfos <- topicRegistry.values)  
  5.         for (partition <- partitionInfos.values)  
  6.           allPartitionInfos ::= partition  
  7.       info("Consumer " + consumerIdString + " selected partitions : " +  
  8.         allPartitionInfos.sortWith((s,t) => s.partition < t.partition).map(_.toString).mkString(","))  
  9.   
  10.       fetcher match {  
  11.         case Some(f) =>  
  12.           // 调用fetcher的startConnections方法,初始化Fetcher并启动它  
  13.           f.startConnections(allPartitionInfos, cluster)  
  14.         case None =>  
  15.       }  
  16.     }  

 注意下面这行代码:

 

Java代码   收藏代码
  1. f.startConnections(allPartitionInfos, cluster)  
 在这个方法里面其实是启动了一个LeaderFinderThread线程的,这个线程主要是通过ClientUtils的io,获取最新的topic元数据,将topic:partitionLeaderId和brokerId对应起来,封装成Map结构。

 

 

 

Java代码   收藏代码
  1. for ((brokerAndFetcherId, partitionAndOffsets) <- partitionsPerFetcher) {  
  2.        var fetcherThread: AbstractFetcherThread = null  
  3.        fetcherThreadMap.get(brokerAndFetcherId) match {  
  4.          case Some(f) => fetcherThread = f  
  5.          case None =>  
  6.            fetcherThread = createFetcherThread(brokerAndFetcherId.fetcherId, brokerAndFetcherId.broker)  
  7.            fetcherThreadMap.put(brokerAndFetcherId, fetcherThread)  
  8.            fetcherThread.start  
  9.        }  
  10.   
  11.        fetcherThreadMap(brokerAndFetcherId).addPartitions(partitionAndOffsets.map { case (topicAndPartition, brokerAndInitOffset) =>  
  12.          topicAndPartition -> brokerAndInitOffset.initOffset  
  13.        })  
  14.      }  
对每个broker创建一个FetcherRunnable线程,插入到fetcherThreadMap中并启动它。这个线程负责从服务器上不断获取数据,把数据插入内部阻塞队列的操作 。

 

下面看一下ConsumerIterator的实现,客户端用它不断的从分区信息 的内部队列中取数据。它实现了IteratorTemplate的接口,它的内部保存一个Iterator的属性current,每次调用 makeNext时会检查它,如果有则从中取否则从队列中取。下面给出代码

 

Java代码   收藏代码
  1. protected def makeNext(): MessageAndMetadata[T] = {  
  2.     var currentDataChunk: FetchedDataChunk = null  
  3.     // if we don't have an iterator, get one,从内部变量中取数据  
  4.     var localCurrent = current.get()  
  5.     if(localCurrent == null || !localCurrent.hasNext) {  
  6. // 内部变量中取不到值,检查timeout的值  
  7.       if (consumerTimeoutMs < 0)  
  8.         currentDataChunk = channel.take // 是负数(-1),则表示永不过期,如果接下来无新数据可取,客户端线程会在channel.take阻塞住  
  9.       else {  
  10. // 设置了过期时间,在没有新数据可用时,pool会在相应的时间返回,返回值为空,则说明没有取到新数据,抛出timeout的异常  
  11.         currentDataChunk = channel.poll(consumerTimeoutMs, TimeUnit.MILLISECONDS)  
  12.         if (currentDataChunk == null) {  
  13.           // reset state to make the iterator re-iterable  
  14.           resetState()  
  15.           throw new ConsumerTimeoutException  
  16.         }  
  17.       }  
  18. // kafka把shutdown的命令也做为一个datachunk放到队列中,用这种方法来保证消息的顺序性  
  19.       if(currentDataChunk eq ZookeeperConsumerConnector.shutdownCommand) {  
  20.         debug("Received the shutdown command")  
  21.         channel.offer(currentDataChunk)  
  22.         return allDone  
  23.       } else {  
  24.         currentTopicInfo = currentDataChunk.topicInfo  
  25.         if (currentTopicInfo.getConsumeOffset != currentDataChunk.fetchOffset) {  
  26.           error("consumed offset: %d doesn't match fetch offset: %d for %s;\n Consumer may lose data"  
  27.                         .format(currentTopicInfo.getConsumeOffset, currentDataChunk.fetchOffset, currentTopicInfo))  
  28.           currentTopicInfo.resetConsumeOffset(currentDataChunk.fetchOffset)  
  29.         }  
  30. // 把取出chunk中的消息转化为iterator  
  31.         localCurrent = if (enableShallowIterator) currentDataChunk.messages.shallowIterator  
  32.                        else currentDataChunk.messages.iterator  
  33. // 使用这个新的iterator初始化current,下次可直接从current中取数据  
  34.         current.set(localCurrent)  
  35.       }  
  36.     }  
  37. // 取出下一条数据,并用下一条数据的offset值设置consumedOffset  
  38.     val item = localCurrent.next()  
  39.     consumedOffset = item.offset  
  40. // 解码消息,封装消息和它的topic信息到MessageAndMetadata对象,返回  
  41.     new MessageAndMetadata(decoder.toEvent(item.message), currentTopicInfo.topic)  
  42.   }  
 下面看一下它的next方法的逻辑:

 

Java代码   收藏代码
  1. override def next(): MessageAndMetadata[T] = {  
  2.     val item = super.next()  
  3.     if(consumedOffset < 0)  
  4.       throw new IllegalStateException("Offset returned by the message set is invalid %d".format(consumedOffset))  
  5. // 使用makeNext方法设置的consumedOffset,去修改topicInfo的消费offset  
  6.     currentTopicInfo.resetConsumeOffset(consumedOffset)  
  7.     val topic = currentTopicInfo.topic  
  8.     trace("Setting %s consumed offset to %d".format(topic, consumedOffset))  
  9.     ConsumerTopicStat.getConsumerTopicStat(topic).recordMessagesPerTopic(1)  
  10.     ConsumerTopicStat.getConsumerAllTopicStat().recordMessagesPerTopic(1)  
  11. // 返回makeNext得到的item  
  12.     item  
  13.   }  

 

 http://flychao88.iteye.com/blog/2268481

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值