Kafka 之 async producer (1)

devos發表於2014-03-27

問題 

  1. 很多條訊息是怎麼打包在一起的?
  2. 如果訊息是發給很多不同的topic的, async producer如何在按batch傳送的同時區分topic的
  3. 它是如何用key來做partition的?
  4. 是如何實現對訊息成批量的壓縮的?

async producer是將producer.type設為async時啟用的producer

此時,呼叫send方法的執行緒和實際完成訊息傳送的執行緒是分開的。

當呼叫java API中producer的send方法時,最終會呼叫kafka.producer.Producer的send方法。在kafka.producer.Producer類中,會根據producer.type配置使用不同的方法傳送訊息。

def send(messages: KeyedMessage[K,V]*) {
    lock synchronized {
      if (hasShutdown.get)
        throw new ProducerClosedException
      recordStats(messages)
      sync match {
        case true => eventHandler.handle(messages)
        case false => asyncSend(messages)
      }
    }
  }

  當async時,會使用asyncSend。asyncSend方法會根據“queue.enqueue.timeout.ms”配置選項採用BlockingQueue的put或offer方法把訊息放入kafka.producer.Producer持有的一個LinkedBlockingQueue。一個ProducerSendThread執行緒從queue裡取訊息,成批量的用eventHandler來處理。

  當使用sync時,對每條訊息會直接使用eventHandler來處理。這就是為什麼前一種方式會被稱為"asynchornization",而這一種會稱為”synchronization"

  private val queue = new LinkedBlockingQueue[KeyedMessage[K,V]](config.queueBufferingMaxMessages)

  在kafka.producer.Producer構造時,會檢查"producer.type“,如果是asnyc,就會開啟一個送發執行緒。

  config.producerType match {
    case "sync" =>
    case "async" =>
      sync = false
      producerSendThread = new ProducerSendThread[K,V]("ProducerSendThread-" + config.clientId,
                                                       queue,
                                                       eventHandler,
                                                       config.queueBufferingMaxMs,
                                                       config.batchNumMessages,
                                                       config.clientId)
      producerSendThread.start()

  現在有了一個佇列,一個傳送執行緒 。看來這個ProducerSendThread是來完成大部分傳送的工作,而"async"的特性都主要都是由它來實現。

   這個執行緒的run方法實現為:

  override def run {
    try {
      processEvents
    }catch {
      case e: Throwable => error("Error in sending events: ", e)
    }finally {
      shutdownLatch.countDown
    }
  }

  看來實際工作由processEvents方法來實現嘍

  private def processEvents() {
    var lastSend = SystemTime.milliseconds //上一次傳送的時間,每傳送一次會更新
    var events = new ArrayBuffer[KeyedMessage[K,V]] //一起傳送的訊息的集合,傳送完後也會更新
    var full: Boolean = false  //是否訊息的數量已大於指定的batch大小(batch大小指多少訊息在一起傳送,由"batch.num.messages"確定)

    // drain the queue until you get a shutdown command
    //構造一個流,它的每個元素為queue.poll(timeout)取出來的值。
    //timeout的值是這麼計算的:lastSend+queueTime表示下次傳送的時間,再減去當前時間,就是最多還能等多長時間,也就是poll阻塞的最長時間
    //takeWhile接受的函式引數決定了當item是shutdownCommand時,流就結束了。這個shutdownCommand是shutdown()方法執行時,往佇列裡發的一個特殊訊息
    Stream.continually(queue.poll(scala.math.max(0, (lastSend + queueTime) - SystemTime.milliseconds), TimeUnit.MILLISECONDS))
                      .takeWhile(item => if(item != null) item ne shutdownCommand else true).foreach {
      currentQueueItem => 										//對每一條處理的訊息
        val elapsed = (SystemTime.milliseconds - lastSend)  //距上次傳送已逝去的時間,只記錄在debug裡,並不會以它作為是否傳送的條件
        // check if the queue time is reached. This happens when the poll method above returns after a timeout and
        // returns a null object
        val expired = currentQueueItem == null //當poll方法超時,就返回一個null,說明一定已經是時候傳送這批訊息了。當時間到了,poll(timeout)中timeout為負值時,poll一定返回null
        if(currentQueueItem != null) {
          trace("Dequeued item for topic %s, partition key: %s, data: %s"
              .format(currentQueueItem.topic, currentQueueItem.key, currentQueueItem.message))
          events += currentQueueItem //如果當前訊息不為空,就附加在傳送集合裡
        }

        // check if the batch size is reached
        full = events.size >= batchSize //是否當前傳送集合的大小已經大於batch size

        if(full || expired) {  //如果傳送集合有了足夠多的訊息或者按時間計可以傳送了,就傳送
          if(expired)
            debug(elapsed + " ms elapsed. Queue time reached. Sending..")
          if(full)
            debug("Batch full. Sending..")
          // if either queue time has reached or batch size has reached, dispatch to event handler
          tryToHandle(events)
          lastSend = SystemTime.milliseconds //更新lastSend,將一個新的ArrayBuffer的引用賦給events
          events = new ArrayBuffer[KeyedMessage[K,V]]
        }
    }
    // send the last batch of events
    tryToHandle(events) //當shutdownCommand遇到時,流會終結。此時之前的訊息只要不是恰好傳送完,就還會有一些在events裡,做為最後一批傳送。
    if(queue.size > 0) //些時producerSendThread已經不再發訊息了,但是queue裡若還有沒發完的,就是一種異常情況
      throw new IllegalQueueStateException("Invalid queue state! After queue shutdown, %d remaining items in the queue"
        .format(queue.size))
  }

  看來Scala的Stream幫了不少忙。shutdown方法將一個特殊的shutdownCommand發給queue,也正好使得這個Stream可以用takeWhile方法正確結束。

  好吧,搞了這麼多,這個ProducerSendThread只有打包的邏輯 ,並沒有處理topic、partition、壓縮的邏輯,這些邏輯都在另一個類中。明天再來看看這個handler

相關文章