最近一直研究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 文件,是主要實現類。
```java
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 工程中,下面是它主要的邏輯代碼。我用註釋的方式解釋下主體代碼的含義。
```java
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 類中的大部分代碼都是與監控指標相關的。