Kafka原始碼分析(四) - Server端-請求處理框架

村口老张头發表於2024-05-06

系列文章目錄

https://zhuanlan.zhihu.com/p/367683572

一. 總體結構

先給一張概覽圖:

服務端請求處理過程涉及到兩個模組:kafka.networkkafka.server

1.1 kafka.network

該包是kafka底層模組,提供了服務端NIO通訊能力基礎。

有4個核心類:SocketServer、Acceptor、Processor、RequestChannel。各自角色如下:

  • SocketServer:服務端的抽象,是服務端通訊的入口;

  • Acceptor:Reactor通訊模式中處理連線ACCEPT事件的執行緒/執行緒池所執行的任務;

  • Processor:Reactor通訊模式中處理連線可讀/可寫事件的執行緒/執行緒池所執行的任務;

  • RequestChannel:請求佇列,儲存已經解析好的請求以等待處理;

對於上層模組而言,該基礎模組有兩個輸入和一個輸出

  1. 輸入:IP+埠號,該模組會對目標埠實現監聽;

  2. 輸出:解析好的請求,透過RequestChannel進行輸出;

  3. 輸入:待傳送的Response,透過Processor.responseQueue來完成輸入;

1.2 kafka.server

該包在kafka.network的基礎上實現各種請求的處理邏輯,主要包含KafkaServer和KafkaApis兩個類。其中:

  • KafkaServer:Kafka服務端的抽象,統一維護Kafka服務端的各流程和狀態;

  • KakfaApis:維護了各類請求對應的業務邏輯,透過KafkaServer.apis欄位組合到KafkaServer之中;

二. Server的埠監聽

整體流程如圖:

接下來按呼叫順序依次分析各方法

2.1 KafkaServer.startup()

關於埠監聽的核心邏輯分4步,程式碼如下(用註釋說明各部分的目的):

def startup() {
      // 省略無關程式碼
      ... ...

      // 1. 建立SocketServer
      socketServer = new SocketServer(config, metrics, time, credentialProvider)

      // 2. 啟動埠監聽
      // (在這裡完成了Acceptor的建立和埠ACCEPT事件的監聽)
      // (startupProcessors = false表示暫不啟動Processor處理執行緒)
      socketServer.startup(startupProcessors = false)

      // 3. 啟動請求處理過程中的相關依賴
      // (這也是第2步中不啟動Processor處理執行緒的原因,有依賴項需要處理)
      ... ...

      // 4. 啟動埠可讀/可寫事件處理執行緒(即Processor執行緒)
      socketServer.startProcessors()

      // 省略無關程式碼
      ... ...
}

2.2 SocketServer.startup(Boolean)

程式碼及說明性註釋如下:

def startup(startupProcessors: Boolean = true) {
    this.synchronized {
      // 省略無關程式碼
      ... ...

      // 1. 建立Accetpor和Processor的例項,
      // 同時頁完成了Acceptor對埠ACCEPT事件的監聽
      createAcceptorAndProcessors(config.numNetworkThreads, config.listeners)

      // 2. [可選]啟動各Acceptor對應的Processor執行緒
      if (startupProcessors) {
        startProcessors()
      }
    }
}

2.3 ScocketServer.createAcceptorAndProcessor()

直接上註釋版的程式碼,流程分3步:

// 入參解釋
// processorsPerListener: 對於每個IP:Port, 指定Reactor模式子執行緒池大小, 
//                        即處理埠可讀/可寫事件的執行緒數(Processor執行緒);
// endpoints: 接收請求的IP:Port列表;
def createAcceptorAndProcessors(processorsPerListener: Int,
                                endpoints: Seq[EndPoint]): Unit = synchronized {
    // 省略無關程式碼
    ... ...

    endpoints.foreach { endpoint =>
      // 省略無關程式碼
      ... ...

      // 1. 建立Acceptor物件
      // 在此步驟中呼叫Acceptor.openServerSocket, 完成了對埠ACCEPT事件的監聽
      val acceptor = new Acceptor(endpoint, sendBufferSize, recvBufferSize, brokerId, connectionQuotas)

      // 2. 建立了與acceptor對應的Processor物件列表
      // (這裡並未真正啟動Processor執行緒)
      addProcessors(acceptor, endpoint, processorsPerListener)

      // 3. 啟動Acceptor執行緒
      KafkaThread.nonDaemon(s"kafka-socket-acceptor-$listenerName-$securityProtocol-${endpoint.port}", acceptor).start()

      // 省略無關程式碼
      ... ...
    }
  }

2.4 Acceptor.openServerSocket()

該方法中沒什麼特殊點,就是java NIO的標準流程:

def openServerSocket(host: String, port: Int): ServerSocketChannel = {
    // 1. 構建InetSocketAddress物件
    val socketAddress =
      if (host == null || host.trim.isEmpty)
        new InetSocketAddress(port)
      else
        new InetSocketAddress(host, port)

    // 2. 構建ServerSocketChannel物件, 並設定必要引數值
    val serverChannel = ServerSocketChannel.open()
    serverChannel.configureBlocking(false)
    if (recvBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE)
      serverChannel.socket().setReceiveBufferSize(recvBufferSize)

    // 3. 埠繫結, 實現事件監聽
    try {
      serverChannel.socket.bind(socketAddress)
      info("Awaiting socket connections on %s:%d.".format(socketAddress.getHostString, serverChannel.socket.getLocalPort))
    } catch {
      case e: SocketException =>
        throw new KafkaException("Socket server failed to bind to %s:%d: %s.".format(socketAddress.getHostString, port, e.getMessage), e)
    }

    // 4. 返回ServerSocketChannel物件, 用於後續register到Selector中
    serverChannel
  }

2.5 SocketServer.startProcessor()

從這步開始,僅剩的工作就是啟動Processor執行緒,程式碼都非常簡單。比如本方法只是遍歷Acceptor列表,並呼叫Acceptor.startProcessors()

def startProcessors(): Unit = synchronized {
  acceptors.values.asScala.foreach { _.startProcessors() }
  info(s"Started processors for ${acceptors.size} acceptors")
}

2.6 Acceptor.startProcessors()

該方法很簡明,直接上程式碼

def startProcessors(): Unit = synchronized {
  if (!processorsStarted.getAndSet(true)) {
    startProcessors(processors)
  }
}

def startProcessors(processors: Seq[Processor]): Unit = synchronized {
  processors.foreach { processor =>
    KafkaThread.nonDaemon(s"kafka-network-thread-$brokerId-${endPoint.listenerName}-${endPoint.securityProtocol}-${processor.id}",
      processor).start()
  }
}

三. 請求/響應的格式

3.1 格式概述

請求和響應都由兩部分組成:Header和Body。RequestHeader中包含ApiKey、ApiVersion、CorrelationId、ClientId;ResponseHeader中只包含CorrelationId欄位。接下來逐個講解這些欄位。

  • ApiKey

    2位元組整型,指明請求的型別;比如0代表Produce請求,1代表Fetch請求;具體id和請求型別之間的對映關係可在 org.apache.kafka.common.protocol.ApiKeys 中找到;

  • ApiVersion

    隨著API的升級迭代,各型別請求的請求體格式可能有變更;這個2位元組的整型指明瞭請求體結構的版本;

  • CorrelationId

    4位元組整型,在Response中傳回,Kafka Server端不處理,用於客戶端內部關聯業務資料;

  • ClientId

    可變長字串,標識客戶端;

3.2 請求體/響應體的具體格式

各業務操作(比如Produce、Fetch等)對應的請求體和響應體格式都維護在 org.apache.kafka.common.protocol.ApiKeys 中。接下來以Produce為例講解ApiKeys是如何表達資料格式的。

ApiKeys是個列舉類,其核心屬性如下:

public enum ApiKeys {
  // 省略部分程式碼
  ... ...

  // 上文提到的請求型別對應的id
  public final short id;

  // 業務操作名稱
  public final String name;

  // 各版本請求體格式
  public final Schema[] requestSchemas;

  // 各版本響應體格式
  public final Schema[] responseSchemas;

  // 省略部分程式碼
  ... ...
}

其中PRODUCE列舉項的定義如下

PRODUCE(0, "Produce", ProduceRequest.schemaVersions(), ProduceResponse.schemaVersions())

可以看到各版本的請求格式維護在 ProduceRequest.schemaVersions(),程式碼如下

public static Schema[] schemaVersions() {
  return new Schema[] {PRODUCE_REQUEST_V0, PRODUCE_REQUEST_V1, PRODUCE_REQUEST_V2, PRODUCE_REQUEST_V3,
    PRODUCE_REQUEST_V4, PRODUCE_REQUEST_V5, PRODUCE_REQUEST_V6};
}

這裡只是簡單返回了一個Schema陣列。一個Schema物件代表了一種資料格式。請求頭中的ApiVersion指明瞭請求體的格式對應陣列的第幾項(從0開始)。

接下來我們看看Schema是如何表達資料格式的。其結構如下

Schema有兩個欄位:fields和fieldsByName。其中fields是體現資料格式的關鍵,它指明瞭欄位的排序和各欄位型別;而fieldsByName只是按欄位名重新組織的Map,用於根據名稱查詢對應欄位。

BoundField只是Field的簡單封裝。Field有兩個核心欄位:name和type。其中name表示欄位名稱,type表示欄位型別。常見的Type如下:

Type.BOOLEAN;
Type.INT8;
Type.INT16;
Type.INT32;

// 可透過org.apache.kafka.common.protocol.types.Type檢視全部型別
... ...

回到PRODUCE API,透過檢視Schema的定義,能看到其V0版本的請求體和響應體的結構如下:

四. 請求的處理流程

  1. Acceptor監聽到ACCEPT事件(TCP建立連線"第一次握手"的SYN);

  2. Acceptor將將連線註冊到Processor列表內的其中一個,由該Processor監聽這個連線的後續可讀可寫事件;

  3. Processor接收到完整請求後,會將Request追加到RequestChannel中進行排隊,等待後續處理;

  4. KafkaServer中有個requestHandlerPool的欄位,KafkaRequestHandlerPool型別,代表請求處理執行緒池;KafkaRequestHandler就是其中的執行緒,會從RequestChannel拉請求進行處理;

  5. KafkaRequestHandler將拉到的Request傳入KafkaApis.handle(Request)方法進行處理;

  6. KafkaApis根據不同的ApiKey呼叫不同的方法進行處理,處理完畢後會將Response最終寫入對應的Processor的ResponseQueue中等待傳送;KafkaApis.handle(Request)的方法結構如下:

def handle(request: RequestChannel.Request) {
  try {
    // 省略部分程式碼
    ... ...
    request.header.apiKey match {
      case ApiKeys.PRODUCE => handleProduceRequest(request)
      case ApiKeys.FETCH => handleFetchRequest(request)
      case ApiKeys.LIST_OFFSETS => handleListOffsetRequest(request)
      case ApiKeys.METADATA => handleTopicMetadataRequest(request)
      case ApiKeys.LEADER_AND_ISR => handleLeaderAndIsrRequest(request)
      // 省略部分程式碼
      ... ...
    }
  } catch {
    case e: FatalExitError => throw e
    case e: Throwable => handleError(request, e)
  } finally {
    request.apiLocalCompleteTimeNanos = time.nanoseconds
  }
}
  1. Processor從自己的ResponseQueue中拉取待傳送的Respnose;

  2. Processor將Response發給客戶端;

五. 總結

才疏學淺,未能窺其十之一二,隨時歡迎各位交流補充。若文章質量還算及格,可以點贊收藏加以鼓勵,後續我繼續更新。


知乎主頁:https://www.zhihu.com/people/hao_zhihu
關注收藏不迷路,第一時間接收技術文章推送


微信公眾號:
微信二維碼

相關文章