上次留下來的問題
- 如果訊息是發給很多不同的topic的, async producer如何在按batch傳送的同時區分topic的
- 它是如何用key來做partition的?
- 是如何實現對訊息成批量的壓縮的?
-
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)