kafka請求全流程(二)—— 請求的接收以及分發

承接上一篇(https://blog.csdn.net/fenglei0415/article/details/106162288)

二. 請求的接收以及分發

主要分析兩個類,實現網絡通信的關鍵部件。分別是Acceptor 類Processor 類。

先介紹下SocketServer組件下的類:

  1. AbstractServerThread 類:這是 Acceptor 線程和 Processor 線程的抽象基類,定義了這兩個線程的公有方法,如 shutdown(關閉線程)等。
  2. Acceptor 線程類:這是接收和創建外部 TCP 連接的線程。每個 SocketServer 實例只會創建一個 Acceptor 線程。它的唯一目的就是創建連接,並將接收到的 Request 傳遞給下游的 Processor 線程處理。
  3. Processor 線程類:這是處理單個 TCP 連接上所有請求的線程。每個 SocketServer 實例默認創建 num.network.threads 個Processor 線程。Processor 線程負責將接收到的 Request 添加到 RequestChannel 的 Request 隊列上,同時還負責將 Response 返還給 Request 發送方。
  4. Processor 伴生對象類:僅僅定義了一些與 Processor 線程相關的常見監控指標和常量等,如 Processor 線程空閒率等。
  5. ConnectionQuotas 類:是控制連接數配額的類。我們能夠設置單個 IP 創建 Broker 連接的最大數量,以及單個 Broker 能夠允許的最大連接數。
  6. TooManyConnectionsException 類:SocketServer 定義的一個異常類,用於標識連接數配額超限情況。
  7. SocketServer 類:實現了對以上所有組件的管理和操作,如創建和關閉 Acceptor、Processor 線程等。
  8. SocketServer 伴生對象類:定義了一些有用的常量,同時明確了 SocketServer 組件中的哪些參數是允許動態修改的。

Acceptor 線程

經典的 Reactor 模式有個 Dispatcher 的角色,接收外部請求並分發給下面的實際處理線程。在 Kafka 中,這個 Dispatcher 就是 Acceptor 線程。

private[kafka] class Acceptor(val endPoint: EndPoint,
                              val sendBufferSize: Int,
                              val recvBufferSize: Int,
                              brokerId: Int,
                              connectionQuotas: ConnectionQuotas,
                              metricPrefix: String) extends AbstractServerThread(connectionQuotas) with KafkaMetricsGroup {
  // 創建底層的NIO Selector對象
  // Selector對象負責執行底層實際I/O操作,如監聽連接創建請求、讀寫請求等
  private val nioSelector = NSelector.open() 
  // Broker端創建對應的ServerSocketChannel實例
  // 後續把該Channel向上一步的Selector對象註冊
  val serverChannel = openServerSocket(endPoint.host, endPoint.port)
  // 創建Processor線程池,實際上是Processor線程數組
  private val processors = new ArrayBuffer[Processor]()
  private val processorsStarted = new AtomicBoolean

  private val blockedPercentMeter = newMeter(s"${metricPrefix}AcceptorBlockedPercent",
    "blocked time", TimeUnit.NANOSECONDS, Map(ListenerMetricTag -> endPoint.listenerName.value))
  ......
}

從定義來看,Acceptor 線程接收 5 個參數,其中比較重要的有 3 個。

  • endPoint。它就是你定義的 Kafka Broker 連接信息,比如 PLAINTEXT://localhost:9092。Acceptor 需要用到 endPoint 包含的主機名和端口信息創建 Server Socket。
  • sendBufferSize。它設置的是 SocketOptions 的 SO_SNDBUF,即用於設置出站(Outbound)網絡 I/O 的底層緩衝區大小。該值默認是 Broker 端參數 socket.send.buffer.bytes 的值,即 100KB。
  • recvBufferSize。它設置的是 SocketOptions 的 SO_RCVBUF,即用於設置入站(Inbound)網絡 I/O 的底層緩衝區大小。該值默認是 Broker 端參數 socket.receive.buffer.bytes 的值,即 100KB。

Acceptor 線程在初始化時,需要創建對應的網絡 Processor 線程池。可見,Processor 線程是在 Acceptor 線程中管理和維護的。既然如此,那它就必須要定義相關的方法。Acceptor 代碼中,提供了 3 個與 Processor 相關的方法,分別是 addProcessors、startProcessors 和 removeProcessors。大概看下代碼邏輯:

addProcessors

private[network] def addProcessors(
  newProcessors: Buffer[Processor], processorThreadPrefix: String): Unit = synchronized {
  processors ++= newProcessors // 添加一組新的Processor線程
  if (processorsStarted.get) // 如果Processor線程池已經啓動
    startProcessors(newProcessors, processorThreadPrefix) // 啓動新的Processor線程
}

startProcessors

private[network] def startProcessors(processorThreadPrefix: String): Unit = synchronized {
    if (!processorsStarted.getAndSet(true)) {  // 如果Processor線程池未啓動
      startProcessors(processors, processorThreadPrefix) // 啓動給定的Processor線程
    }
}

private def startProcessors(processors: Seq[Processor], processorThreadPrefix: String): Unit = synchronized {
  processors.foreach { processor => // 依次創建並啓動Processor線程
  // 線程命名規範:processor線程前綴-kafka-network-thread-broker序號-監聽器名稱-安全協議-Processor序號
  // 假設爲序號爲0的Broker設置PLAINTEXT://localhost:9092作爲連接信息,那麼3個Processor線程名稱分別爲:
  // data-plane-kafka-network-thread-0-ListenerName(PLAINTEXT)-PLAINTEXT-0
  // data-plane-kafka-network-thread-0-ListenerName(PLAINTEXT)-PLAINTEXT-1
  // data-plane-kafka-network-thread-0-ListenerName(PLAINTEXT)-PLAINTEXT-2
  KafkaThread.nonDaemon(s"${processorThreadPrefix}-kafka-network-thread-$brokerId-${endPoint.listenerName}-${endPoint.securityProtocol}-${processor.id}", processor).start()
  }
}

removeProcessors

private[network] def removeProcessors(removeCount: Int, requestChannel: RequestChannel): Unit = synchronized {
  // 獲取Processor線程池中最後removeCount個線程
  val toRemove = processors.takeRight(removeCount)
  // 移除最後removeCount個線程
  processors.remove(processors.size - removeCount, removeCount)
  // 關閉最後removeCount個線程
  toRemove.foreach(_.shutdown())
  // 在RequestChannel中移除這些Processor
  toRemove.foreach(processor => requestChannel.removeProcessor(processor.id))
}

Acceptor 類邏輯的重頭戲其實是 run 方法,它是處理 Reactor 模式中分發邏輯的主要實現方法。

def run(): Unit = {
  //註冊OP_ACCEPT事件
  serverChannel.register(nioSelector, SelectionKey.OP_ACCEPT)
  // 等待Acceptor線程啓動完成
  startupComplete()
  try {
    // 當前使用的Processor序號,從0開始,最大值是num.network.threads - 1
    var currentProcessorIndex = 0
    while (isRunning) {
      try {
        // 每500毫秒獲取一次就緒I/O事件
        val ready = nioSelector.select(500)
        if (ready > 0) { // 如果有I/O事件準備就緒
          val keys = nioSelector.selectedKeys()
          val iter = keys.iterator()
          while (iter.hasNext && isRunning) {
            try {
              val key = iter.next
              iter.remove()
              if (key.isAcceptable) {
                // 調用accept方法創建Socket連接
                accept(key).foreach { socketChannel =>
                  var retriesLeft = synchronized(processors.length)
                  var processor: Processor = null
                  do {
                    retriesLeft -= 1
                    // 指定由哪個Processor線程進行處理
                    processor = synchronized {
                      currentProcessorIndex = currentProcessorIndex % processors.length
                      processors(currentProcessorIndex)
                    }
                    // 更新Processor線程序號
                    currentProcessorIndex += 1
                  } while (!assignNewConnection(socketChannel, processor, retriesLeft == 0)) // Processor是否接受了該連接
                }
              } else
                throw new IllegalStateException("Unrecognized key state for acceptor thread.")
            } catch {
              case e: Throwable => error("Error while accepting connection", e)
            }
          }
        }
      }
      catch {
        case e: ControlThrowable => throw e
        case e: Throwable => error("Error occurred", e)
      }
    }
  } finally { // 執行各種資源關閉邏輯
    debug("Closing server socket and selector.")
    CoreUtils.swallow(serverChannel.close(), this, Level.ERROR)
    CoreUtils.swallow(nioSelector.close(), this, Level.ERROR)
    shutdownComplete()
  }
}

基本上,Acceptor 線程使用 Java NIO 的 Selector + SocketChannel 的方式循環地輪詢準備就緒的 I/O 事件。這裏的 I/O 事件,主要是指網絡連接創建事件,即代碼中的 SelectionKey.OP_ACCEPT。一旦接收到外部連接請求,Acceptor 就會指定一個 Processor 線程,並將該請求交由它,讓它創建真正的網絡連接。總的來說,Acceptor 線程就做這麼點事。

Processor 線程

如果說 Acceptor 是做入站連接處理的,那麼,Processor 代碼則是真正創建連接以及分發請求的地方。顯然,它要做的事情遠比 Acceptor 要多得多。看下run 方法:

override def run(): Unit = {
    startupComplete() // 等待Processor線程啓動完成
    try {
      while (isRunning) {
        try {
          configureNewConnections() // 創建新連接
          // register any new responses for writing
          processNewResponses() // 發送Response,並將Response放入到inflightResponses臨時隊列
          poll() // 執行NIO poll,獲取對應SocketChannel上準備就緒的I/O操作
          processCompletedReceives() // 將接收到的Request放入Request隊列
          processCompletedSends() // 爲臨時Response隊列中的Response執行回調邏輯
          processDisconnected() // 處理因發送失敗而導致的連接斷開
          closeExcessConnections() // 關閉超過配額限制部分的連接
        } catch {
          case e: Throwable => processException("Processor got uncaught exception.", e)
        }
      }
    } finally { // 關閉底層資源
      debug(s"Closing selector - processor $id")
      CoreUtils.swallow(closeAll(), this, Level.ERROR)
      shutdownComplete()
    }
}

每個 Processor 線程在創建時都會創建 3 個隊列。注意,這裏的隊列是廣義的隊列,其底層使用的數據結構可能是阻塞隊列,也可能是一個 Map 對象而已,如下所示:

private val newConnections = new ArrayBlockingQueue[SocketChannel](connectionQueueSize)
private val inflightResponses = mutable.Map[String, RequestChannel.Response]()
private val responseQueue = new LinkedBlockingDeque[RequestChannel.Response]()

隊列一:newConnections

它保存的是要創建的新連接信息,具體來說,就是 SocketChannel 對象。這是一個默認上限是 20 的隊列,而且,目前代碼中硬編碼了隊列的長度,因此,你無法變更這個隊列的長度。

每當 Processor 線程接收新的連接請求時,都會將對應的 SocketChannel 放入這個隊列。後面在創建連接時(也就是調用 configureNewConnections 時),就從該隊列中取出 SocketChannel,然後註冊新的連接。

隊列二:inflightResponses

嚴格來說,這是一個臨時 Response 隊列。當 Processor 線程將 Response 返還給 Request 發送方之後,還要將 Response 放入這個臨時隊列。爲什麼需要這個臨時隊列呢?

這是因爲,有些 Response 回調邏輯要在 Response 被髮送回發送方之後,才能執行,因此需要暫存在一個臨時隊列裏面。這就是 inflightResponses 存在的意義。

隊列三:responseQueue

這是 Response 隊列,而不是 Request 隊列。這告訴了我們一個事實:每個 Processor 線程都會維護自己的 Response 隊列,Response 隊列裏面保存着需要被返還給發送方的所有 Response 對象。需要注意的是:Request隊列是共享的,而response隊列是某個Processor線程專享的,並不是每個線程都需要有響應的。

下面依次看下run裏面的方法:

configureNewConnections

private def configureNewConnections(): Unit = {
    var connectionsProcessed = 0 // 當前已配置的連接數計數器
    while (connectionsProcessed < connectionQueueSize && !newConnections.isEmpty) { // 如果沒超配額並且有待處理新連接
      val channel = newConnections.poll() // 從連接隊列中取出SocketChannel
      try {
        debug(s"Processor $id listening to new connection from ${channel.socket.getRemoteSocketAddress}")
        // 用給定Selector註冊該Channel
        // 底層就是調用Java NIO的SocketChannel.register(selector, SelectionKey.OP_READ)
        selector.register(connectionId(channel.socket), channel)
        connectionsProcessed += 1 // 更新計數器
      } catch {
        case e: Throwable =>
          val remoteAddress = channel.socket.getRemoteSocketAddress
          close(listenerName, channel)
          processException(s"Processor $id closed connection from $remoteAddress", e)
      }
    }
}

該方法最重要的邏輯是調用 selector 的 register 來註冊 SocketChannel。每個 Processor 線程都維護了一個 Selector 類實例。Selector 類是社區提供的一個基於 Java NIO Selector 的接口,用於執行非阻塞多通道的網絡 I/O 操作。在覈心功能上,Kafka 提供的 Selector 和 Java 提供的是一致的。

processNewResponses

它負責發送 Response 給 Request 發送方,並且將 Response 放入臨時 Response 隊列。處理邏輯如下:

private def processNewResponses(): Unit = {
    var currentResponse: RequestChannel.Response = null
    while ({currentResponse = dequeueResponse(); currentResponse != null}) { // Response隊列中存在待處理Response
      val channelId = currentResponse.request.context.connectionId // 獲取連接通道ID
      try {
        currentResponse match {
          case response: NoOpResponse => // 無需發送Response
            updateRequestMetrics(response)
            trace(s"Socket server received empty response to send, registering for read: $response")
            handleChannelMuteEvent(channelId, ChannelMuteEvent.RESPONSE_SENT)
            tryUnmuteChannel(channelId)
          case response: SendResponse => // 發送Response並將Response放入inflightResponses
            sendResponse(response, response.responseSend)
          case response: CloseConnectionResponse => // 關閉對應的連接
            updateRequestMetrics(response)
            trace("Closing socket connection actively according to the response code.")
            close(channelId)
          case _: StartThrottlingResponse =>
            handleChannelMuteEvent(channelId, ChannelMuteEvent.THROTTLE_STARTED)
          case _: EndThrottlingResponse =>
            handleChannelMuteEvent(channelId, ChannelMuteEvent.THROTTLE_ENDED)
            tryUnmuteChannel(channelId)
          case _ =>
            throw new IllegalArgumentException(s"Unknown response type: ${currentResponse.getClass}")
        }
      } catch {
        case e: Throwable =>
          processChannelException(channelId, s"Exception while processing response for $channelId", e)
      }
    }
}

這裏的關鍵是 SendResponse 分支上的 sendResponse 方法。這個方法的核心代碼其實只有三行:

if (openOrClosingChannel(connectionId).isDefined) { // 如果該連接處於可連接狀態
  selector.send(responseSend) // 發送Response
  inflightResponses += (connectionId -> response) // 將Response加入到inflightResponses隊列
}

poll

嚴格來說,上面提到的所有發送的邏輯都不是執行真正的發送。真正執行 I/O 動作的方法是這裏的 poll 方法。

poll 方法的核心代碼就只有 1 行:selector.poll(pollTimeout)。在底層,它實際上調用的是 Java NIO Selector 的 select 方法去執行那些準備就緒的 I/O 操作,不管是接收 Request,還是發送 Response。因此,你需要記住的是,poll 方法纔是真正執行 I/O 操作邏輯的地方。

processCompletedReceives

private def processCompletedReceives(): Unit = {
  // 遍歷所有已接收的Request
  selector.completedReceives.asScala.foreach { receive =>
    try {
      // 保證對應連接通道已經建立
      openOrClosingChannel(receive.source) match {
        case Some(channel) =>
          val header = RequestHeader.parse(receive.payload)
          if (header.apiKey == ApiKeys.SASL_HANDSHAKE && channel.maybeBeginServerReauthentication(receive, nowNanosSupplier))
            trace(s"Begin re-authentication: $channel")
          else {
            val nowNanos = time.nanoseconds()
            // 如果認證會話已過期,則關閉連接
            if (channel.serverAuthenticationSessionExpired(nowNanos)) {
              debug(s"Disconnecting expired channel: $channel : $header")
              close(channel.id)
              expiredConnectionsKilledCount.record(null, 1, 0)
            } else {
              val connectionId = receive.source
              val context = new RequestContext(header, connectionId, channel.socketAddress,
                channel.principal, listenerName, securityProtocol,
                channel.channelMetadataRegistry.clientInformation)
              val req = new RequestChannel.Request(processor = id, context = context,
                startTimeNanos = nowNanos, memoryPool, receive.payload, requestChannel.metrics)
              if (header.apiKey == ApiKeys.API_VERSIONS) {
                val apiVersionsRequest = req.body[ApiVersionsRequest]
                if (apiVersionsRequest.isValid) {
                  channel.channelMetadataRegistry.registerClientInformation(new ClientInformation(
                    apiVersionsRequest.data.clientSoftwareName,
                    apiVersionsRequest.data.clientSoftwareVersion))
                }
              }
              // 核心代碼:將Request添加到Request隊列
              requestChannel.sendRequest(req)
              selector.mute(connectionId)
              handleChannelMuteEvent(connectionId, ChannelMuteEvent.REQUEST_RECEIVED)
            }
          }
        case None =>
          throw new IllegalStateException(s"Channel ${receive.source} removed from selector before processing completed receive")
      }
    } catch {
      case e: Throwable =>
        processChannelException(receive.source, s"Exception while processing request from ${receive.source}", e)
    }
  }
}

其實最核心的代碼就只有 1 行:requestChannel.sendRequest(req),也就是將此 Request 放入 Request 隊列。其他代碼只是一些常規化的校驗和輔助邏輯。

這個方法的意思是說,Processor 從底層 Socket 通道不斷讀取已接收到的網絡請求,然後轉換成 Request 實例,並將其放入到 Request 隊列。

processCompletedSends

它負責處理 Response 的回調邏輯。我之前說過,Response 需要被髮送之後才能執行對應的回調邏輯,這便是該方法代碼要實現的功能:

private def processCompletedSends(): Unit = {
  // 遍歷底層SocketChannel已發送的Response
  selector.completedSends.asScala.foreach { send =>
    try {
      // 取出對應inflightResponses中的Response
      val response = inflightResponses.remove(send.destination).getOrElse {
        throw new IllegalStateException(s"Send for ${send.destination} completed, but not in `inflightResponses`")
      }
      updateRequestMetrics(response) // 更新一些統計指標
      // 執行回調邏輯
      response.onComplete.foreach(onComplete => onComplete(send))
      handleChannelMuteEvent(send.destination, ChannelMuteEvent.RESPONSE_SENT)
      tryUnmuteChannel(send.destination)
    } catch {
      case e: Throwable => processChannelException(send.destination,
        s"Exception while processing completed send to ${send.destination}", e)
    }
  }
}

processDisconnected

它就是處理已斷開連接的。比較關鍵的代碼是需要從底層 Selector 中獲取那些已經斷開的連接,之後把它們從 inflightResponses 中移除掉,同時也要更新它們的配額數據。

private def processDisconnected(): Unit = {
  // 遍歷底層SocketChannel的那些已經斷開的連接
  selector.disconnected.keySet.asScala.foreach { connectionId =>
    try {
      // 獲取斷開連接的遠端主機名信息
      val remoteHost = ConnectionId.fromString(connectionId).getOrElse {
        throw new IllegalStateException(s"connectionId has unexpected format: $connectionId")
      }.remoteHost
  // 將該連接從inflightResponses中移除,同時更新一些監控指標
  inflightResponses.remove(connectionId).foreach(updateRequestMetrics)
  // 更新配額數據
  connectionQuotas.dec(listenerName, InetAddress.getByName(remoteHost))
    } catch {
      case e: Throwable => processException(s"Exception while processing disconnection of $connectionId", e)
    }
  }
}

closeExcessConnections

這是 Processor 線程的 run 方法執行的最後一步,即關閉超限連接。

private def closeExcessConnections(): Unit = {
    // 如果配額超限了
    if (connectionQuotas.maxConnectionsExceeded(listenerName)) {
      // 找出優先關閉的那個連接
      val channel = selector.lowestPriorityChannel() 
      if (channel != null)
        close(channel.id) // 關閉該連接
    }
}

所謂優先關閉,是指在諸多 TCP 連接中找出最近未被使用的那個。這裏“未被使用”就是說,在最近一段時間內,沒有任何 Request 經由這個連接被髮送到 Processor 線程。

總結一下:

  • 接收分發請求主要由SocketServer 組件下的 Acceptor 和 Processor 線程處理。
  • SocketServer 實現了 Reactor 模式,用於高性能地併發處理 I/O 請求。
  • SocketServer 底層使用了 Java 的 Selector 實現 NIO 通信。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章