用Reactor模式構建的Kafka Server網絡層和API層---架構和設計

1.前言

從Yarn RPC Server到Kafka Server,凡是有高併發需求的服務端,無一例外採用了基於了Reactor設計模式。在我的博客《Hadoop RPC Server基於Reactor模式和Java NIO 的架構和原理》中,分析了Yarn的基於Reactor設計模式和Java NIO實現的RPC Server的架構和設計,而Kafka的Server端網絡層也同樣使用了Reactor設計模式。Reactor模式有以下顯著特定:
1. 通過事件派發、分離IO操作,來提高系統的併發特性
2. 提供粗粒度的併發控制,使用單線程實現,避免了複雜的併發處理

下文中,我們就從Kafka Server端網絡層代碼入手,分析和講解Kafka基於Ractor模式的具體網絡層實現以及網絡層和具體的業務層的銜接邏輯。在我的另外一篇博客《基於Java NIO的Kafka底層網絡層源碼和架構》中,已經詳細講解了Kafka使用Java NIO實現的底層網絡架構,本文講解Reactor設計模式,將忽略具體的網絡層,而將注意力放在NIO的上層,即網絡事件的派發、請求和響應的處理上面。

同樣,事先說明,Kafka自己對NIO的Selector進行了封裝,放在了org.apache.kafka.common.network.Selector,爲了與java原生的java.nio.channels.Selector區分,我參照《Apache Kafka源碼剖析》一書的叫法,將org.apache.kafka.common.network.Selector叫做KSelector。Kafka的客戶端與服務器端的通信以及服務器之間的通信,底層都是用KSelector進行。

爲了便於在代碼層理解,我畫了整個KafkaServer從網絡層消息到業務處理,以及相反,業務處理結果通過網絡層返回的基於Reactor模式的處理流如下:

從網絡層接收請求到交付給業務處理流程示意圖:
這裏寫圖片描述

從業務處理層返回結果,到將結果通過網絡層返回給客戶端的流程圖:
這裏寫圖片描述


2. SocketServer初始化和啓動

Kafka的網絡層入口類是SocketServer。
我們知道,kafka.Kafka是Kafka Broker的入口類,kafka.Kafka.main()是Kafka Server的main()方法,即Kafka Broker的啓動入口。我們跟蹤代碼,即沿着方法調用棧kafka.Kafka.main() -> KafkaServerStartable() -> KafkaServer().startup可以從main()方法入口一直跟蹤到SocketServer即網絡層對象的創建,這意味着Kafka Server啓動的時候會初始化並啓動SocketServer,即我們所熟知的打開9092端口,這樣,SocketServer啓動完畢以後,就可以通過9092端口監聽、解析、處理所有請求。

SocketServer的核心任務,是完成9092端口的綁定,監聽並處理客戶端或者其它Kafka Broker的請求,將請求分派,然後將相應返回給發起請求的對應客戶端或者其它的Kafka Broker。核心角色,是同基本Reactor模式一致的負責接收請求的Acceptor角色、負責具體請求管理的Processor角色、負責請求和響應隊列的RequestChannel角色以及負責管理、限制整個網絡負載的ConnectionQuotas角色。

 private val endpoints = config.listeners
  private val numProcessorThreads = config.numNetworkThreads
  private val maxQueuedRequests = config.queuedMaxRequests
  private val totalProcessorThreads = numProcessorThreads * endpoints.size
  private val maxConnectionsPerIp = config.maxConnectionsPerIp
  private val maxConnectionsPerIpOverrides = config.maxConnectionsPerIpOverrides
  //創建RequestChannel,有totalProcessorThreads個responseQueue隊列,
  val requestChannel = new RequestChannel(totalProcessorThreads, maxQueuedRequests)
  //所有的processers,長度爲totalProcessorThreads
  private val processors = new Array[Processor](totalProcessorThreads)

def startup() {
    this.synchronized {

      connectionQuotas = new ConnectionQuotas(maxConnectionsPerIp, maxConnectionsPerIpOverrides)

      val sendBufferSize = config.socketSendBufferBytes
      val recvBufferSize = config.socketReceiveBufferBytes
      val brokerId = config.brokerId

      var processorBeginIndex = 0
      endpoints.values.foreach { endpoint =>//遍歷endPoint集合,對於每一個endpoint,創建一個acceptor和多個processor
        val protocol = endpoint.protocolType
        val processorEndIndex = processorBeginIndex + numProcessorThreads  //按照序號,創建process

        for (i <- processorBeginIndex until processorEndIndex)
          processors(i) = newProcessor(i, connectionQuotas, protocol)

        val acceptor = new Acceptor(endpoint, sendBufferSize, recvBufferSize, brokerId,
          processors.slice(processorBeginIndex, processorEndIndex), connectionQuotas)
        acceptors.put(endpoint, acceptor)//保存endr的point和accepto對應關係
        //起一個名字叫做"kafka-socket-acceptor-%s-%d".format(protocol.toString, endpoint.port)的acceptor線程,非deamon
        Utils.newThread("kafka-socket-acceptor-%s-%d".format(protocol.toString, endpoint.port), acceptor, false).start()
        acceptor.awaitStartup()//一直等到acceptor的run()方法的第一條語句開始執行,才證明已經啓動

        processorBeginIndex = processorEndIndex
      }
    }

SocketServer的啓動方法中,會遍歷本機的所有EndPoint(一個EndPoint一般對應一個網卡),爲每一個EndPoint創建一個唯一獨立的SocketServer.Acceptor對象,負責處理這個EndPoint上的所有請求。我們從代碼private val endpoints = config.listeners可以看到EndPoint是通過server.properties的listeners中進行配置的,用戶可以配置一個或者多個listener。

  • numProcessorThreads:通過num.network.threads進行配置,單個Acceptor所管理的Processor對象的數量;
  • maxQueuedRequests:通過queued.max.requests進行配置,請求隊列所允許的最大的未響應請求的數量,用來給ConnectionQuotas進行請求限額控制,避免Kafka Server產生過大的網絡負載;
  • totalProcessorThreads:計算方式爲numProcessorThreads * endpoints.size,即單臺機器總的Processor的數量;
  • maxConnectionsPerIp:配置項爲max.connections.per.ip,單個IP上的最大連接數,用來給ConnectionQuotas控制連接數;
  • maxConnectionsPerIpOverrides:針對每個IP進行的個性化限制,因爲每個IP的最大連接數限額可能會差異化,因此可以通過此項覆蓋maxConnectionsPerIp中的全局統一配置,這是在KAFKA-1512中增加的improvement;

同時,負責創建一個空的Processor對象的數組,用來放置所有的Processor線程對象,但是並未實際創建Processor對象。Processor是由Acceptor直接管理,因此也是由SocketServer.Acceptor負責創建的,這符合角色分層的原則。角色分層在Yarn的代碼中也體現得非常好,這樣做可以讓一個複雜的系統的每個功能模塊都變得清晰可控。每層角色只會負責自己還有自己直接管理的直接下層角色的初始化和啓動,絕對不會去觸碰不是自己所直接管理的角色。

根據Acceptor的數量,將這些線程對象平均分配給Acceptor。然後,一直等待Acceptor啓動完成,SocketServer.startup()會一直阻塞等待啓動完成纔會退出代表SocketServer完成啓動。阻塞方式我們在SocketServer.Acceptor的講解中會進行分析。總之,下層多個服務全部啓動完成,上層服務纔算啓動完成,這也是職責分層的原則。


3.SocketServer.Acceptor

每一個SocketServer.Acceptor負責唯一一個endpoint上的網絡請求和響應的管理。正常情況下,我們會給的Kafka Server配置唯一的一個Endpoint。但是有些服務器具有多個網卡驅動,因此可以配置多個EndPoint。舊版本的Kafka使用host.name來標識集羣中的主機身份,到了新版本的kakfa,則改爲使用listenersadvertised.listeners 進行配置,這主要用於複雜網絡環境下的Kafka配置,比如,我們的Kafka Server安裝在亞馬遜雲上,三臺服務器,每臺服務器都有一個內網IP和外網IP,Server之間的通信走內網IP以節省網絡流量,而外網用戶的訪問則只能使用公網IP,這時候我們就可以將內網IP的host配置在listeners中,而將外網IP配置在advertised.listeners 中。在下文中,我們默認一臺機器只有一個網卡的基本情況,以避免不必要的麻煩。

private[kafka] class Acceptor(val endPoint: EndPoint,
                              val sendBufferSize: Int,
                              val recvBufferSize: Int,
                              brokerId: Int,
                              processors: Array[Processor],
                              connectionQuotas: ConnectionQuotas) extends AbstractServerThread(connectionQuotas) with KafkaMetricsGroup {

  private val nioSelector = NSelector.open()
  val serverChannel = openServerSocket(endPoint.host, endPoint.port)//創建一個ServerSocketChannel,監聽endPoint.host, endPoint.port套接字

  //Acceptor被構造的時候就會啓動所有的processor線程
  this.synchronized {
    //每個processor創建一個單獨線程
    processors.foreach { processor =>
      Utils.newThread("kafka-network-thread-%d-%s-%d".format(brokerId, endPoint.protocolType.toString, processor.id), processor, false).start()
    }
  }

Acceptor的構造方法中,首先通過openServerSocket()打開自己負責的EndPoint的Socket,即打開端口並啓動監聽。
然後,Acceptor會負責構造自己管理的一個或者多個Processor對象。其實,每一個Processor都是一個獨立線程,下面在介紹Processor的時候會說到。

Acceptor使用CountDownLatch對象來標記啓動或者關閉是否完成:

Acceptor完成啓動,會調用

  /**
   * Record that the thread startup is complete
   */
  protected def startupComplete() = {
    startupLatch.countDown() 
  }

方法將startupLatch置位爲0,通知上層SocketServer自己已經完成啓動。此時上層SocketServer正通過調用startupLatch.await一直阻塞等待當前acceptor完成初始化。當SocketServer管理的所有Acceptor均完成了啓動,SocketServer完成啓動。

  def run() {
    //向selector註冊channel,可以接收ACCEPT事件,只有非阻塞的serverChannel纔可以註冊給Selector
    serverChannel.register(nioSelector, SelectionKey.OP_ACCEPT)
    startupComplete() //啓動完成的標記,放在run()方法的第一行,說明當確認線程開始運行,則認爲啓動成功,啓動過以後就是運行了
    try {
      var currentProcessor = 0
      while (isRunning) {///無限循環,持續等待OP_ACCEPT事件發生
        try {
          val ready = nioSelector.select(500)
          if (ready > 0) {//已經有對應的Accept事件發生
            val keys = nioSelector.selectedKeys()//取出發了了對應事件的事件的key,即有連接事件發生
            val iter = keys.iterator()
            while (iter.hasNext && isRunning) {
              try {
                val key = iter.next
                iter.remove()//NIO的通用做法,取出一個有相關事件發生的channel以後,必須remove掉對應的SelectionKey,防止下次重複取出
                if (key.isAcceptable)//
                  accept(key, processors(currentProcessor))//通過round-robin的方式取出一個acceptor進行處理
                else
                  throw new IllegalStateException("Unrecognized key state for acceptor thread.")

                //round-robin序號增1,下一個連接會取出下一個processor
                currentProcessor = (currentProcessor + 1) % processors.length
              } catch {
                case e: Throwable => error("Error while accepting connection", e)
              }
            }
          }
        }
        catch { //略  }
      }
    } finally { //略 }
  }

Acceptor線程的run()方法,是不斷監聽對應ServerChannel上的連接請求,如果有新的連接請求,就選擇出一個Processor,用來處理這個請求,將這個新連接交付給Processor是在方法Acceptor.accept()中:

 def accept(key: SelectionKey, processor: Processor) {
    val serverSocketChannel = key.channel().asInstanceOf[ServerSocketChannel]//取出channel
    val socketChannel = serverSocketChannel.accept()//創建socketChannel,專門負責與這個客戶端的連接
    try {
      //socketChannel參數設置
      processor.accept(socketChannel)//將SocketChannel交給process進行處理
    } catch {
      //異常處理
    }
  }

//Processor.accept():
 /**
   * Queue up a new connection for reading
   */
  def accept(socketChannel: SocketChannel) {
    newConnections.add(socketChannel)
    wakeup()
  }

Processor將這個新的SocketChannel加入到自己維護的ConcurrentLinkedQueue[SocketChannel] newConnections中。
下面介紹Processor線程.

4.Processor

Processor負責不斷檢查Acceptor是否有交付給自己新的連接,如果有,就負責這個channel上消息的讀寫操作,即:監聽自己維護的所有channel上的讀請求,解析並交付給對應的業務處理邏輯;監聽新的寫操作,把服務端的響應數據通過SocketChannel準確地發送給客戶端。

注意,每一個Processor都維護了一個單獨的KSelector對象,這個KSelector只負責這個Processor上所有channel的監聽。這樣最大程度上保證了不同Processor線程之間的完全並行和業務隔離,儘管,在異步IO情況下,一個Selector負責成百上千個socketChannel的狀態監控也不會帶來效率問題。

  override def run() {
    startupComplete()//表示初始化流程已經結束,通過這個CountDownLatch代表初始化已經結束,這個Processor已經開始正常運行了
    while (isRunning) {
      try {
        // setup any new connections that have been queued up
        configureNewConnections()//爲已經接受的請求註冊OR_READ事件
        // register any new responses for writing
        processNewResponses()//處理響應隊列,這個響應隊列是Handler線程處理以後的結果,會交付給RequestChannel.responseQueue.同時調用unmute,開始接受請求
        poll()  //調用KSelector.poll(),進行真正的數據讀寫
        processCompletedReceives()//調用mute,停止接受新的請求
        processCompletedSends()
        processDisconnected()
      } catch {
        //異常處理 略
    }

    debug("Closing selector - processor " + id)
    swallowError(closeAll())
    shutdownComplete()
  }

Processor.run()方法中,Processor主要進行以下幾步操作:
1. 新連接的處理:通過configureNewConnections()來處理Acceptor交付給自己的新的連接請求,即:把在自己的Selector上註冊這個SocketChannel,並監聽SelectionKey.OP_READ操作:
2. 響應處理:Processor如何將響應交付給業務層進行處理的我在後面會講到,在這裏,Processor通過processNewResponses()方法,從RequestChannel.responseQueue中取出當前Processor的一個Response,通過對應的SocketChannel,發送給Client。注意,這裏的”發送”,其實並沒有真正發送,只是通過調用KafkaChannel.send()方法,將待發送數據交付給對應的Channel,真正的數據發送是調用KafkaChannel.write()方法,這是在下面第三步的poll()中進行的;關於RequestChannel的職責,下文中會詳細講解。
3. 響應處理完畢,開始進行真正的讀寫操作,即監控自己負責的所有SocketChannel,是否有新的讀請求進來,如果有,就進行相應的處理,主要是對請求進行解析並交付給業務邏輯進行具體調用,如果有待寫數據,就執行寫操作,真正將數據發送給遠程;如果有新的連接,則放到connected中,如果有斷開的連接,則放入到disconnected中:

  • List<String> connected:已經建立連接的KafkaChannel的id的list
  • List<String> disconnected:出現異常的KafkaChannel的id的list
  • List<Send> completedSends:已經完成的發送請求,服務端可以通過查詢completedSends,知道哪些請求已經發送出去
  • List<NetworkReceive> completedReceives:已經接收到的數據,服務端拿到這些已經接收到的數據以後,會將這些數據交付給具體數據的處理者進行處理

我們先來看Processor對於已經收到的請求,是如何交付給業務端進行處理的:


   * 將completedReceived中的對象進行封裝,交付給requestQueue.completRequets
   */
  private def processCompletedReceives() {
    selector.completedReceives.asScala.foreach { receive =>//每一個receive是一個NetworkReceivedui'xiagn
      try {
        //receive.source代表了這個請求的發送者的身份,KSelector保存了channel另一端的身份和對應的SocketChannel之間的對應關係
        val channel = selector.channel(receive.source)
        val session = RequestChannel.Session(new KafkaPrincipal(KafkaPrincipal.USER_TYPE, channel.principal.getName),
          channel.socketAddress)
        val req = RequestChannel.Request(processor = id, connectionId = receive.source, session = session, buffer = receive.payload, startTimeMs = time.milliseconds, securityProtocol = protocol)
        requestChannel.sendRequest(req)//將請求通過RequestChannel.requestQueue交付給Handler
        selector.mute(receive.source)//不再接受Read請求,發送響應之前,不可以再接收任何請求
      } catch {
        //異常處理 略
      }
    }
  }

Processor.processCompletedReceives()通過遍歷completedReceives,對於每一個已經完成接收的數據,對數據進行解析和封裝,交付給RequestChannel,RequestChannel會交付給具體的業務處理層進行處理。
從上面的分析可以知道,Processor從RequetsChannel中獲取業務層的處理結果然後將結果發送給客戶端,同時,收到遠程的某些請求,也是交付給RequetsChannel進行處理。可見,RequetsChannel負責了網絡層和業務層的數據交付。下文中,我們一起來看RequestChannel的工作機制。


5. RequestChannel

RequestChannel負責消息從網絡層轉接到業務層,以及將業務層的處理結果交付給網絡層進而返回給客戶端。每一個SocketServer只有一個RequestChannel對象,在SocketServer中構造。

  //創建RequestChannel,有totalProcessorThreads個responseQueue隊列,
  val requestChannel = new RequestChannel(totalProcessorThreads, maxQueuedRequests)

我們一起來看RequestChannel的構造方法:

class RequestChannel(val numProcessors: Int, val queueSize: Int) extends KafkaMetricsGroup {
  private var responseListeners: List[(Int) => Unit] = Nil
  //request存放了所有Processor接收到的遠程請求,負責把requestQueue中的請求交付給具體業務邏輯進行處理
  private val requestQueue = new ArrayBlockingQueue[RequestChannel.Request](queueSize)
  //responseQueues存放了所有Processor的帶出來的response,即每一個Processor都有一個response queue
  private val responseQueues = new Array[BlockingQueue[RequestChannel.Response]](numProcessors)
  for(i <- 0 until numProcessors) //初始化responseQueues
    responseQueues(i) = new LinkedBlockingQueue[RequestChannel.Response]()

  //一些metrics用來監控request和response的數量,代碼略
  }

RequestChannel構造方法中初始化了requestQueue,用來存放網絡層接收到的請求,這些請求即將交付給業務層進行處理。同時,初始化了responseQueues,爲每一個Processor建立了一個response隊列,用來存放這個Processor的一個或者多個Response,這些response即將交付給網絡層返回給客戶端。

6. KafkaRequestHandler線程和KafkaRequestHandlerPool線程池

在上面講到Processor.processCompletedReceives()方法時,可以看到Processor調用了RequestChannel.sendRequest()方法將請求交付給了RequestChannel。而從RequestChannel取出請求並交付業務層處理的邏輯,是在獨立線程中完成的,它是KafkaRequestHandler。並且,KafkaRequestHandler是由 KafkaRequestHandlerPool線程池進行管理的。與我們文章最開始提到的SocketServer創建相同,KafkaRequestHandlerPool也是在KafkaServer.startup()中創建的:

        /* start processing requests */
        apis = new KafkaApis(socketServer.requestChannel, replicaManager, groupCoordinator,
          kafkaController, zkUtils, config.brokerId, config, metadataCache, metrics, authorizer)
        //KafkaRequestHandlerPool線程池,用來管理所有KafkaRequestHandler線程
        requestHandlerPool = new KafkaRequestHandlerPool(config.brokerId, socketServer.requestChannel, apis, config.numIoThreads)

這裏,KafkaApis是Kafka的API接口層,可以理解爲一個工具類,職責就是解析請求然後獲取請求類型,根據請求類型將請求交付給對應的業務層,可見,KafkaApis是實現了網絡層到業務層的真正映射關係,下文會詳解。

class KafkaRequestHandlerPool(val brokerId: Int,
                              val requestChannel: RequestChannel,
                              val apis: KafkaApis,
                              numThreads: Int) extends Logging with KafkaMetricsGroup {

  /* a meter to track the average free capacity of the request handlers */
  private val aggregateIdleMeter = newMeter("RequestHandlerAvgIdlePercent", "percent", TimeUnit.NANOSECONDS)

  this.logIdent = "[Kafka Request Handler on Broker " + brokerId + "], "
  val threads = new Array[Thread](numThreads)
  //初始化由KafkaRequestHandler線程構成的線程數組
  val runnables = new Array[KafkaRequestHandler](numThreads)
  for(i <- 0 until numThreads) {
    runnables(i) = new KafkaRequestHandler(i, brokerId, aggregateIdleMeter, numThreads, requestChannel, apis)
    threads(i) = Utils.daemonThread("kafka-request-handler-" + i, runnables(i))
    threads(i).start()
  }

KafkaRequestHandlerPool構造方法中初始化並啓動了多個KafkaRequestHandler線程對象,線程池大小通過Kafka配置文件配置項num.io.threads進行配置。

KafkaRequestHandlerPool線程池中的所有KafkaRequestHandler,通過競爭方式從RequestChannel.requestQueue中獲取請求進行處理。由於requestQueue的類型是ArrayBlockingQueue,通過調用ArrayBlockingQueue.poll()方法取出請求。我們看ArrayBlockingQueue.poll()方法源碼,可以看到ArrayBlockingQueue.poll()方法線程安全,因此多個KafkaRequestHandler線程競爭requestQueue不會出現線程安全問題。
這是KafkaRequestHandler.run()方法,就是不斷從requestQueue中取出請求,調用API層業務處理邏輯進行處理。

 def run() {
    while(true) {
      try {
        var req : RequestChannel.Request = null
        while (req == null) {
        //略
        req = requestChannel.receiveRequest(300)//從RequestChannel.requestQueue中取出請求
        //略
        apis.handle(req)//調用KafkaApi.handle(),將請求交付給業務
      } catch {}
    }
  }

6.KafkaApis

KafkaApis類似一個工具類,解析用戶請求並將請求交付給業務層,我們可以把它看做Kafka的API層。從上面KafkaRequestHandler.run()方法可以看到,這是通過調用KafkaApis.handle()方法完成的。

 def handle(request: RequestChannel.Request) {
    try {
      ApiKeys.forId(request.requestId) match {
        case ApiKeys.PRODUCE => handleProducerRequest(request)
        case ApiKeys.FETCH => handleFetchRequest(request)
        case ApiKeys.LIST_OFFSETS => handleOffsetRequest(request)
        case ApiKeys.METADATA => handleTopicMetadataRequest(request)
        case ApiKeys.LEADER_AND_ISR => handleLeaderAndIsrRequest(request)
        //其它ApiKeys,略
      }
    } catch { //異常處理,略 }
    } finally{
      request.apiLocalCompleteTimeMs = SystemTime.milliseconds
      }
  }

通過Switch-Case代碼塊,根據請求中的requestId,將請求交付給handleProducerRequest()handleFetchRequest()等等handle*Request()方法。具體每個handle*Request()方法的處理流程不在本文描述範圍之內,但是都是在獲取返回結果以後,調用requestChannel.sendResponse()將response交付給RequestChannel。這裏只是交付給RequestChannel保存在RequestChannel.responseQueues中。
在上文講解Processor的時候說過,Procossor.processNewResponses()就是從requestChannel.responseQueues取出屬於自己的連接上的響應,準備返回給客戶端。


結束

這樣,我們通過對Acceptor、Processor、RequestChannel、KafkaRequestHandler以及KafkaApis多個角色的解析,完成了整個Kafka的消息流通閉環,即從客戶端建立連接、發送請求給Kafka Server進行處理、Kafka Server將請求交付給具體業務進行處理、業務將處理結果返回給網絡層、網絡層將結果通過NIO返回給客戶端的整個流程。同時,由於多Processor線程、以及KafkaRequestHandlerPoll線程池的存在,通過交付-獲取的方式而不是阻塞等待的方式,讓整個消息處理實現完全的異步化,各個角色各司其職,模塊之間無耦合,線程之間或者相互競爭任務,或者被上層安排處理部分任務,整個效率非常高,結構也相當清晰。Processor線程的數量、KafkaRequestHandlerPool線程池可配置,因此可以根據CPU以及內存性能,合理調整Kafka Server的並行程度和處理能力。對Kafka基於Reactor模式的網絡層的理解,以及消息從網絡層到業務層交付邏輯的理解,非常有利於我們對Kafka集羣的管理以及對Kafka問題原因的排查診斷,同時,也可以基於各個不同的角色暴露出來的一個java metrics,我們可以對Kafka進行有效的調整優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章