系列文章目錄
https://zhuanlan.zhihu.com/p/367683572
一. 總體結構
先給一張概覽圖:
服務端請求處理過程涉及到兩個模組:kafka.network和kafka.server。
1.1 kafka.network
該包是kafka底層模組,提供了服務端NIO通訊能力基礎。
有4個核心類:SocketServer、Acceptor、Processor、RequestChannel。各自角色如下:
-
SocketServer:服務端的抽象,是服務端通訊的入口;
-
Acceptor:Reactor通訊模式中處理連線ACCEPT事件的執行緒/執行緒池所執行的任務;
-
Processor:Reactor通訊模式中處理連線可讀/可寫事件的執行緒/執行緒池所執行的任務;
-
RequestChannel:請求佇列,儲存已經解析好的請求以等待處理;
對於上層模組而言,該基礎模組有兩個輸入和一個輸出
-
輸入:IP+埠號,該模組會對目標埠實現監聽;
-
輸出:解析好的請求,透過RequestChannel進行輸出;
-
輸入:待傳送的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版本的請求體和響應體的結構如下:
四. 請求的處理流程
-
Acceptor監聽到ACCEPT事件(TCP建立連線"第一次握手"的SYN);
-
Acceptor將將連線註冊到Processor列表內的其中一個,由該Processor監聽這個連線的後續可讀可寫事件;
-
Processor接收到完整請求後,會將Request追加到RequestChannel中進行排隊,等待後續處理;
-
KafkaServer中有個requestHandlerPool的欄位,KafkaRequestHandlerPool型別,代表請求處理執行緒池;KafkaRequestHandler就是其中的執行緒,會從RequestChannel拉請求進行處理;
-
KafkaRequestHandler將拉到的Request傳入KafkaApis.handle(Request)方法進行處理;
-
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
}
}
-
Processor從自己的ResponseQueue中拉取待傳送的Respnose;
-
Processor將Response發給客戶端;
五. 總結
才疏學淺,未能窺其十之一二,隨時歡迎各位交流補充。若文章質量還算及格,可以點贊收藏加以鼓勵,後續我繼續更新。
知乎主頁:https://www.zhihu.com/people/hao_zhihu
關注收藏不迷路,第一時間接收技術文章推送
微信公眾號: