Kafka 之 async producer (2) kafka.producer.async.DefaultEventHandler

devos發表於2014-03-29

上次留下來的問題

  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)

  

相關文章