上次留下来的问题

  1. 如果消息是发给很多不同的topic的, async producer如何在按batch发送的同时区分topic的
  2. 它是如何用key来做partition的?
  3. 是如何实现对消息成批量的压缩的?
  • async producer如何在按batch发送的同时区分topic的

  这个问题的答案是: DefaultEventHandler会把发给它的一个batch的消息(实际上是Seq[KeyedMessage[K,V]]类型)拆开,确定每条消息该发送给哪个broker。对发给每个broker的消息,会按topic和partition来组合。即:拆包=>根据metaData组装

这个功能是通过partitionAndCollate方法实现的

def partitionAndCollate(messages: Seq[KeyedMessage[K,Message]]): Option[Map[Int, collection.mutable.Map[TopicAndPartition, Seq[KeyedMessage[K,Message]]]]]

  它返回一个Option对象,这个Option的元素是一个Map,Key是brokerId,value是发给这个broker的消息。对每一条消息,先确定它要被发给哪一个topic的哪个parition。然后确定这个parition的leader broker,然后去Map[Int, collection.mutable.Map[TopicAndPartition, Seq[KeyedMessage[K,Message]]]]这个Map里找到对应的broker,然后把这条消息填充给对应的topic+partition对应的Seq[KeyedMessage[K,Message]]。这样就得到了最后的结果。这个结果表示了哪些消息要以怎样的结构发给一个broker。真正发送的时候,会按照brokerId的不同,把打包好的消息发给不同的broker。

首先,看一下kafka protocol里对于Producer Request结构的说明:

ProduceRequest => RequiredAcks Timeout [TopicName [Partition MessageSetSize MessageSet]]
  RequiredAcks => int16
  Timeout => int32
  Partition => int32
  MessageSetSize => int32

发给一个broker的消息就是这样的结构。

同时,在kafka wiki里对于Produce API 有如下说明:

The produce API is used to send message sets to the server. For efficiency it allows sending message sets intended for many topic partitions in a single request.

即在一个produce request里,可以同时发消息给多个topic+partition的组合。当然一个produce request是发给一个broker的。

使用

send(brokerid, messageSetPerBroker)

  把消息set发给对应的brokerid。

  • 它是如何用key来做partition的?

首先看下KeyedMessage类的定义:

case class KeyedMessage[K, V](val topic: String, val key: K, val partKey: Any, val message: V) {
if(topic == null)
throw new IllegalArgumentException("Topic cannot be null.")
def this(topic: String, message: V) = this(topic, null.asInstanceOf[K], null, message)
def this(topic: String, key: K, message: V) = this(topic, key, key, message)
def partitionKey = {
if(partKey != null)
partKey
else if(hasKey)
key
else
null
}
def hasKey = key != null
}

  当使用三个参数的构造函数时, partKey会等于key。partKey是用来做partition的,但它不会最当成消息的一部分被存储。

前边提到了,在确定一个消息应该发给哪个broker之前,要先确定它发给哪个partition,这样才能根据paritionId去找到对应的leader所在的broker。

val topicPartitionsList = getPartitionListForTopic(message) //获取这个消息发送给的topic的partition信息
val partitionIndex = getPartition(message.topic, message.partitionKey, topicPartitionsList)//确定这个消息发给哪个partition

  注意传给getPartition方法中时使用的是partKey。getPartition方法为:

  private def getPartition(topic: String, key: Any, topicPartitionList: Seq[PartitionAndLeader]): Int = {
val numPartitions = topicPartitionList.size
if(numPartitions <= 0)
throw new UnknownTopicOrPartitionException("Topic " + topic + " doesn't exist")
val partition =
if(key == null) {
// If the key is null, we don't really need a partitioner
// So we look up in the send partition cache for the topic to decide the target partition
val id = sendPartitionPerTopicCache.get(topic)
id match {
case Some(partitionId) =>
// directly return the partitionId without checking availability of the leader,
// since we want to postpone the failure until the send operation anyways
partitionId
case None =>
val availablePartitions = topicPartitionList.filter(_.leaderBrokerIdOpt.isDefined)
if (availablePartitions.isEmpty)
throw new LeaderNotAvailableException("No leader for any partition in topic " + topic)
val index = Utils.abs(Random.nextInt) % availablePartitions.size
val partitionId = availablePartitions(index).partitionId
sendPartitionPerTopicCache.put(topic, partitionId)
partitionId
}
} else
partitioner.partition(key, numPartitions)

  当partKey为null时,首先它从sendParitionPerTopicCache里取这个topic缓存的partitionId,这个cache是一个Map.如果之前己经使用sendPartitionPerTopicCache.put(topic, partitionId)缓存了一个,就直接取出它。否则就随机从可用的partitionId里取出一个,把它缓存到sendParitionPerTopicCache。这就使得当sendParitionPerTopicCache里有一个可用的partitionId时,很多消息都会被发送给这同一个partition。因此若所有消息的partKey都为空,在一段时间内只会有一个partition能收到消息。之所以会说“一段”时间,而不是永久,是因为handler隔一段时间会重新获取它发送过的消息对应的topic的metadata,这个参数通过topic.metadata.refresh.interval.ms来设置。当它重新获取metadata之后,会消空一些缓存,就包括这个sendParitionPerTopicCache。因此,接下来就会生成另一个随机的被缓存的partitionId。

  if (topicMetadataRefreshInterval >= 0 &&
SystemTime.milliseconds - lastTopicMetadataRefreshTime > topicMetadataRefreshInterval) { //若该refresh topic metadata 了,do the refresh
Utils.swallowError(brokerPartitionInfo.updateInfo(topicMetadataToRefresh.toSet, correlationId.getAndIncrement))
sendPartitionPerTopicCache.clear()
topicMetadataToRefresh.clear
lastTopicMetadataRefreshTime = SystemTime.milliseconds
}

  当partKey不为null时,就用传给handler的partitioner的partition方法,根据partKey和numPartitions来确定这个消息被发给哪个partition。注意这里的numPartition是topicPartitionList.size获取的,有可能会有parition不存在可用的leader。这样的问题将留给send时解决。实际上发生这种情况时,partitionAndCollate会将这个消息分派给brokerId为-1的broker。而send方法会在发送前判断brokerId

    if(brokerId < 0) {
warn("Failed to send data since partitions %s don't have a leader".format(messagesPerTopic.map(_._1).mkString(",")))
messagesPerTopic.keys.toSeq

  当brokerId<0时,就返回一个非空的Seq,包括了所有没有leader的topic+partition的组合,如果重试了指定次数还不能发送,将最终导致handle方法抛出一个 FailedToSendMessageException异常。

  • 是如何实现对消息成批量的压缩的?

这个是在

private def groupMessagesToSet(messagesPerTopicAndPartition: collection.mutable.Map[TopicAndPartition, Seq[KeyedMessage[K,Message]]])

中处理。

说明为:

/** enforce the compressed.topics config here.
* If the compression codec is anything other than NoCompressionCodec,
* Enable compression only for specified topics if any
* If the list of compressed topics is empty, then enable the specified compression codec for all topics
* If the compression codec is NoCompressionCodec, compression is disabled for all topics
*/

即,如果没有设置压缩,就所有topic对应的消息集都不压缩。如果设置了压缩,并且没有设置对个别topic启用压缩,就对所有topic都使用压缩;否则就只对设置了压缩的topic压缩。

在这个gruopMessageToSet中,并不有具体的压缩逻辑。而是返回一个ByteBufferMessageSet对象。它的注释为:

/**
* A sequence of messages stored in a byte buffer
*
* There are two ways to create a ByteBufferMessageSet
*
* Option 1: From a ByteBuffer which already contains the serialized message set. Consumers will use this method.
*
* Option 2: Give it a list of messages along with instructions relating to serialization format. Producers will use this method.

看来它是对于消息集进行序列化和反序列化的工具。

在它的实现里用到了CompressionFactory对象。从它的实现里可以看到Kafka只支持GZIP和Snappy两种压缩方式。

compressionCodec match {
case DefaultCompressionCodec => new GZIPOutputStream(stream)
case GZIPCompressionCodec => new GZIPOutputStream(stream)
case SnappyCompressionCodec =>
import org.xerial.snappy.SnappyOutputStream
new SnappyOutputStream(stream)
case _ =>
throw new kafka.common.UnknownCodecException("Unknown Codec: " + compressionCodec)

  

Kafka 之 async producer (2) kafka.producer.async.DefaultEventHandler的更多相关文章

  1. Kafka深度解析(如何在producer中指定partition)(转)

    原文链接:Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅的消息系统.主要设计目标如下: 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能 ...

  2. kafka 0.8.2 消息生产者 producer

    package com.hashleaf.kafka; import java.util.Properties; import kafka.javaapi.producer.Producer; imp ...

  3. Kafka 0.11.0.0 实现 producer的Exactly-once 语义(官方DEMO)

    <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients&l ...

  4. Kafka 0.11.0.0 实现 producer的Exactly-once 语义(中文)

    很高兴地告诉大家,具备新的里程碑意义的功能的Kafka 0.11.x版本(对应 Confluent Platform 3.3)已经release,该版本引入了exactly-once语义,本文阐述的内 ...

  5. Kafka 0.11.0.0 实现 producer的Exactly-once 语义(英文)

    Exactly-once Semantics are Possible: Here’s How Kafka Does it I’m thrilled that we have hit an excit ...

  6. Kafka 详解(三)------Producer生产者

    在第一篇博客我们了解到一个kafka系统,通常是生产者Producer 将消息发送到 Broker,然后消费者 Consumer 去 Broker 获取,那么本篇博客我们来介绍什么是生产者Produc ...

  7. Kafka学习(四)-------- Kafka核心之Producer

    通过https://www.cnblogs.com/tree1123/p/11243668.html 已经对consumer有了一定的了解.producer比consumer要简单一些. 一.旧版本p ...

  8. Apache Kafka(六)- High Throughput Producer

    High Throughput Producer 在有大量消息需要发送的情况下,默认的Kafka Producer配置可能无法达到一个可观的的吞吐.在这种情况下,我们可以考虑调整两个方面,以提高Pro ...

  9. Apache Kafka(五)- Safe Kafka Producer

    Kafka Safe Producer 在应用Kafka的场景中,需要考虑到在异常发生时(如网络异常),被发送的消息有可能会出现丢失.乱序.以及重复消息. 对于这些情况,我们可以创建一个“safe p ...

  10. 《Apache kafka实战》读书笔记-kafka集群监控工具

    <Apache kafka实战>读书笔记-kafka集群监控工具 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 如官网所述,Kafka使用基于yammer metric ...

随机推荐

  1. SQL按照日、周、月、年统计数据

    写sql语句分别按日,星期,月,季度,年统计销售额 --按日 select sum(consume),day([date]) from consume_record where year([date] ...

  2. GPU总结(1)

    目录 1.为什么要引入GPU 2.CUDA环境的搭建--CUDA全称Compute Unified Device Architecture统一计算设备架构,CUDA是以后总将GPU作为数据并行计算设备 ...

  3. AES加密解密的例子小结

    话不多说,先放上代码,一共有两个文件:AES.php(aes算法类文件)和aesDemo.php(应用实例文件),这里只贴出aesDemo.php,其他的看附件吧!aesDemo.php: 例子,   ...

  4. .net转java了

    公司技术部门  要求.net全体转向java 本来要看看.net core的 看来是没必要了  现在国内互联网公司.net是越来越少  不知道为何会这样 不过java的生态圈 确实是很强大 也很丰富 ...

  5. LINQ(隐式表达式、lambda 表达式)

    .NET 中一项突破性的创新是 LINQ(Language Integrated Query,语言集成查询),这组语言扩展让你能够不必离开舒适的 C# 语言执行查询. LINQ 定义了用于构建查询表达 ...

  6. HR不会告诉你的秘密

    原文转载自http://blog.csdn.net/happy08god/article/details/5534326 下面,只是摘出来一些基本的观点. 1. 入职时的工资高低不重要,只要你努力工作 ...

  7. 使用JavaScript实现简单的输入校验

    HTML页面代码: <!doctype html> <html lang="en"> <head> <meta charset=" ...

  8. iOS 非ARC基本内存管理系列 4-autorelease方法和@autoreleasepool

    1.autorelease 基本用法 对象执行autorelease方法时会将对象添加到自动释放池中 当自动释放池销毁时自动释放池中所有对象作release操作 对象执行autorelease方法后自 ...

  9. PHP 魔术方法 __construct __destruct (一)

    慢慢长寻夜,明月高空挂 __construct() - 在每次创建新对象时先调用此方法 __destruct()  - 对象的所有引用都被删除或者当对象被显式销毁时执行 <?php /** * ...

  10. 登堂入室——java流

    ——文章出自PeterYe,不得私自转载 我所知道的 java.io里面的[流],就仿佛太平洋里面的水一样,浩浩荡荡,横无际涯... -----2016/7/16--------公寓处记录------ ...