【原创】kafka producer源代码分析

    Kafka 0.8.2引入了一个用Java写的producer。下一个版本还会引入一个对等的Java版本的consumer。新的API旨在取代老的使用Scala编写的客户端API,但为了兼容性的考虑两版API还要同时存在一段时间。另外,新版的API可以打成独立的jar包,而Scala版本的不行。
    Kafka官网是鼓励用户使用java版的producer的,而kafka.core.producer包实际上是老版的实现。这个包中还包括一个async包,应该是实现异步发送的,与之平行的代码应该都是供同步发送使用。
一、BaseProducer.scala
这个scala代码结构很清晰,定义了一个trait和两个class:
BaseProducer trait —— 基本的producer trait,新旧两版producer都实现了这个trait,据说在Kafka 0.9版本会被移除掉。这个trait定义了一个producer应该有的两个方法: send 和 close。send方法接收一个topic,一个字节数组表示消息的key以及一个字节数组表示消息;而close方法就是关闭producer,其实主要工作就是关闭对应线程
NewShinyProducer —— 这个应该就是java版的新版producer(具体实现代码在kafka.clients工程中)。每个producer都要根据producer.type属性的值来确定它是同步发送还是异步发送。这个类定义了一个producer变量将其赋予一个KafkaProducer类的实例——后者类就是新版producer的实现类——接收一个Properties实例的[K,V]泛型类。K和V分别是消息的键和消息体,类型都是字节数组。有了这个变量,实现send和close方法也就很容易了。在close方法中直接调用KafkaProducer的close方法;而send方法中先构建一个ProducerRecord类表示消息记录,然后调用KafkaProducer的send方法将记录发送出去。因为KafkaProducer使用了Feture类来实现异步发送,因此如果是同步发送的话,代码中使用了Future.get方法来模拟同步阻塞。如果是异步发送的话程序中还会注册一个ErrorLoggingCallback的回调函数,一旦发送完成调用其onCompletion方法进行错误判断。
OldProducer —— 老版使用Scala实现的producer。程序一开始就要设定partitioner.class属性,如果没有设定使用ByteArrayPartitioner作为默认的分区策略——该策略就是将计算key的hashCode然后与分区数求模,即 Utils.abs(hashCode(key)) % numPartitions。既然是发送者,它也维护了一个叫producer的变量,只不过这次被赋值为老版的producer实例(kakfa.producer.Producer,后面会说到),然后方法send中调用该类的send方法,close方法也是调用该类的close方法实现。
二、ProducerClosedException.scala
没啥好说的,就是标识producer已经关闭的异常
三、SyncProducerConfig.scala
这个scala定义了同步producer所需的各种配置,与之对应的在async包中还有一个AysncProducerConfig.scala用于定义异步producer的配置。在SyncProducerConfig.scala文件中定义了一个trait: SyncProducerConfigShared,里面定义了很多公共的属性,比如:
send.buffer.bytes —— SOCKET写缓冲区大小
client.id —— 用户指定的标识符帮助追踪调用,日志监控等
request.required.acks —— producer请求完成前有多少个非leader broker提交数据并响应leader broker,这是实现持久性的关键,可能的取值有-1, 0, 1。具体含义前面api包的时候已经说过了,或者查阅官网含义。
request.timeout.ms —— producer请求的默认应答超时时间,默认是10秒
SyncProducerConfig类实现了SyncProducerConfigShared trait,因此除了继承了公共的属性外,还定义了host和port分别表示producer要发送的broker的主机名和端口信息
四、ByteArrayPartitioner.scala
Kafka基于字节数组的分区策略——计算key的哈希值绝对值,然后对分区数求模
五、DefaultPartitioner.scala
策略和ByteArrayPartitioner一样,也是计算key的哈希值绝对值,然后对分区数求模
六、KafkaLog4jAppender.scala
看名字与Log4j有关,不过很奇怪的是在整个工程中貌似没有用到,所以也就不详细说了。对了,我的kafka版本是0.8.2.1
七、Partitioner.scala
除了求模的分区策略之外,kafka运行用户定义自己的分区策略,具体方法就是要实现这个trait,然后kafka通过反射机制构建用户自定义了类实例。值得注意的是,每个自定义分区类必须提供一个只接收VerifiableProperties实例的构造函数。这个trait非常简单,就是定义了一个partition方法,根据给定的key和分区数计算最后的目标分区ID
八、BrokerPartitionInfo.scala
这个scala文件中定义了两个类:BrokerPartitionInfo类和PartitionAndLeader类。后者比较简单,就是一个case类,保存一个topic分区对应的leader broker id。下面重点说说BrokerPartitionInfo类。
构造函数
1. producerConfig —— producer的配置,用于获取broker列表以及一组topic的元数据信息
2. producerPool —— ProducerPool对象实例,底层保存为Hashmap(k是broker id,v是SyncProducer)的一组producer,主要用于更新producer使用。
3. topicPartitionInfo —— 保存了topic到TopicMetadata元数据信息的映射的HashMap。所谓的元数据,这里再稍微复习一下,分区的元数据包括分区id,leader broker id,AR和ISR,而Topic的元数据自然包括topic名字以及一组分区元数据信息
类变量
1. brokerList —— producer属性metadata.broker.list中配置的broker列表,格式为host1:port1,host2:port2...
2. brokers —— 根据上面brokerList解析成的一组Broker对象实例,封装到一个List中
类方法
1. updateInfo —— 往任何一个broker发送一个metadata请求来更新缓存数据。具体做法就是调用ClientUtils的fetchTopicMetadata方法提交一个metadata request,从得到的response中提取topicMetadata并更新对应的Hashmap
2. getBrokerPartitionInfo —— 获取指定topic的broker分区信息。具体做法是先从缓存中查询指定topic的元数据信息,如果缓存中不存在对应的记录则调用前面的updateInfo方法更新元数据信息到缓存中然后再次获取。一旦获取了Topic的元数据信息,提取出分区的元数据信息部分,遍历所有分区,为每个分区都创建对应的PartitionAndLeader集合,如果某个分区是leader需要在创建PartitionAndLeader实例时将该leader字段赋值,最后返回该集合就是指定topic的分区信息。
九、KeyedMessage.scala
这是很重要的一个类,就是代表了某topic的一条消息。值得一提的是构造函数中有一个分区key专门用于分区使用,如果提供了的话会覆盖掉key参与到分区策略的计算中,但这个键kafka是不保存的,只是用于确定分区使用。
十、ProducerPool.scala
就是一个producer池,底层的数据结构使用了HashMap来保存brokerid到同步producer的映射。ProducerPool类定义的方法如下:
1. updateProducer —— 使用指定的topic元数据信息更新保存在HashMap中的缓存信息。具体做法就是遍历给定的topic元数据集合,将每个topic的leader broker保存在一个名为newBrokers的HashSet中,然后以同步锁的方式遍历该brokers set,如果集合中的某个broker不在我们原先的同步producer map中那创建一个新的syncproducer添加进去否则先断开已有producer的连接,然后再创建个新的加进去
2. getProducer —— 从同步producer map中获取指定broker id对应的producer,如果不存在的话抛出异常。
3. close —— 关闭同步producer map中的所有producer
十一、ProducerConfig.scala
严格来说,其实应该先学习一下async包中的AsyncProducerConfig然后再来看这个类,因为这个类同时实现了SyncProducerConfigShared和AsyncProducerConfig。不过producer的配置类都非常简单明了,我们就直接来看和这个类好了。有一些重要的字段还是需要显著提及一下:
1. brokerList —— broker列表,CSV格式的,每项都是Host:port的格式。主要用于启动producer时创建Socket连接使用
2. partitionClass —— 用于消息分区的类名,默认就是DefaultPartitioner
3. producerType —— 表示producer是同步的还是异步的
4. compressionCodec —— 是否压缩producer产生的数据,默认是不压缩
5. compressedTopics —— 只有启用了压缩这项才有意义,即只压缩指定的topic的消息
6. messageSendMaxRetries —— leader broker可能短暂地不可用从而导致发送消息失败。这个属性就指定失败重试的次数
7. retryBackoffMs —— 每次重试前producer都会刷新相关topic的元数据信息,这个属性值就指定了刷新前producer需要等待的时间,默认是100ms
8. topicMetadataRefreshIntervalMs —— producer在每次broker失败时候都会刷新topic的元数据信息。它也会定期地去更新,时间间隔就是这个属性指定的,默认是10分钟,如果你将这个属性设置为负数则关闭了定期更新,只在broker失败时才会更新。注意: 刷新操作只会在消息发送之后,如果producer不发送消息了也就不能刷新元数据信息。
除了定义的class之外,该scala还定义了一个object对象,定义了一些方法:
validateClientId —— 验证client id的值不能包含非法字符
validateBatchSize —— 验证异步发送时的batch size不能超过缓冲队列的大小
validateProducerType —— 验证producer类型只能是async或sync
validate —— 调用上面三个方法做一次总的验证
十二、ProducerStats.scala
这个scala定义了两个类,一个叫ProducerStast,主要用于统计某clientId的producer状态信息,包括每秒的序列化错误数、每秒重发送数以及每秒发送失败数。第二个类其实是一个object,叫做ProducerStatsRegistry,用于度量每个producer客户端序列化及消息发送,并将结果保存在一个map中,key是client id,value就是ProducerStats实例,提供两个方法getProducerStats和removeProducerStats,分别用户获取指定producer客户端的度量信息,以及移除度量信息。
十三、ProducerTopicStats.scala
这个是关于producer topic的统计信息,因此需要定义一个类来保存topic的度量元统计信息: ProducerTopicMetrics,用于保存client与topic的映射,另外创建了三个度量元: 每秒消息数、每秒字节数和每秒丢弃消息数。定义好度量元之后,这个scala还定义了一个类ProducerTopicStats保存指定producer客户端每个topic的度量元。最后定义了一个object: ProducerTopicStatsRegistry用于注册每个producer客户端的topic统计信息。
十四、Producer.scala
Producer实现类,接收两个构造器参数: 一个ProducerConfig实例和一个EventHandler实例。其中producerConfig就是producer的配置,而eventHandler主要用于异步producer使用。具体定义的类字段和方法如下:
1. hasShutdown —— 表明producer是否处于关闭状态
2. queue —— 异步producer的消息队列,基于LinkedBlockingQueue实现,队列大小由属性queue.buffering.max.messages属性指定,默认是10000条消息。
3. sync —— 表明该producer是同步还是异步的
4. producerSendThread —— 异步producer的发送线程,由async包中的ProducerSendThread类定义。如果是异步的producer的话需要创建一个新的ProducerSendThread实例
5. producerTopicStats —— 创建一个producer的topic统计信息实例用于度量producer的各个统计信息
6. send方法 —— 用于发送KeyedMessage形式的消息,需要判断是同步还是异步,如果是同步的话,调用默认的事件处理器(DefaultEventHandler,后面会说)来发送即可,否则调用asyncSend来异步发送消息
7. recordStats方法 —— 为每条待发送的消息统计对应的度量信息
8. asyncSend方法 —— 异步发送消息。有个属性queue.enqueue.timeout.ms控制消息进出队列的行为。依据不同的值(0, >0,或<0)调用offer或put方法尝试将消息入队列。如果没有添加成功,更新度量统计信息并抛出异常表明队列已满。
9. close方法 —— 关闭producer与broker的连接,也关闭zookeeper客户端连接。使用非阻塞的CAS方式尝试关闭,如果可以关闭的话移除所有度量元统计信息,并关闭对应的线程以及事件处理器。
十五、ProducerRequestStats.scala
这个是关于producer request的度量统计信息,还是两个类一个object的代码结构。其中ProducerRequestMetrics定义了了具体的度量信息,包括producer request速率和时间以及producer request的大小统计。而ProducerRequestStats类主用保存每个broker处理的producer request的统计信息。最后的ProducerRequestStatsRegistry object用于注册某个client id的度量元统计信息收集器。
十六、SyncProducer.scala
使用一组伴生对象定义的同步producer类。它定义了一个volatile var变量标识是否处于关闭状态,默认是未关闭的,同时它还定义了一个BlockingChannel(network包中定义的带超时的阻塞通道)用于发送producer请求。处于统计度量信息的需要,它也定义了一个ProducerRequestStats变量收集producer请求的统计信息。该类定义的方法如下:
1. verifyRequest —— 这个方法只有在日志级别是DEBUG时才会真正有效,就是将请求封装到一个缓冲区中,然后查看一下请求类型ID是否是producer request,并打出整个请求字符串(在TRACE级别)
2. getOrMakeConnection —— 如果底层的BlockingChannle未连接调用connect方法打开socket通道连接
3. connect —— 如果未连接且producer也没有关闭,调用BlockingChannel的connect方法打开socket通道连接,如果出错断开连接抛出异常
4. disconnect —— 调用BlockingChannel的disconnect方法断开socket通道连接。运行结束后,底层的SocketChannel对象会变为null
5. close —— 先断开连接(调用disconnect),然后将shutdown标志位置成true。
6. doSend —— 用于发送请求。第二个参数用于控制是否接收response。首先验证请求合法性(verifyRequest)以及确保对应连接已打开(getOrMakeConnection),然后调用BlockingChannel的send方法发送请求。如果需要接收响应,则调用BlockingChannel的receive方法获得response并返回。如果出现IOException,则断开连接,上层代码处理重试
7. 两个send方法 —— 一个是发送metadata请求,一个是发送producer请求。值得一提的是在发送producer请求时,如果配置required.request.acks是0,那么就传递doSend的第二个参数为false,表示不接收response。
async包
十七、IllegalQueueStateException.scala
异步发送时候在发送完最后一个批次的消息后,队列中不应该再有记录,如果还有那么抛出该异常。
十八、MissingConfigException.scala
有些必须指定的属性缺失时就会抛出该异常,比如没有指定brokersList或topic信息。
十九、EventHandler.scala
以批处理方式异步分发队列中数据的泛型trait。K是消息的键,V是消息体。只定义了2个方法: handle和close。handle用于分发按批次的消息数据并发送至一个Kafka服务器,而close就是关闭这个事件处理器底层维护的ProducerPool,也就是pool中每一个SyncProducer底层维护的SocketChannel
二十、AsyncProducerConfig.scala
与异步发送相关的配置,包括
1. queue.buffering.max.ms:  缓冲队列中数据的最大等待时间,默认是5秒。
2. queue.buffering.max.messages: 缓冲队列的最大长度,默认是10000,即一次最多能缓存10000条消息
3. queue.enqueue.timeout.ms: 如果设置为0且队列已满,立即丢弃该消息;如果是-1且队列已满producer进程会无限期阻塞,不会丢弃该消息。
4. batch.num.messages: 一个批次中的消息数。这个属性和queue.buffering.max.ms属性哪个先到了producer都会开始发送消息
5. serializer.class: 默认是DefaultEncoder类,即使用哪个类对消息实现序列化
6. key.serializer.class: key也可以有自己的序列化类,如果不指定默认使用serializer.class中的配置。
二十一、ProducerSendThread.scala
    主要是为异步发送使用。该类就是一个集成Java Thread类的子类实现,底层维护了一个BlockingQueue阻塞队列用于实现消息缓冲区,并且还指定了队列数据最大等待时间和批处理大小。同时该类还定义了一个事件处理器类EventHandler用于处理事件。
    在类的内部定义了一个CountDownLatch用于关闭该线程使用,同时还构造了一个空的消息作为关闭命令在关闭时候会将该空消息放入缓冲队列中。ProducerSendThread提供的类方法如下:
1. tryHandle —— 使用EventHandler实现类的handle方法来处理事件发送
2. processEvents —— 从队列中一次性提取出queuetime(5s)以内的所有消息(不包括用于标识关闭状态的空消息)将其加到最后待发送的事件数组中。并判断超时和batch满哪个条件被满足了——主要是用于日志输出时使用——最后调用tryToHandle方法发送事件。不断循环该过程直到我们碰到了shutdown command的空消息就退出该循环。然后发送最后一批的事件。最后方法结束。
3. shutdown —— 将shutdown空消息放入队列并等待线程运行结束后关闭
4. run —— 就是运行processEvents发送事件,最后将shutdownLatch减为0关闭线程
二十二、DefaultEventHandler.scala
默认的事件处理器类。由于类代码太多,我们按照构造函数、类变量、类方法的顺序分析
构造函数: 接收一个配置类实例,分区策略类实例,消息编码类实例,键编码类实例,ProducerPool以及一个哈希表,保存topic的元数据信息。
类变量
1. isSync —— 标识是同步发送还是异步
2. correlationId —— 发送请求的correlationId,初始值是0
3. brokerPartitionInfo —— broker的分区信息
4. topicMetadataRefreshInterval —— Kafka定期查询topic的元数据信息,默认是10分钟
5. lastTopicMetadataRefreshTime —— 最近一次刷新topic元数据信息的时间
6. topicMetadataRefresh —— 待刷新元数据的topic集合
7. sendPartitionPerTopicCache —— 每个topic的分区缓存,保存topic =》 分区号的映射
8. producerStats —— producer统计信息
9. producerTopicStats —— producer topic的统计信息
类方法
1. close: 如果底层的producer池不为空,关闭池中所有producer的SocketChannel
2. groupMessageToSet: 返回一个映射表,保存每个topic分区对应的消息列表。这里需要考虑压缩的情况,如果指定了压缩算法但没有指定要压缩的topic,那么就为所有topic都创建带压缩的消息集合,否则就只为compressed.topics中指定的topic创建这样的消息集合。
3. getPartition: 计算producer要发送消息到哪个分区,如果该分区的ID不在[0, 分区数 -1]区间内抛出异常。值得一提的是,如果key不为空,那么直接传递给producer的partitioner用于计算最终的目标分区号。但如果没有指定key的话,其实partitioner及分区策略就没有用了,Kafka只是查询该topic的发送分区缓存来决定目标分区。具体做法是先从发送分区缓存(sendPartitionPerTopicCache变量)中查询对应topic的元数据信息。如果元数据信息存在,那么直接返回元数据信息中的分区号即可——这里并不检查这个分区的可用性,因为如果不可用发送会失败的;如果元数据信息不存在,就用传入的可用分区列表中找出各个partition的leader分区,如果都不存在抛出异常,否则就随机挑选一个leader broker,然后获取其对应的partition id加入到缓存中并作为目标分区号返回。注意这个缓存也不是一直有效,默认是10分钟会刷新一次,另外请求更新topic元数据时也会刷新该缓存。
4. getPartitionListForTopic: 根据给定的一组消息获取对应topic的分区列表,包括topic + 分区号+leaderbroker id
5. serialize: 将待发送消息序列化。遍历待发送消息集合,序列化每条消息。在序列化过程中出现任何异常,如果是同步producer就抛出异常,否则只是记录下该错误
6. partitionAndCollate: 将待发送的消息分组。最后返回的格式是【leader broker id -> 【topic + partition -> messages】】。具体做法就是遍历待发送的消息集合,每当获取一条消息的时候,先计算出要发送这条消息到哪个分区,然后找出这个目标分区的leader broker id加入到最后返回的哈希map中。然后将该条消息放入topic+parititon为key的子哈希表中,再将这个子哈希表记录加到最后的哈希表中返回。计算目标分区的时候如果给定的分区列表为空表明topic不存在,会抛出UnknownTopicOrPartitionException异常,同样地,如果计算出来的目标分区不在[0, numPartitions - 1]区间内也会抛出这个异常。如果分区列表中所有分区的leader都不存在的话那么就会抛出LeaderNotAvailableException异常。
7. send: 创建producer请求发送消息,需要指定要接收此请求的broker以及要发送的消息集合。该方法会返回一个列表,表示发送出错的那些消息所属的topic和分区。
8. dispatchSerializedData: 调用send方法发送分区后的消息,该方法会返回发送失败的请求
9. handle: 核心方法,就是发送一个消息列表。首先将消息序列化,然后尝试message.send.max.retries + 1(默认是3 + 1 = 4次)次发送这些消息,每次有发送失败的就等待retry.backoff.ms(默认是100ms)重新更新这些消息所属topic的元数据然后再试。如果4次之后还有失败的发送请求,那么记录下对应topic和correlation id抛出异常表明发送消息失败。
posted @ 2015-05-07 17:44  huxihx  阅读(1992)  评论(0编辑  收藏  举报