kafka請求隊列模塊

最近一直研究kafka源碼,想着有必要記錄一下。不管研究是否到位,也算是一個里程碑吧。

當我們說到 Kafka 服務器端,也就是 Broker 的時候,往往會說它承擔着消息持久化的功能,但本質上,它其實就是一個不斷接收外部請求、處理請求,然後發送處理結果的 Java 進程 (因爲scala 代碼被編譯之後生成.class 文件,它和 Java 代碼被編譯後的效果是一樣的)。

高效地保存排隊中的請求,是確保 Broker 高處理性能的關鍵。既然這樣,Broker 上的請求隊列是怎麼實現的呢?接下來,就看下 Broker 底層請求對象的建模和請求隊列的實現原理,以及 Broker請求處理方面的核心監控指標。目前,Broker 與 Clients 進行交互主要是基於Request / Response 機制,所以,很有必要學習一下源碼是如何定義 Request 和 Response 的。

一. 請求(Request)

先看一下 RequestChannel 源碼中的 Request 定義代碼。源碼位於 core/src/main/scala/kafka/network ,RequestChannel.scala 文件,是主要實現類。

sealed trait BaseRequest
case object ShutdownRequest extends BaseRequest

class Request(val processor: Int,
              val context: RequestContext,
              val startTimeNanos: Long,
              memoryPool: MemoryPool,
              @volatile private var buffer: ByteBuffer,
              metrics: RequestChannel.Metrics) extends BaseRequest {
              .....
              }

 

BaseRequest 是一個 trait 接口,定義了基礎的請求類型。它有兩個實現類:ShutdownRequest 類和 Request 類。

ShutdownRequest 僅僅起到一個標誌位的作用。當 Broker 進程關閉時,請求處理器會發送 ShutdownRequest 到專屬的請求處理線程。該線程接收到此請求後,會主動觸發一系列的 Broker 關閉邏輯。Request 則是真正定義各類 Clients 端或 Broker 端請求的實現類。它定義的屬性包括 processor、context、startTimeNanos、memoryPool、buffer 和 metrics等等。

1.  processor

processor 是 Processor 線程的序號,即這個請求是由哪個 Processor 線程接收處理的。Broker 端參數 num.network.threads 控制了 Broker 每個監聽器上創建的 Processor 線程數。在默認情況下,Broker 啓動時會創建 3 個 Processor 線程,爲一組,分別給 listeners 參數中設置的監聽器使用,每組的序號分別是 0、1、2。

那爲什麼要保存 Processor 線程的序號呢?這是因爲,當 Request 被後面的 I/O 線程處理完成後,還要依靠 Processor 線程發送 Response 給請求發送方,因此,Request 中必須記錄它之前是被哪個 Processor 線程接收的。另外,這裏明確一點:Processor 線程僅僅是網絡接收線程,不會執行真正的 Request 請求處理邏輯,那是 I/O 線程負責的事情。

2.  context

context 是用來標識請求上下文信息的。Kafka 源碼中定義了 RequestContext 類,顧名思義,它保存了有關 Request 的所有上下文信息。RequestContext 類定義在 clients 工程中,下面是它主要的邏輯代碼。我用註釋的方式解釋下主體代碼的含義。

public class RequestContext implements AuthorizableRequestContext {
    // Request頭部數據,主要是一些對用戶不可見的元數據信息,如Request類型、Request API版本、clientId等
    public final RequestHeader header;
    // Request發送方的TCP連接串標識,由Kafka根據一定規則定義,主要用於表示TCP連接
    public final String connectionId;
    // Request發送方IP地址
    public final InetAddress clientAddress;
    // Kafka用戶認證類,用於認證授權
    public final KafkaPrincipal principal;
    // 監聽器名稱,可以是預定義的監聽器(如PLAINTEXT),也可自行定義
    public final ListenerName listenerName;
    // 安全協議類型,目前支持4種:PLAINTEXT、SSL、SASL_PLAINTEXT、SASL_SSL
    public final SecurityProtocol securityProtocol;
    // 用戶自定義的一些連接方信息
    public final ClientInformation clientInformation;
    // RequestContext的封裝
    public RequestContext(RequestHeader header,
                          String connectionId,
                          InetAddress clientAddress,
                          KafkaPrincipal principal,
                          ListenerName listenerName,
                          SecurityProtocol securityProtocol,
                          ClientInformation clientInformation) {
        this.header = header;
        this.connectionId = connectionId;
        this.clientAddress = clientAddress;
        this.principal = principal;
        this.listenerName = listenerName;
        this.securityProtocol = securityProtocol;
        this.clientInformation = clientInformation;
    }
    // 從給定的ByteBuffer中提取出Request和對應的Size值
    public RequestAndSize parseRequest(ByteBuffer buffer) {
        if (isUnsupportedApiVersionsRequest()) {
            // Unsupported ApiVersion requests are treated as v0 requests and are not parsed
            // 不支持的ApiVersions請求類型被視爲是V0版本的請求,並且不做解析操作,直接返回
            ApiVersionsRequest apiVersionsRequest = new ApiVersionsRequest(new ApiVersionsRequestData(), (short) 0, header.apiVersion());
            return new RequestAndSize(apiVersionsRequest, 0);
        } else {
            // 從請求頭部數據中獲取ApiKeys信息
            ApiKeys apiKey = header.apiKey();
            try {
                // 從請求頭部數據中獲取版本信息
                short apiVersion = header.apiVersion();
                // 解析請求
                Struct struct = apiKey.parseRequest(apiVersion, buffer);
                AbstractRequest body = AbstractRequest.parseRequest(apiKey, apiVersion, struct);
                // 封裝解析後的請求對象以及請求大小返回
                return new RequestAndSize(body, struct.sizeOf());
            } catch (Throwable ex) {
                // 解析過程中出現任何問題都視爲無效請求,拋出異常
                throw new InvalidRequestException("Error getting request for apiKey: " + apiKey +
                        ", apiVersion: " + header.apiVersion() +
                        ", connectionId: " + connectionId +
                        ", listenerName: " + listenerName +
                        ", principal: " + principal, ex);
            }
        }
    }

3. startTimeNanos

startTimeNanos 記錄了 Request 對象被創建的時間,主要用於各種時間統計指標的計算。

請求對象中的很多 JMX 指標,特別是時間類的統計指標,都需要使用 startTimeNanos 字段。它是以納秒爲單位的時間戳信息,可以實現非常細粒度的時間統計精度。

4. memoryPool

memoryPool 表示源碼定義的一個非阻塞式的內存緩衝區,主要作用是避免 Request 對象無限使用內存。當前,該內存緩衝區的接口類和實現類,分別是 MemoryPool 和 SimpleMemoryPool。

5. buffer

buffer 是真正保存 Request 對象內容的字節緩衝區。Request 發送方必須按照 Kafka RPC 協議規定的格式向該緩衝區寫入字節,否則將拋出 InvalidRequestException 異常。這個邏輯主要是由 RequestContext 的 parseRequest 方法實現的,看上圖。

6. metrics

metrics 是 Request 相關的各種監控指標的一個管理類。它裏面構建了一個 Map,封裝了所有的請求 JMX 指標。除了上面這些重要的字段屬性之外,Request 類中的大部分代碼都是與監控指標相關的。

二.  響應(Response)

Kafka 爲 Response 定義了 1 個抽象父類和 5 個具體子類。具體如下:

  • Response:定義 Response 的抽象基類。每個 Response 對象都包含了對應的 Request 對象。這個類裏最重要的方法是 onComplete 方法,用來實現每類 Response 被處理後需要執行的回調邏輯。
  • SendResponse:Kafka 大多數 Request 處理完成後都需要執行一段回調邏輯,SendResponse 就是保存返回結果的 Response 子類。裏面最重要的字段是 onCompletionCallback,即指定處理完成之後的回調邏輯
  • NoResponse:有些 Request 處理完成後無需單獨執行額外的回調邏輯。NoResponse 就是爲這類 Response 準備的。
  • CloseConnectionResponse:用於出錯後需要關閉 TCP 連接的場景,此時返回 CloseConnectionResponse 給 Request 發送方,顯式地通知它關閉連接。
  • StartThrottlingResponse:用於通知 Broker 的 Socket Server 組件(後面幾節課我會講到它)某個 TCP 連接通信通道開始被限流(throttling)。
  • EndThrottlingResponse:與 StartThrottlingResponse 對應,通知 Broker 的 SocketServer 組件某個 TCP 連接通信通道的限流已結束。

接下來看下Response的代碼:

abstract class Response(val request: Request) {
    locally {
      val nowNs = Time.SYSTEM.nanoseconds
      request.responseCompleteTimeNanos = nowNs
      if (request.apiLocalCompleteTimeNanos == -1L)
        request.apiLocalCompleteTimeNanos = nowNs
    }

    def processor: Int = request.processor

    def responseString: Option[String] = Some("")

    def onComplete: Option[Send => Unit] = None

    override def toString: String
  }

這個抽象基類只有一個屬性字段:request。這就是說,每個 Response 對象都要保存它對應的 Request 對象。上面說過,onComplete 方法是調用指定回調邏輯的地方。SendResponse 類就是複寫(Override)了這個方法,如下所示:


class SendResponse(request: Request,
                     val responseSend: Send,
                     val responseAsString: Option[String],
                     val onCompleteCallback: Option[Send => Unit]) 
  extends Response(request) {
    ......
    override def onComplete: Option[Send => Unit] = onCompleteCallback
}

這裏的 SendResponse 類繼承了 Response 父類,並重新定義了 onComplete 方法。複寫的邏輯很簡單,就是指定輸入參數 onCompleteCallback。

三.  RequestChannel(通道)

RequestChannel,顧名思義,就是傳輸 Request/Response 的通道。

class RequestChannel(val queueSize: Int, val metricNamePrefix : String) extends KafkaMetricsGroup {
  import RequestChannel._
  val metrics = new RequestChannel.Metrics
  // ArrayBlockingQueue 是java中阻塞隊列,來保存Broker接收到的各類請求
  private val requestQueue = new ArrayBlockingQueue[BaseRequest](queueSize)
  // 字段 processors 封裝的是 RequestChannel 下轄的 Processor 線程池。
  // 每個 Processor 線程負責具體的請求處理邏輯
  private val processors = new ConcurrentHashMap[Int, Processor]()
  val requestQueueSizeMetricName = metricNamePrefix.concat(RequestQueueSizeMetric)
  val responseQueueSizeMetricName = metricNamePrefix.concat(ResponseQueueSizeMetric)
  ......
}

就 RequestChannel 類本身的主體功能而言,它定義了最核心的 3 個屬性:requestQueue、queueSize 和 processors。

  • 每個 RequestChannel 對象實例創建時,會定義一個隊列來保存 Broker 接收到的各類請求,這個隊列被稱爲請求隊列或 Request 隊列。Kafka 使用 Java 提供的阻塞隊列 ArrayBlockingQueue 實現這個請求隊列,並利用它天然提供的線程安全性來保證多個線程能夠併發安全高效地訪問請求隊列。在代碼中,這個隊列由變量requestQueue定義。
  • 字段 queueSize 就是 Request 隊列的最大長度。當 Broker 啓動時,SocketServer 組件會創建 RequestChannel 對象,並把 Broker 端參數 queued.max.requests 賦值給 queueSize。因此,在默認情況下,每個 RequestChannel 上的隊列長度是 500。
  • 字段 processors 封裝的是 RequestChannel 下轄的 Processor 線程池。每個 Processor 線程負責具體的請求處理邏輯。

說下有關processor的管理

Processor 管理

Processor 線程池——它是用 Java 的 ConcurrentHashMap 數據結構去保存的。Map 中的 Key 就是前面我們說的 processor 序號,而 Value 則對應具體的 Processor 線程對象。

這個線程池的存在告訴了我們一個事實:當前 Kafka Broker 端所有網絡線程都是在 RequestChannel 中維護的。既然創建了線程池,代碼中必然要有管理線程池的操作。RequestChannel 中的 addProcessor 和 removeProcessor 方法就是做這些事的。


def addProcessor(processor: Processor): Unit = {
  // 添加Processor到Processor線程池  
  if (processors.putIfAbsent(processor.id, processor) != null)
    warn(s"Unexpected processor with processorId ${processor.id}")
    newGauge(responseQueueSizeMetricName, 
      () => processor.responseQueueSize,
      // 爲給定Processor對象創建對應的監控指標
      Map(ProcessorMetricTag -> processor.id.toString))
}

def removeProcessor(processorId: Int): Unit = {
  processors.remove(processorId) // 從Processor線程池中移除給定Processor線程
  removeMetric(responseQueueSizeMetricName, Map(ProcessorMetricTag -> processorId.toString)) // 移除對應Processor的監控指標
}

代碼很簡單,基本上就是調用 ConcurrentHashMap 的 putIfAbsent 和 remove 方法分別實現增加和移除線程。每當 Broker 啓動時,它都會調用 addProcessor 方法,向 RequestChannel 對象添加 num.network.threads 個 Processor 線程

如果查詢 Kafka 官方文檔的話,你就會發現,num.network.threads 這個參數的更新模式(Update Mode)是 Cluster-wide。這就說明,Kafka 允許你動態地修改此參數值。比如,Broker 啓動時指定 num.network.threads 爲 8,之後你通過 kafka-configs 命令將其修改爲 3。顯然,這個操作會減少 Processor 線程池中的線程數量。在這個場景下,removeProcessor 方法會被調用。

處理 Request 和 Response

除了 Processor 的管理之外,RequestChannel 的另一個重要功能,是處理 Request 和 Response,具體表現爲收發 Request 和發送 Response。

1. 比如,收發 Request 的方法有 sendRequest 和 receiveRequest:


def sendRequest(request: RequestChannel.Request): Unit = {
    requestQueue.put(request)
}
def receiveRequest(timeout: Long): RequestChannel.BaseRequest =
    requestQueue.poll(timeout, TimeUnit.MILLISECONDS)
def receiveRequest(): RequestChannel.BaseRequest =
    requestQueue.take()

所謂的 sendRequest,僅僅是將 Request 對象放置在 Request 隊列中而已,而接收 Request 則是從隊列中取出 Request。整個流程構成了一個迷你版的“生產者 - 消費者”模式,然後依靠 ArrayBlockingQueue 的線程安全性來確保整個過程的線程安全。

2. 對於 Response 而言,則沒有所謂的接收 Response,只有發送 Response,即 sendResponse 方法。sendResponse 是啥意思呢?其實就是把 Response 對象發送出去,也就是將 Response 添加到 Response 隊列的過程。


def sendResponse(response: RequestChannel.Response): Unit = {
    if (isTraceEnabled) {  // 構造Trace日誌輸出字符串
      val requestHeader = response.request.header
      val message = response match {
        case sendResponse: SendResponse =>
          s"Sending ${requestHeader.apiKey} response to client ${requestHeader.clientId} of ${sendResponse.responseSend.size} bytes."
        case _: NoOpResponse =>
          s"Not sending ${requestHeader.apiKey} response to client ${requestHeader.clientId} as it's not required."
        case _: CloseConnectionResponse =>
          s"Closing connection for client ${requestHeader.clientId} due to error during ${requestHeader.apiKey}."
        case _: StartThrottlingResponse =>
          s"Notifying channel throttling has started for client ${requestHeader.clientId} for ${requestHeader.apiKey}"
        case _: EndThrottlingResponse =>
          s"Notifying channel throttling has ended for client ${requestHeader.clientId} for ${requestHeader.apiKey}"
      }
      trace(message)
    }
    // 找出response對應的Processor線程,即request當初是由哪個Processor線程處理的
    val processor = processors.get(response.processor)
    // 將response對象放置到對應Processor線程的Response隊列中
    if (processor != null) {
      processor.enqueueResponse(response)
    }
}

前面的一大段 if 代碼塊僅僅是構造 Trace 日誌要輸出的內容。根據不同類型的 Response,代碼需要確定要輸出的 Trace 日誌內容。接着,代碼會找出 Response 對象對應的 Processor 線程。當 Processor 處理完某個 Request 後,會把自己的序號封裝進對應的 Response 對象。一旦找出了之前是由哪個 Processor 線程處理的,代碼直接調用該 Processor 的 enqueueResponse 方法,將 Response 放入 Response 隊列中,等待後續發送。

監控指標實現

RequestChannel 類還定義了豐富的監控指標,用於實時動態地監測 Request 和 Response 的性能表現。

object RequestMetrics {
  val consumerFetchMetricName = ApiKeys.FETCH.name + "Consumer"
  val followFetchMetricName = ApiKeys.FETCH.name + "Follower"
  val RequestsPerSec = "RequestsPerSec"
  val RequestQueueTimeMs = "RequestQueueTimeMs"
  val LocalTimeMs = "LocalTimeMs"
  val RemoteTimeMs = "RemoteTimeMs"
  val ThrottleTimeMs = "ThrottleTimeMs"
  val ResponseQueueTimeMs = "ResponseQueueTimeMs"
  val ResponseSendTimeMs = "ResponseSendTimeMs"
  val TotalTimeMs = "TotalTimeMs"
  val RequestBytes = "RequestBytes"
  val MessageConversionsTimeMs = "MessageConversionsTimeMs"
  val TemporaryMemoryBytes = "TemporaryMemoryBytes"
  val ErrorsPerSec = "ErrorsPerSec"
}
  • RequestsPerSec:每秒處理的 Request 數,用來評估 Broker 的繁忙狀態。
  • RequestQueueTimeMs:計算 Request 在 Request 隊列中的平均等候時間,單位是毫秒。倘若 Request 在隊列的等待時間過長,你通常需要增加後端 I/O 線程的數量,來加快隊列中 Request 的拿取速度。
  • LocalTimeMs:計算 Request 實際被處理的時間,單位是毫秒。一旦定位到這個監控項的值很大,你就需要進一步研究 Request 被處理的邏輯了,具體分析到底是哪一步消耗了過多的時間。
  • RemoteTimeMs:Kafka 的讀寫請求(produce 請求和 fetch 請求)邏輯涉及等待其他 Broker 操作的步驟。RemoteTimeMs 計算的,就是等待其他 Broker 完成指定邏輯的時間。因爲等待的是其他 Broker,因此被稱爲 Remote Time。這個監控項非常重要!Kafka 生產環境中設置 acks=all 的 Producer 程序發送消息延時高的主要原因,往往就是 Remote Time 高。因此,如果你也碰到了這樣的問題,不妨先定位一下 Remote Time 是不是瓶頸。
  • TotalTimeMs:計算 Request 被處理的完整流程時間。這是最實用的監控指標,沒有之一!畢竟,我們通常都是根據 TotalTimeMs 來判斷系統是否出現問題的。一旦發現了問題,我們纔會利用前面的幾個監控項進一步定位問題的原因。

最後,總結一下RequestChannel請求類:

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