Kafka | Java 消費者是如何管理TCP連接的? | 極客時間

今天我要和你分享的主題是:Kafka 的 Java 消費者是如何管理 TCP 連接的。

在專欄中,我們專門聊過“Java生產者是如何管理 TCP 連接資源的”這個話題,你應該還有印象吧?今天算是它的姊妹篇,我們一起來研究下 Kafka 的 Java消費者管理 TCP 或 Socket 資源的機制。只有完成了今天的討論,我們纔算是對 Kafka 客戶端的 TCP 連接管理機制有了全面的瞭解。

和之前一樣,我今天會無差別地混用 TCP 和 Socket 兩個術語。畢竟,在 Kafka 的世界中,無論是 ServerSocket,還是 SocketChannel,它們實現的都是 TCP 協議。或者這麼說,Kafka 的網絡傳輸是基於 TCP 協議的,而不是基於 UDP 協議,因此,當我今天說到 TCP 連接或 Socket 資源時,我指的是同一個東西。

何時創建 TCP 連接?

我們先從消費者創建 TCP 連接開始討論。消費者端主要的程序入口是 KafkaConsumer 類。和生產者不同的是,構建 KafkaConsumer 實例時是不會創建任何 TCP 連接的,也就是說,當你執行完 new KafkaConsumer(properties) 語句後,你會發現,沒有 Socket 連接被創建出來。這一點和 Java 生產者是有區別的,主要原因就是生產者入口類 KafkaProducer 在構建實例的時候,會在後臺默默地啓動一個 Sender 線程,這個 Sender 線程負責 Socket 連接的創建。

從這一點上來看,我個人認爲 KafkaConsumer 的設計比 KafkaProducer 要好。就像我在第 13 講中所說的,在 Java 構造函數中啓動線程,會造成 this 指針的逃逸,這始終是一個隱患。

如果 Socket 不是在構造函數中創建的,那麼是在 KafkaConsumer.subscribe 或 KafkaConsumer.assign 方法中創建的嗎?嚴格來說也不是。我還是直接給出答案吧:TCP 連接是在調用 KafkaConsumer.poll 方法時被創建的。再細粒度地說,在 poll 方法內部有 3 個時機可以創建 TCP 連接。

1.發起 FindCoordinator 請求時

還記得消費者端有個組件叫協調者(Coordinator)嗎?它駐留在 Broker 端的內存中,負責消費者組的組成員管理和各個消費者的位移提交管理。當消費者程序首次啓動調用 poll 方法時,它需要向 Kafka 集羣發送一個名爲 FindCoordinator 的請求,希望 Kafka 集羣告訴它哪個 Broker 是管理它的協調者。

不過,消費者應該向哪個 Broker 發送這類請求呢?理論上任何一個 Broker 都能回答這個問題,也就是說消費者可以發送 FindCoordinator 請求給集羣中的任意服務器。在這個問題上,社區做了一點點優化:消費者程序會向集羣中當前負載最小的那臺 Broker 發送請求。負載是如何評估的呢?其實很簡單,就是看消費者連接的所有 Broker 中,誰的待發送請求最少。當然了,這種評估顯然是消費者端的單向評估,並非是站在全局角度,因此有的時候也不一定是最優解。不過這不併影響我們的討論。總之,在這一步,消費者會創建一個 Socket 連接。

2.連接協調者時。

Broker 處理完上一步發送的 FindCoordinator 請求之後,會返還對應的響應結果(Response),顯式地告訴消費者哪個 Broker 是真正的協調者,因此在這一步,消費者知曉了真正的協調者後,會創建連向該 Broker 的 Socket 連接。只有成功連入協調者,協調者才能開啓正常的組協調操作,比如加入組、等待組分配方案、心跳請求處理、位移獲取、位移提交等。

3.消費數據時。

消費者會爲每個要消費的分區創建與該分區領導者副本所在 Broker 連接的 TCP。舉個例子,假設消費者要消費 5 個分區的數據,這 5 個分區各自的領導者副本分佈在 4 臺 Broker 上,那麼該消費者在消費時會創建與這 4 臺 Broker 的 Socket 連接。

創建多少個 TCP 連接?

下面我們來說說消費者創建 TCP 連接的數量。你可以先思考一下大致需要的連接數量,然後我們結合具體的 Kafka 日誌,來驗證下結果是否和你想的一致。

我們來看看這段日誌。

[2019-05-27 10:00:54,142] DEBUG [Consumer clientId=consumer-1, groupId=test] 
Initiating connection to node localhost:9092 (id: -1 rack: null) 
using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)

[2019-05-27 10:00:54,188] DEBUG [Consumer clientId=consumer-1, groupId=test] 
Sending metadata request  MetadataRequestData(topics=[MetadataRequestTopic(name=‘t4’)], 
allowAutoTopicCreation=true, includeClusterAuthorizedOperations=false, 
includeTopicAuthorizedOperations=false) to node localhost:9092 
(id: -1 rack: null) (org.apache.kafka.clients.NetworkClient:1097)

[2019-05-27 10:00:54,188] TRACE [Consumer clientId=consumer-1, groupId=test] 
Sending FIND_COORDINATOR {key=test,key_type=0} with correlation id 0 to node -1 
(org.apache.kafka.clients.NetworkClient:496)

[2019-05-27 10:00:54,203] TRACE [Consumer clientId=consumer-1, groupId=test] 
Completed receive from node -1 for FIND_COORDINATOR with correlation id 0,
 received {throttle_time_ms=0,error_code=0,error_message=null, 
node_id=2,host=localhost,port=9094} (org.apache.kafka.clients.NetworkClient:837)

[2019-05-27 10:00:54,204] DEBUG [Consumer clientId=consumer-1, groupId=test]
 Initiating connection to node localhost:9094 (id: 2147483645 rack: null) 
using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)

[2019-05-27 10:00:54,237] DEBUG [Consumer clientId=consumer-1, groupId=test] 
Initiating connection to node localhost:9094 (id: 2 rack: null) 
using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)

[2019-05-27 10:00:54,237] DEBUG [Consumer clientId=consumer-1, groupId=test] 
Initiating connection to node localhost:9092 (id: 0 rack: null) 
using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)

[2019-05-27 10:00:54,238] DEBUG [Consumer clientId=consumer-1, groupId=test] 
Initiating connection to node localhost:9093 (id: 1 rack: null) 
using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)

這裏我稍微解釋一下,日誌的第一行是消費者程序創建的第一個 TCP 連接,就像我們前面說的,這個 Socket 用於發送 FindCoordinator 請求。由於這是消費者程序創建的第一個連接,此時消費者對於要連接的 Kafka 集羣一無所知,因此它連接的 Broker 節點的 ID 是 -1,表示消費者根本不知道要連接的 Kafka Broker 的任何信息。

值得注意的是日誌的第二行,消費者複用了剛纔創建的那個 Socket 連接,向 Kafka 集羣發送元數據請求以獲取整個集羣的信息。

日誌的第三行表明,消費者程序開始發送 FindCoordinator 請求給第一步中連接的 Broker,即 localhost:9092,也就是 nodeId 等於 -1 的那個。在十幾毫秒之後,消費者程序成功地獲悉協調者所在的 Broker 信息,也就是第四行標爲橙色的“node_id = 2”。

完成這些之後,消費者就已經知道協調者 Broker 的連接信息了,因此在日誌的第五行發起了第二個 Socket 連接,創建了連向 localhost:9094 的 TCP。只有連接了協調者,消費者進程才能正常地開啓消費者組的各種功能以及後續的消息消費。

在日誌的最後三行中,消費者又分別創建了新的 TCP 連接,主要用於實際的消息獲取。還記得我剛纔說的嗎?要消費的分區的領導者副本在哪臺 Broker 上,消費者就要創建連向哪臺 Broker 的 TCP。在我舉的這個例子中,localhost:9092,localhost:9093 和 localhost:9094 這 3 臺 Broker 上都有要消費的分區,因此消費者創建了 3 個 TCP 連接。

看完這段日誌,你應該會發現日誌中的這些 Broker 節點的 ID 在不斷變化。有時候是 -1,有時候是 2147483645,只有在最後的時候纔回歸正常值 0、1 和 2。這又是怎麼回事呢?

前面我們說過了 -1 的來由,即消費者程序(其實也不光是消費者,生產者也是這樣的機制)首次啓動時,對 Kafka 集羣一無所知,因此用 -1 來表示尚未獲取到 Broker 數據。

那麼 2147483645 是怎麼來的呢?它是由 Integer.MAX_VALUE 減去協調者所在 Broker 的真實 ID 計算得來的。看第四行標爲橙色的內容,我們可以知道協調者 ID 是 2,因此這個 Socket 連接的節點 ID 就是 Integer.MAX_VALUE 減去 2,即 2147483647 減去 2,也就是 2147483645。這種節點 ID 的標記方式是 Kafka 社區特意爲之的結果,目的就是要讓組協調請求和真正的數據獲取請求使用不同的 Socket 連接。

至於後面的 0、1、2,那就很好解釋了。它們表徵了真實的 Broker ID,也就是我們在 server.properties 中配置的 broker.id 值。

我們來簡單總結一下上面的內容。通常來說,消費者程序會創建 3 類 TCP 連接:

那麼,這三類 TCP 請求的生命週期都是相同的嗎?換句話說,這些 TCP 連接是何時被關閉的呢?

何時關閉 TCP 連接?

和生產者類似,消費者關閉 Socket 也分爲主動關閉和 Kafka 自動關閉。主動關閉是指你顯式地調用消費者 API 的方法去關閉消費者,具體方式就是手動調用 KafkaConsumer.close() 方法,或者是執行 Kill 命令,不論是 Kill -2 還是 Kill -9;而 Kafka 自動關閉是由消費者端參數 connection.max.idle.ms控制的,該參數現在的默認值是 9 分鐘,即如果某個 Socket 連接上連續 9 分鐘都沒有任何請求“過境”的話,那麼消費者會強行“殺掉”這個 Socket 連接。

不過,和生產者有些不同的是,如果在編寫消費者程序時,你使用了循環的方式來調用 poll 方法消費消息,那麼上面提到的所有請求都會被定期發送到 Broker,因此這些 Socket 連接上總是能保證有請求在發送,從而也就實現了“長連接”的效果。

針對上面提到的三類 TCP 連接,你需要注意的是,當第三類 TCP 連接成功創建後,消費者程序就會廢棄第一類 TCP 連接,之後在定期請求元數據時,它會改爲使用第三類 TCP 連接。也就是說,最終你會發現,第一類 TCP 連接會在後臺被默默地關閉掉。對一個運行了一段時間的消費者程序來說,只會有後面兩類 TCP 連接存在。

可能的問題

從理論上說,Kafka Java 消費者管理 TCP 資源的機制我已經說清楚了,但如果仔細推敲這裏面的設計原理,還是會發現一些問題。

我們剛剛講過,第一類 TCP 連接僅僅是爲了首次獲取元數據而創建的,後面就會被廢棄掉。最根本的原因是,消費者在啓動時還不知道 Kafka 集羣的信息,只能使用一個“假”的 ID 去註冊,即使消費者獲取了真實的 Broker ID,它依舊無法區分這個“假”ID 對應的是哪臺 Broker,因此也就無法重用這個 Socket 連接,只能再重新創建一個新的連接。

爲什麼會出現這種情況呢?主要是因爲目前 Kafka 僅僅使用 ID 這一個維度的數據來表徵 Socket 連接信息。這點信息明顯不足以確定連接的是哪臺 Broker,也許在未來,社區應該考慮使用< 主機名、端口、ID>三元組的方式來定位 Socket 資源,這樣或許能夠讓消費者程序少創建一些 TCP 連接。

也許你會問,反正 Kafka 有定時關閉機制,這算多大點事呢?其實,在實際場景中,我見過很多將 connection.max.idle.ms 設置成 -1,即禁用定時關閉的案例,如果是這樣的話,這些 TCP 連接將不會被定期清除,只會成爲永久的“殭屍”連接。基於這個原因,社區應該考慮更好的解決方案。

小結

好了,今天我們補齊了 Kafka Java 客戶端管理 TCP 連接的“拼圖”。我們不僅詳細描述了 Java 消費者是怎麼創建和關閉 TCP 連接的,還對目前的設計方案提出了一些自己的思考。希望今後你能將這些知識應用到自己的業務場景中,並對實際生產環境中的 Socket 管理做到心中有數。


640?wx_fmt=jpeg

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