承接上一篇(https://blog.csdn.net/fenglei0415/article/details/106162288)
二. 請求的接收以及分發
主要分析兩個類,實現網絡通信的關鍵部件。分別是Acceptor 類和Processor 類。
先介紹下SocketServer組件下的類:
- AbstractServerThread 類:這是 Acceptor 線程和 Processor 線程的抽象基類,定義了這兩個線程的公有方法,如 shutdown(關閉線程)等。
- Acceptor 線程類:這是接收和創建外部 TCP 連接的線程。每個 SocketServer 實例只會創建一個 Acceptor 線程。它的唯一目的就是創建連接,並將接收到的 Request 傳遞給下游的 Processor 線程處理。
- Processor 線程類:這是處理單個 TCP 連接上所有請求的線程。每個 SocketServer 實例默認創建 num.network.threads 個Processor 線程。Processor 線程負責將接收到的 Request 添加到 RequestChannel 的 Request 隊列上,同時還負責將 Response 返還給 Request 發送方。
- Processor 伴生對象類:僅僅定義了一些與 Processor 線程相關的常見監控指標和常量等,如 Processor 線程空閒率等。
- ConnectionQuotas 類:是控制連接數配額的類。我們能夠設置單個 IP 創建 Broker 連接的最大數量,以及單個 Broker 能夠允許的最大連接數。
- TooManyConnectionsException 類:SocketServer 定義的一個異常類,用於標識連接數配額超限情況。
- SocketServer 類:實現了對以上所有組件的管理和操作,如創建和關閉 Acceptor、Processor 線程等。
- 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 通信。