上一篇中分析了Scala版的console producer代码,这篇文章为读者带来一篇console consumer工作原理分析的随笔。其实不论是哪个consumer,大部分的工作原理都是类似的。本文利用console consumer作为切入点,既容易理解又不失一般性。
 
本文使用的Kafka环境是0.8.2.1版本,这也是当前最新的版本。(注:Kafka 0.9版本据说会用Java重新设计并编写consumer代码,对此我们拭目以待) 由于主要目的是分析consumer原理,因此本文并不过多纠结于console consumer特定的使用方法。一条最简单的命令足以作为我们的开始:
bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic test-topic
 
kafka-console-consumer.sh脚本内容简洁明了: exec $(dirname $0)/kafka-run-class.sh kafka.tools.ConsoleConsumer $@
 
很显然,该shell脚本调用了kafka.tools包下的ConsoleConsumer类,并将提供的命令行参数全部传给该类。由此可知,我们需要从这个类开始分析。不过在此之前,简单说一下console consumer整体的启动流程,如下图所示:

上图流程具体展开如下:
1. 加载并解析命令行参数,唯一的必要参数(Required)是zookeeper
2. 如果没有传入group.id,ConsoleConsumer将生成自己的group.id,即console-consumer-[10万以内的一个随机数]
3. 创建ConsumerConfig用于封装consumer的各种配置
4. 创建默认的消息格式化类,其定义的writeTo方法会默认将消息输出到控制台
5. 创建ZookeeperConsumerConnector。Kafka使用它来创建KafkaStream消费流
5.1 创建本地缓存, 保存topic下每个分区的信息,包括该分区底层的阻塞队列,已消费的位移、已获取到的最新位移以及获取大小等
5.2 创建本地缓存,保存每个topic分区当前在zookeeper中保存的位移值
5.3 创建本地缓存,保存topic的每个读取线程底层对应的阻塞队列,主要用于关闭Connector时可以批量关闭底层的阻塞队列
5.4 生成consumer id,规则为[group.id]_[主机名]_[时间戳]_[随机产生的一个UUID的前8位]。其中主机名就是运行ConsoleConsumer所在broker节点的主机名
5.5 创建获取线程管理器(ConsumerFetcherManager)
5.6 启动一个特定线程,用于定时地(默认是1分钟)向Zookeeper提交更改过的位移 
6. 增加JVM关闭钩子,确保JVM关闭后资源也能够被释放
7. 创建KafkaStream并通过迭代器不断遍历该stream, KafkaStream的迭代器的底层实现包含一个阻塞队列,如果没有新的消息到来,该迭代器会一直阻塞,除非你显式设置了consumer.timeout.ms参数(默认是-1表示consumer会一直等待新消息的带来)
8. 每接收到一条新的消息,默认的消息格式化类会将其输出到控制台上。然后再次等待迭代器传过来的下一条消息

本质上来说,console consumer启动时会创建一个KafkaStream(可以简单翻译成Kafak流),该stream会不停地等待可消费的新消息——具体做法就是通过LinkedBlockingQueue阻塞队列来实现,后续会有详细描述。针对上面启动的顺序列表,我们在ConsoleConsumer.scala中逐一进行代码走读:

1. 加载必要参数 zookeeper
ConsoleConsumer.scala类定义了main方法,说明这是个可执行的类。类的前100多行几乎都在处理命令行参数的解析。其中真正必要的参数只有zookeeper.connect一个,如下面代码所示:
 // REQUIRED表示这是一个必须要指定的参数
val zkConnectOpt = parser.accepts("zookeeper", "REQUIRED: The connection string for the zookeeper connection in the form host:port. " +
"Multiple URLS can be given to allow fail-over.").withRequiredArg.describedAs("urls").ofType(classOf[String])
2. 生成group.id
乍一看和官网上要求的配置不匹配,因为官网中说过consumer真正必要的参数实际上有两个:zookeeper.connect和group.id。由此可以推断console consumer应该会生成group.id的值,且它本质上也是一个consumer,必然属于一个消费组,因此也必然定义了consumer id。下面的代码中即展示了console consumer如何生成自己的group id: (consumer id是如何生成的后面再说)
 // 如果没有显式指定group.id,那么代码就自己合成一个
// 具体格式: console-consumer-[10万以内的一个随机数]
// 10万是一个很大的数,因此只有非常低的几率会碰到多个console consumer的group id相同的情况
if(!consumerProps.containsKey("group.id")) {
consumerProps.put("group.id","console-consumer-" + new Random().nextInt(100000))
groupIdPassed=false
}
3. 创建ConsumerConfig对象封装配置

确定了consumer的group.id之后console consumer需要把传入参数封装进ConsumerConfig类中并把后者传给Consumer的create方法以构造一个ConsumerConnector——即初始化consumer了,具体逻辑见下面的代码:

 val config = new ConsumerConfig(consumerProps) // 封装ConsumerConfig配置类
val skipMessageOnError = if (options.has(skipMessageOnErrorOpt)) true else false
4. 创建默认的消息格式化类,其定义的writeTo方法会默认将消息输出到控制台
 val messageFormatterClass = Class.forName(options.valueOf(messageFormatterOpt))  // 创建消息格式类,用于最后的输出显示
val formatterArgs = CommandLineUtils.parseKeyValueArgs(options.valuesOf(messageFormatterArgOpt))
val maxMessages = if(options.has(maxMessagesOpt)) options.valueOf(maxMessagesOpt).intValue else -1
5. 创建ZookeeperConsumerConnector
ZookeeperConsumerConnector非常重要,它实现了ConsumerConnector接口(该接口定义了创建KafkaStream和提交位移的操作,如createMessageStreams、commitOffsets等)。Kakfa官网把这个接口称为high level的consumer API。对于大多数consumer来说,这个high level的consumer API提供的功能已经足够了。不过很多用户可能需要对位移有更大的控制,这个时候Kafka推荐用户使用被称为low level的consumer API—— SimpleConsumer。大家参考这篇文章来深入学习high level API的用法。目前为止,我们只需要知道Kafka通过下面的语句构建了ConsumerConnector这个consumer的核心接口:
 val connector = Consumer.create(config) // 创建ConsumerConnector,Consumer核心接口

6. 构建JVM关闭钩子线程 
这部分非常简单,就是在线程中关闭上一步创建的connector,并根据传入的参数决定是否删除zookeeper下/consumers/[group.id]节点
7. 创建KafkaStream,通过迭代器等待消息到来
由于console consumer支持同时消费多个topic的消息,因此它提供了类似于过滤器这样的实现,这也是为什么connector调用createMessageStreamsByFilter来创建KafkaStream的原因,如下面的代码所示。
 val stream = connector.createMessageStreamsByFilter(filterSpec, 1, new DefaultDecoder(), new DefaultDecoder()).get(0)
val iter = if(maxMessages >= 0)
stream.slice(0, maxMessages)
else
stream
createMessageStreamsByFilter方法返回的是一组KafkaStream,但console consumer默认只是创建了1个stream,所以这里直接调用get(0)取到这个stream就可以了。
8. 通过迭代器以阻塞等待的方式消费消息
创建好KafkaStream之后,console consumer通过迭代器遍历KafkaStream。这里值得注意的是,该迭代器底层实现依赖一个阻塞队列。如果没有显式配置过consumer.timeout.ms参数(默认是-1表示consumer会一直等待新消息),那么迭代器会一直处于阻塞状态等待可供消费的消息——具体的实现细节参见下一篇。迭代器每收到一条消息后,它就会使用默认的消息格式化类DefaultMessageFormatter将消息输出到控制台,这也是console consumer名字的由来,如下面的代码所示:
 for(messageAndTopic <- iter) {
try {
formatter.writeTo(messageAndTopic.key, messageAndTopic.message, System.out) // 输出到控制台
numMessages += 1
} catch { ... }
...
}

好了,至此我们按照启动顺序概述了console consumer启动时的各个阶段。不过,ZookeeperConsumerConnector和创建和迭代器的实现我们并未详细展开,这部分内容将作为后面续篇的内容呈现给大家。敬请期待!

【原创】Kafka console consumer源代码分析(一)的更多相关文章

  1. 【原创】Kafka console consumer源代码分析(二)

    我们继续讨论console consumer的实现原理,本篇着重探讨ZookeeperConsumerConnector的使用,即后续所有的内容都由下面这条语句而起: val connector = ...

  2. 【原创】kafka consumer源代码分析

    顾名思义,就是kafka的consumer api包. 一.ConsumerConfig.scala Kafka consumer的配置类,除了一些默认值常量及验证参数的方法之外,就是consumer ...

  3. 【原创】Kakfa utils源代码分析(三)

    Kafka utils包最后一篇~~~ 十五.ShutdownableThread.scala 可关闭的线程抽象类! 继承自Thread同时还接收一个boolean变量isInterruptible表 ...

  4. 【原创】Kakfa utils源代码分析(二)

    我们继续研究kafka.utils包 八.KafkaScheduler.scala 首先该文件定义了一个trait:Scheduler——它就是运行任务的一个调度器.任务调度的方式支持重复执行的后台任 ...

  5. 【原创】Kakfa utils源代码分析(一)

    Kafka.utils,顾名思义,就是一个工具套件包,里面的类封装了很多常见的功能实现——说到这里,笔者有一个感触:当初为了阅读Kafka源代码而学习了Scala语言,本以为Kafka的实现会用到很多 ...

  6. Kafka 源代码分析之LogManager

    这里分析kafka 0.8.2的LogManager logmanager是kafka用来管理log文件的子系统.源代码文件在log目录下. 这里会逐步分析logmanager的源代码.首先看clas ...

  7. Kafka 0.10 SocketServer源代码分析

    1概要设计 Kafka SocketServer是基于Java NIO来开发的,采用了Reactor的模式,其中包含了1个Acceptor负责接受客户端请求,N个Processor负责读写数据,M个H ...

  8. Spark SQL 源代码分析之 In-Memory Columnar Storage 之 in-memory query

    /** Spark SQL源代码分析系列文章*/ 前面讲到了Spark SQL In-Memory Columnar Storage的存储结构是基于列存储的. 那么基于以上存储结构,我们查询cache ...

  9. Spark SQL Catalyst源代码分析之TreeNode Library

    /** Spark SQL源代码分析系列文章*/ 前几篇文章介绍了Spark SQL的Catalyst的核心执行流程.SqlParser,和Analyzer,本来打算直接写Optimizer的,可是发 ...

随机推荐

  1. shell简单用法笔记(shell中数值运算)二

    shell中变量值,如果不手动指定类型,默认都是字符串类型: 例如: a= b= c=$a+#b echo $c 结果会输出:123+456 shell中,如果要进行数值运算,可以通过一下方法: 方法 ...

  2. Lesson 3 Please send me a card

    Text Postcards always spoil my holidays. Last summer, I went to Italy. I visited museums and sat in ...

  3. 实战JS正则表达式

    -正则表达式是一种文本模式的匹配工具. -文章导读: --1.正则对象的属性和方法 --2.字符串对象的方法 --3.使用正则表达式: ---3.1 给字符串加上千分符 ---3.2 字符串中出现次数 ...

  4. 从阿里巴巴IPO联想到创始人和资方关系

    [小九的学堂,致力于以平凡的语言描述不平凡的技术.如要转载,请注明来源:小九的学堂.cnblogs.com/xfuture] 5月7日,阿里巴巴于今日向美国证券交易委员会(SEC)提交了IPO(首次公 ...

  5. MySQL 远程连接(federated存储引擎)

    标签:federated存储引擎 概述 本文主要介绍通过federated存储引擎建立远程连接表 测试环境:mysql 5.6.21 步骤 开启federated存储引擎 先查看federated存储 ...

  6. [ASP.NET MVC 小牛之路]03 - Razor语法

    本人博客已转移至:http://www.exblr.com/liam  Razor是MVC3中才有的新的视图引擎.我们知道,在ASP.NET中,ASPX的视图引擎依靠<%和%>来调用C#指 ...

  7. spring快速入门(四)

    一.在spring快速入门(三)的基础上,我们来了解BeanFactory及配置. Client package com.murong.client; import org.springframewo ...

  8. HTML5 音频播放器-Javascript代码(短小精悍)

    直接上干货咯! //HTML5 音频播放器 lzpong 2015/01/19 var wavPlayer = function () { if(window.parent.wavPlayer) re ...

  9. KnockoutJS 3.X API 第七章 其他技术(1) 加载和保存JSON数据

    Knockout允许您实现复杂的客户端交互性,但几乎所有Web应用程序还需要与服务器交换数据,或至少将本地存储的数据序列化. 最方便的交换或存储数据的方式是JSON格式 - 大多数Ajax应用程序今天 ...

  10. python学习之day1-基础知识

    1.变量 变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念.变量可以通过变量名访问.在python中,如果要使用一个变量,不需要提前声明,只需要在用的时候,给这个变量赋值即可.pytho ...