前言
如果是第一次看到這篇的,建議先去補一下以往的5篇基礎,會對你理解起來有很大幫助哦
從上一年開始鴿了好久的原始碼篇,終於也是給整了一下。其實一方面也是,怕自己整理不好,看的雲裡霧裡,那也沒什麼意思,所以還是花了些時間準備,也是希望能夠和大家一起進步吧。注意,本文篇幅非常長,建議結合PC端的右側導航觀看,效果更佳。好的!話不多說,開始吧!
二、Producer的初始化核心流程
把原始碼導進來,這裡需要有一段時間去下載依賴,導完了就可以看到整個原始碼的結構是這樣的
![](https://i.iter01.com/images/c57cb3ebf18e0d93e65d49644b24217b019340a7d76700a9af8556327075a8e5.png)
如果要一個一個類地去說明,那肯定會非常亂套的,所以要藉助場景驅動。巧了,這個場景甚至還不需要我來寫。看見原始碼裡面有個example包了嗎?大部分的大資料框架都是開源的,為了推廣,首先官方文件要寫的詳細,而且還得自己提供一些不錯的示例包才方便。
![](https://i.iter01.com/images/c9ad19931bcd9fb6141cd8067a04825721e5b7e6e933e04d59082d0ac04f0659.png)
2.1 原始碼中自帶的Producer.java例子
此時點開Producer.java,是否發現在它的構造器中,這段程式碼我們有點似曾相識,甚至可以說非常熟悉
![](https://i.iter01.com/images/5c6ad28759d1b70fe6a2e9ebe5307b2e7eaa75631a8a21e6066855e8858e1da9.png)
/**
* 初始化生產者物件
*
* @param topic
* @param isAsync
*/
public Producer(String topic, Boolean isAsync) {
// 新建一個配置檔案
Properties props = new Properties();
// 拉取kafka的後設資料
props.put("bootstrap.servers", "localhost:9092");
// 這個引數先無視(client.id是管理許可權用的)
props.put("client.id", "DemoProducer");
// 針對key和value設定序列化類
props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 使用上方的引數初始化了一個KafkaProducer
producer = new KafkaProducer<>(props);
this.topic = topic;
this.isAsync = isAsync;
}
複製程式碼
甚至說你會覺得當時 插曲:Kafka的生產者原理及重要引數說明 我們來模擬生產者的時候做的配置更多,那是因為當時多了調優部分的引數。這個只是最基礎的。
補充一下對 “拉取kafka的後設資料” 的說明
因為之前的文章曾經有小夥伴對 props.put("bootstrap.servers", "localhost:9092"); 有疑問,這裡再補充兩句。叢集中每一個broker的後設資料都是一致的,我指定的那個localhost:9092不一定是leader,我獲取到了叢集後設資料,自然就知道leader partition在哪裡了,消費者和生產者都只會和leader打交道。然後通過leader我才知道該發去哪裡。所以,指定地址(這個地址可指定多個)是為了找到一個broker拿到後設資料,從而得知leader在哪裡而已。
用作場景驅動的 run 方法
![](https://i.iter01.com/images/e5711f1f215598abd4946613f51128176919d5f96d732089f8c6627045760a14.png)
緊接著有一個run方法,模擬資料傳入的
2.2 Kafka的初始化方法
![](https://i.iter01.com/images/b4ee57c60f8a12750f7050e40a773666f39d061f8492c6e868a0958c31b18a0c.png)
此時我們把目光聚焦在這個初始化方法,看它都幹了些神馬事情,此時會跳轉到KafkaProducer.java,將近188行的那一個
![](https://i.iter01.com/images/f5ca7fcf78484254a7737427c28d9787912ab093f53ee2f1ddbf5915c7de2578.png)
點進去this,我們就可以找到Kafka的建構函式了
2.3 Kafka的建構函式(原理分析)
此時我們先撇開原始碼不說,先來畫個原理圖。這整一個流程,和當時我們在 分析生產者的那一篇 中是一樣的。
![](https://i.iter01.com/images/adb57747431b039827b87dff49853e27c8bec956521f07c7aeac091acd3770df.png)
在 Kafka的執行流程總結和原始碼前準備 也有提到,如果對這塊還不瞭解的朋友,可以跳轉到這兩篇中閱讀,整理一下思路
![](https://i.iter01.com/images/5f3c4b00242843139b9b91bdb755d0f77c90d326a5f966d6d0ccf9420802bba4.png)
2.3.1 丟進緩衝區前的操作
首先我們現在是初始化了一個 KafkaProducer 對吧。然後會有一個 ProducerInterceptors ,看這個英文像是攔截器,它會把我們的訊息根據一定的規則去過濾掉。但是這個東西其實作用不大,因為我通過if-else都可以代替它的作用,所以就是比較雞肋。所以傳送訊息前會用它進行一個訊息的過濾,結束後會對訊息進行 序列化 。序列化結束,就找到 Partitioner分割槽器 (要知道該傳送到哪一臺伺服器上的哪一個分割槽)進行分割槽。
所以我們現在得到的四個關鍵詞是
![](https://i.iter01.com/images/4550868e1a802e092e5f80e998f0c40b674e93fdd42111b81c4ceca0925425f7.png)
2.3.2 緩衝區的結構
此時傳送之前,我們要先把訊息放入一個緩衝區裡面,那麼這個緩衝區其實是叫 RecordAccumulator ,緩衝區裡面會存在多個deque佇列,之前的文章中也提到過,kafka的訊息並不是逐條傳送的,而是會打包成一個個批次(每個批次預設16K)傳送。這些佇列裡面的封裝好的訊息批次會依次傳送給不同的分割槽(圖中僅列出1,2,3),比如下圖
![](https://i.iter01.com/images/d5997d990a542f15db6a78dd44cfcc7a1826da1389aadfbf094665d4c1bd08a9.png)
第一個deque就只負責傳送給分割槽1,第二個deque就僅傳送給分割槽2···依次類推
2.3.3 Sender執行緒的結構
真正傳送資料的其實就是這個Sender執行緒,如下圖
![](https://i.iter01.com/images/f1e299c408b7ac993242b305c728f473d0a167e3824721e0e6f96b28c6358080.png)
Sender啟動起來之後會建立請求ClientRequest,這裡的ClientRequest並不是完全一樣的。因為發往不同的伺服器應該是各種不同的請求。建立請求完成後,會傳送給NetWorkClient,它是管理Kafka網路的非常重要的元件。它會在它的裡面暫存請求,至於為何需要這樣,我們之後說明。
後面的selector裡的KafkaChannel其實就是類似於我們在 NIO 中所提到的SocketChannel,之後selector會傳送訊息給Kafka,這個過程是客戶端向服務端傳送訊息,此時服務端,也就是Kafka會再返回響應,這個響應也仍舊是這個KafkaChannel接收,然後返回給NetworkClient,經過處理後返回給客戶端。
2.3.4 原理分析總圖
所以整個流程走下來應該就是這樣的一張圖。圖中已經用數字1~12標好流程,當然也可以增加一個
13.NetworkClient返回結果給客戶端
複製程式碼
![](https://i.iter01.com/images/057d889e4d6a2cebc85403d84f6355fa6b673b9dc4eb81be7bf40e6247934be1.png)
這個圖也是非常非常粗略的一個流程說明,Kafka的原始碼細節遠比這個圖來的細緻,所以大家看到這裡如果覺得似懂非懂也是正常,後面結合原始碼說明一定能更加清楚。
2.4 建構函式原始碼
說白了原始碼我們講到的部分就是我們剛剛畫好的圖的第一步,KafkaProducer的初始化操作。原始碼非常的長,所以我們會以小段擷取的方式講解,此時回到KafkaProducer.java,注意,不是主要邏輯部分,就會標明非重點
2.4.1 配置使用者自定義的引數(非重點)
![](https://i.iter01.com/images/6535de6d0b3cb5876bf00c724315dde8676f57f9c41e162ffc85affb8ab08149.png)
2.4.2 clientId(非重點)
![](https://i.iter01.com/images/5513f585fd3b0de9df29879ed7887dde2cf45323159aa06a40698d126db151f4.png)
2.4.3 metric(非重點)
metric是監控方面的,不是我們關心的邏輯部分
![](https://i.iter01.com/images/6f67c3c9d93ade296ee7366585c3610abd74aaeb91341552eceb4cd445e1a17e.png)
2.4.4 分割槽器
![](https://i.iter01.com/images/52bfbdef36189fd1943b1a4be13d6c55773cc89045df526580e81f10ee9c81a4.png)
當時我們也有所提及,可以給每一個訊息設定一個key,也可以不指定,這個key跟我們要把這個訊息傳送到哪個主題的哪個分割槽是有關係的。而分割槽器就是為了處理這些事情,這裡預設你們忘了,擷取以前的文章片段,是 Kafka的生產者案例和消費者原理解析 中的
![](https://i.iter01.com/images/035572630c7335934f78b357e2cacea7e0e7bc04f08937a75cd52d95a617aba4.png)
所以非常推薦大家能把以前的幾篇基礎讀一下,相信會對你理解這些操作幫助很大。
2.4.5 重試時間(非重點)
![](https://i.iter01.com/images/bbd05280627f39e290994514d981328f557b283c431f8a4ec731b645c0e763d0.png)
這裡大家知道這個引數就好了,也可以自行點進去看一下預設值,這裡直接告訴大家預設是100毫秒得了
2.4.6 序列化器(非重點)
![](https://i.iter01.com/images/14a1022f545e8bb73763019017839d1d169edb3483d31d3ea21bc6d4b8cef3ef.png)
其實就是我們文章開篇的那兩個
![](https://i.iter01.com/images/2a49c744632f45eb0d34daf865976d18427611cd68604ec68d41570ffc8ec6a1.png)
2.4.7 攔截器 (非重點)
![](https://i.iter01.com/images/663604c4ba9089b9f7189036c0b094e8cea2e8f42ba09f95b9a2dc56365f54a6.png)
2.4.8 後設資料單元 Metadata
下方4個引數會分別提及一下
![](https://i.iter01.com/images/2458c4e2ea87c2972d02c3b4187e3471e575226f8453a29af60c117aad885436.png)
第一個引數
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners);
複製程式碼
引數 METADATA_MAX_AGE_CONFIG ,預設值是5分鐘,作用是預設每隔5分鐘,生產者會從叢集中去獲取一次後設資料資訊。因為要傳送訊息的話我們必須保證後設資料資訊是準確的。
第二個引數
this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
複製程式碼
引數 MAX_REQUEST_SIZE_CONFIG 這裡代表的是生產者往服務端傳送訊息時規定一條訊息最大為多少。而如果你超過了這個規定的大小,你的訊息就無法傳送出去。預設是1M,這個值有點偏小了,生產環境中需要去修改這個值。比如10M,當然這個因地制宜,大家需要結合公司的實際情況決定。
第三個引數
this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
複製程式碼
引數 BUFFER_MEMORY_CONFIG 指的是緩衝區,也就是 RecordAccumulator 大小。這個值一般是夠用的,預設是32M
第四個引數
this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));
複製程式碼
引數 COMPRESSION_TYPE_CONFIG 預設情況下是不支援壓縮,不過也可以設定,可供選擇的除了none,還有gzip,snappy,lz4,我們一般會使用lz4,這些都是可以點進去原始碼裡面檢視的。這裡我就不點進去了。
進行了壓縮後,一次傳送出去的訊息就變多,自然吞吐量是上來了,不過會對cpu造成一定的負擔,請思考清楚後使用。
2.4.9 根據先前提供的引數初始化緩衝區
![](https://i.iter01.com/images/20256165d3bb8fba21f42b0d77967b719e1f1c5e8700160263393b85679e5c94.png)
2.4.10 獲取叢集中的後設資料資訊的地址
![](https://i.iter01.com/images/21a84e0260151f3c1bdcee83272562135b5d96dc651f2a9795ce41aaaab0b9b3.png)
引數 BOOTSTRAP_SERVERS_CONFIG 和我們之前寫過的demo程式碼是一樣的
props.put("bootstrap.servers", "hadoop1:9092,hadoop2:9092,hadoop3:9092");
複製程式碼
BOOTSTRAP_SERVERS_CONFIG 就是這個"hadoop1:9092,hadoop2:9092,hadoop3:9092",它的作用就是給生產者指明方向去獲取叢集中的後設資料而已。
2.4.11 update
這個看起來把地址作為引數傳進去了,像是獲取或更新後設資料資訊的方法,後面我們來驗證一下我們的猜測是否正確
![](https://i.iter01.com/images/a88ea95625cc8cbf7d5858ce33821bca7606a708e54be69ed512da13d79d7fc9.png)
2.4.12 初始化元件 NetworkClient
![](https://i.iter01.com/images/62e956e46087b2f8302f7dd6c4dc13c6a9f5c099691d621cee853690711d193b.png)
這裡面也有好幾個引數需要去注意
--- ① CONNECTIONS_MAX_IDLE_MS_CONFIG
一個網路連線最大空閒時間,超過之後會自動關閉此連線,預設值為9min
一般情況下我們會設定成-1,-1時是什麼情況下都不回收
--- ② (重要)MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION
每個傳送資料的網路連線對並未接收到響應的訊息的最大數。預設值是5
是不是感覺非常地拗口,那我們換個說法,producer向各個伺服器傳送資料都會建立不同的網路連線,然後開始傳送資料,假如現在我們的MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION設定成預設值5,傳送了1,2,3,4,5···,這伺服器都沒給我們返回響應,那訊息6我們就不能繼續再發了。
注意:因為Kafka的重試機制有可能會導致訊息亂序,所以我們一般為了保證訊息有序會把 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 設定為1.
比如我們常見的訂單系統和會員積分系統就是非常鮮明的場景,訂單是要建立過後才能取消的,而對應的會員積分是要先增後減的,如果這個順序不能保證,系統就會出現問題。
所以千萬不要以為,給我們的message設定了key,保證了同一個場景的訊息放到了同一個分割槽,就可以保證訊息的順序,在Kafka中要保證真正的有序,是需要設定這個 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 引數為 1 的
--- ③ SEND_BUFFER_CONFIG 和 RECEIVE_BUFFER_CONFIG
因為這不是很重要的東西所以就丟一起了,就是NIO的一些東西
SEND_BUFFER_CONFIG 指socket傳送資料時緩衝區的大小,預設128K(如果忘記了請回顧NIO篇)
RECEIVE_BUFFER_CONFIG 指socket接受資料的緩衝區的大小,預設是32K
2.4.13 Sender執行緒的初始化流程
![](https://i.iter01.com/images/dcb68a260e02fa80ac6198659564d2764857ed970b44870411983845a306d5da.png)
重要的引數就一個,RETRIES_CONFIG 是重試次數,預設是不重試,這樣就十分坑爹了,這個情況下程式很脆弱,只要稍微出現了一些小毛病就掛掉了。大資料都是分散式的系統,因為網路的一些不穩定,導致整個系統掛掉,那就得不償失了。之前也告訴過大家了,程式中 95% 的問題,都是可以通過重試解決的
當然 ACKS_CONFIG 這個引數也十分重要,不過我們在之前講生產者的時候已經講過了,不信我給你截圖
![](https://i.iter01.com/images/dd9090c59257a5bc3200abc5fa20226ef75e7d274b8db75d09ec98ebfbf469a0.png)
你看,我不會騙你的。在此也是再強調一次,前面幾篇的基礎都是有用的,最好還是可以去補補哦!
所以如果面試時候問如何保證資料不丟失,ACKS_CONFIG是一個很重要的引數。要設定為 -1,還有另外一個引數後面再提。
2.4.14 啟動Sender執行緒
![](https://i.iter01.com/images/387c444447b213ae3705d5b7385357d1eb4190029ceffe107da485cf3a1c2261.png)
在這裡你會發現,Kafka的原始碼在一些細節方面做的相當的出色,它這個new KafkaThread可以點進去看一下
![](https://i.iter01.com/images/932dc305987dd8942f044c5c21348074e1ad261622ea3e5f579c4b9e2583cd23.png)
它就是把這個執行緒設定成後臺執行緒,它不直接啟動而是建立執行緒把Sender傳進去的原因就是因為它要把業務程式碼和執行緒相關的程式碼隔離開來,就算之後你還要增加一些引數給這個執行緒,你也直接在 KafkaThread.java 中補充即可。通過這些小細節,是可見這個程式碼的編寫是十分優秀的。
到這裡這個生產者的建構函式就差不多了,不過我們還有metadata這個關鍵的東西沒有展開
2.5 Metadata是如何管理後設資料的
我們點進去Metadata.java來看看
![](https://i.iter01.com/images/8b0242d41af1426514d3eb900a5e628405edad049ae19bdee9f4213bc12e2dd6.png)
這裡面的引數簡單過一下
2.5.1 refreshBackoffMs
兩次更新後設資料的請求的最小時間間隔,預設100ms。因為我們請求後設資料的過程其實不是一定成功的,而請求不到後設資料資訊的話,那我們就找不到leader partition了。
2.5.2 metadataExpireMs
這個是多久時間自動更新一次後設資料,預設5min一次
2.5.3 version
對於producer端來說,後設資料是有版本號的,每次更新後設資料後都會更新這個版本號。
2.5.4 lastRefreshMs
最後一次更新後設資料的時間
2.5.5 lastSuccessfulRefreshMs
最後一次 成功 更新後設資料的時間
2.5.6 cluster(最重要)
Kafka叢集的後設資料
2.5.7 needUpdate
是否需要更新後設資料的標識
2.5.8 topics
表示現在已有的topic
2.6 Cluster --- Kafka叢集中的後設資料
![](https://i.iter01.com/images/1b058cd41f6cf08dabfaa9de053798ed8426b4cec2fee95a3a6ad3ed50def423.png)
2.6.1 nodes
我們知道Kafka叢集是多個節點的,這個引數代表的就是Kafka的節點,我們也可以點進去node看看,其實無非就是一些主機名,埠號等欄位
![](https://i.iter01.com/images/5d5cd20a089fae8069c25956f24dc165671c1ba328e2269a7ee4cf174542e1b3.png)
2.6.2 unauthorizedTopics 和 internalTopics
關於Kafka的許可權方面的topic,知道有這麼回事就可以了
2.6.3 一些封裝好的資料結構
這些資料結構不一定是全部用的上的
private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition;
private final Map<String, List<PartitionInfo>> partitionsByTopic;
private final Map<String, List<PartitionInfo>> availablePartitionsByTopic;
private final Map<Integer, List<PartitionInfo>> partitionsByNode;
private final Map<Integer, Node> nodesById;
private final ClusterResource clusterResource;
複製程式碼
Map partitionsByTopicPartition ,代表了partition和它的關聯資訊,我們可以點進去 PartitionInfo 看看,為了方便觀看,我就直接寫好註釋了
![](https://i.iter01.com/images/2b7514d22ce087e3279558293df55d8f609a19483f77df47e10301dbfebf381d.png)
所以 PartitionInfo 其實就是這個 partition 所對應的資訊
Map> partitionsByTopic 代表這個topic有哪些分割槽
Map> availablePartitionsByTopic 代表這個topic有哪些可用的partition
Map> partitionsByNode 這臺伺服器上面有哪些partition(伺服器用的是伺服器的編號標識)
private final Map nodesById 記錄伺服器和伺服器編號(編號從0開始)的Map
private final ClusterResource clusterResource Kafka叢集的id資訊(這個引數不怎麼重要)
那到這裡,Metadata的結構我們大體上就瞭解了,回到一開始的 KafkaProducer.java
![](https://i.iter01.com/images/6079be8d166602e0d170d72285fcb821acc3550978d85dde697c46f0d888bbef.png)
現在我們就知道了,它是靠上面我們所說的這些資料結構去維護後設資料資訊的
2.6 剛剛我們猜想是獲取更新後設資料的update
對2.4.11 的展開說明
![](https://i.iter01.com/images/774d84a9764f6227b22bc4c0daf38328d2c735f742280ad900a72f2b36eb4de9.png)
點進去update,拉到大概204行看看
![](https://i.iter01.com/images/03fc3bbde4014e0664c88e14c51e94b92472613a9c3234fe4a3c74368a59dc18.png)
我們都可以逐一地把這些條件的預設值看一下
![](https://i.iter01.com/images/a78c78c3faed620e3d350cc2641d4f136118826f20161730864f815f7132f10a.png)
可是我們現在就懵了,這個cluster不就是剛剛我們傳進來的引數 Cluster.bootstrap(addresses) 嗎?這明顯啥都沒幹啊,所以我們一開始的猜想就錯了,所以結論就是:
生產者在初始化的過程中,是並沒有去獲取後設資料資訊的
但是轉念一想,反正我們傳送訊息的時候,是一定要獲取到叢集後設資料才可以得知叢集中leader的存在的,所以我們之後只要到傳送的邏輯前後去找就好了。
到這些,生產者的初始化就結束了。
2.7 獲取到的資訊
在KafkaProducer的初始化過程裡面,初始化了很多重要的引數和幾個核心的元件,也帶領大家把圖大致地畫了出來,例如 RecordAccumulator ,Sender,NetworkClient,而且Sender執行緒其實是初始化好的時候就已經啟動了。還有初始化的過程中並沒有拉取後設資料的行為。
三、producer傳送訊息的核心流程
回到夢開始的地方,那個原始碼中自帶的例子
![](https://i.iter01.com/images/6b19303eb7c6b83d270a3a01becab0be1ae646489ec6c13862915491fcfd85e3.png)
我們知道就是這個send來傳送訊息的,那我們就點進去看看吧
![](https://i.iter01.com/images/bd6db3588c6eafba1ba742863399014787cfb64ae8a48ee248cbbadad7386d8b.png)
我們可以看到,程式碼非常長,大致跳轉到了KafkaProducer的454行,直接從try開始整理步驟,其實這個步驟,我們也是大致清楚的
![](https://i.iter01.com/images/7c59362be152453927ad1756b84f1a704079a2118262e1c8880e4da62bb35cd4.png)
暫時走這前5步
3.1 拉取後設資料
![](https://i.iter01.com/images/b5d2af27f6cc701d06d9f5c5bca2be4f21ca589ac267826ca1065ee5a52d0117.png)
把註釋放到百度翻譯,這東西就完美地理解了,我們在傳送訊息前就是通過這個waitOnMetadata來同步等待後設資料的拉取的。maxBlockTimeMs是指最多等待這個拉取過程多久,因為這個拉取過程進行時程式碼是阻塞在這裡的,所以我們必須設定一個時間限制來放行。
![](https://i.iter01.com/images/2b57fc7b011452cfe125c2140bf25283bca384a110cd5da2c34a54ca49fa6134.png)
這個計算了一個剩餘時間,然後把叢集中的後設資料更新。
![](https://i.iter01.com/images/1773ca723729e968d40ad216dc312f9bcae32949467e0a80980c50cedc219694.png)
3.2 對訊息的key,value進行序列化
![](https://i.iter01.com/images/86e6688223fd9639c82157eed8b912eb1234d9aa9c5558a77eba0e8c0c523cf1.png)
3.3 根據分割槽器選擇訊息應該發往的分割槽
int partition = partition(record, serializedKey, serializedValue, cluster);
int serializedSize = Records.LOG_OVERHEAD + Record.recordSize(serializedKey, serializedValue);
複製程式碼
因為現在我們已經獲取到了後設資料了,這兒就可以開始根據後設資料資訊進行計算得出傳送結果。
3.4 確認一下訊息的大小是否超過了最大值
ensureValidRecordSize(serializedSize);
複製程式碼
KafkaProducer初始化的時候,指定了一個引數,代表了producer這裡最大能傳送的一條訊息能有多大,預設1M,一般都會修改
3.5 根據後設資料資訊封裝分割槽物件
tp = new TopicPartition(record.topic(), partition);
long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();
log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
複製程式碼
3.6 給訊息繫結回撥函式
Callback interceptCallback = this.interceptors == null ? callback : new InterceptorCallback<>(callback, this.interceptors, tp);
複製程式碼
因為我們是非同步傳送的方式,所以需要回撥函式來確認
3.7 訊息存入 RecordAccumulator
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);
if (result.batchIsFull || result.newBatchCreated) {
log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
this.sender.wakeup();
}
return result.future;
複製程式碼
RecordAccumulator是預設32M的一塊緩衝區,之後在這裡我們需要把訊息封裝成一個個的batch來傳送,如果批次滿了,就會新建立出一個新的批次。啟動Sender執行緒去傳送資料。
四、waitOnMetadata是如何工作的
![](https://i.iter01.com/images/5886477f4cfe8169060bb46158b4f705ce369e87516779ae14371fa42229d4cc.png)
4.1 把當前的topic存入到後設資料裡面
// add topic to metadata topic list if it is not there already and reset expiry
metadata.add(topic);
複製程式碼
註釋翻譯:將主題新增到後設資料主題列表(如果尚未存在),並重置過期時間
4.2 fetch操作
Cluster cluster = metadata.fetch();
複製程式碼
這裡fetch是直接從快取中獲取到已存在的後設資料。但是經過我們剛剛的分析,我們知道此時這個cluster是沒有資料的,這裡面只有我們作為引數的addresses而已。根據我們的場景驅動,在第一次執行到這裡時也是剛好KafkaProducer初始化完成的時候。此時cluster並沒有獲取到後設資料
4.3 檢視分割槽資訊
Integer partitionsCount = cluster.partitionCountForTopic(topic);
複製程式碼
這裡是根據當前的topic從叢集中的cluster檢視分割槽資訊,但是同理,第一次執行時也是沒有資料的,cluster啥都沒有
4.4 返回後設資料資訊和時間
if (partitionsCount != null && (partition == null || partition < partitionsCount))
return new ClusterAndWaitTime(cluster, 0);
複製程式碼
同理,我們根本不會執行到這一步,因為我們第一次執行時根本沒有獲取過後設資料
4.5 從服務端拉取後設資料
剛剛的4.2~4.4第一次執行都是無用功,拉取後設資料還得從這裡開始。先是定義了3個關於時間的引數
// 記錄當前時間
long begin = time.milliseconds();
// 剩餘多長時間,預設值為剛剛提到的最大等待時間
long remainingWaitMs = maxWaitMs;
// 已花時間
long elapsed;
複製程式碼
然後是一個do···while迴圈來獲取後設資料
![](https://i.iter01.com/images/2e72f0345652f0b02d0eb29d4acfc6f478b67c7a3fe008fd6d0e9a18bc7d6064.png)
4.5.1 版本號的問題
int version = metadata.requestUpdate();
複製程式碼
此時我們的第一個操作就是獲取到後設資料的版本,對於producer來說,後設資料是有版本號的,每次成功更新後設資料都得要更新一次版本號。requestUpdate方法主要是把 2.6.7中的 needUpdate 的值改為true,然後獲取到當前後設資料的版本號。
4.5.2 sender.wakeup()
![](https://i.iter01.com/images/788681da17159bd89a1578a074692053d156e28af8b11e01cf7402e59f075bd0.png)
這裡我們發現Sender執行緒也開始幹活了,其實是因為拉取後設資料的過程是由Sender執行緒來完成的,這個地方把Sender喚醒之後,就開始 同步 等待後設資料的到來,這一點可以從while (partitionsCount == null)可知
4.5.3 嘗試獲取一些引數
如果成功執行,應該就已經獲取到後設資料了,所以我們可以嘗試獲取一些引數資訊
![](https://i.iter01.com/images/8595bb68cf694bd1ae711f82e409a5f2fd4c9cda87aaa9bbea46d5f6422f1c7a.png)
4.5.4 同步等待後設資料的awaitUpdate
在4.5.2我們提到了通過這個方法同步等待後設資料的到來
![](https://i.iter01.com/images/2e912ee8a8dfe71578efeecf7bd510f2a53505f4750baf1a233d1f67527618f5.png)
雖然我們還沒去看Sender執行緒的原始碼,可是我們猜也能猜到,更新後設資料成功之後一定會把這個 wait(remainingWiaitMs) 給喚醒。其他大部分的程式碼都是大家已經可以看懂的程式碼了。
4.6 Sender是如何拉取後設資料的
去到Sender.java,然後找到run方法
![](https://i.iter01.com/images/55337c14b4ae9f74f0590af5a325cc59b0a7e2668e2fbbfa26a9febc0f976bf7.png)
第一句Cluster cluster = metadata.fetch();我們已經看了好多好多次了,是沒有後設資料的大家應該都知道了。是的,這個run方法雖然很長,可是,一直到236行,在第一次執行,沒有後設資料的情況下,都是不執行的,執行的只有下面這一句
![](https://i.iter01.com/images/1c75842473a627f3375534b3365f4d752d6834eca70089e54bd56d6b11d36e09.png)
這裡面的client,我們點一下,會發現是一個 KafkaClient,而這 KafkaClient,它的實現類是 NetworkClient
![](https://i.iter01.com/images/34a0fea70cf266c67b675282be95e465781dc6aa1742e839fbc53e9a90a26e4d.png)
而我們如果要看poll方法的邏輯,就直接點開 NetworkClient 的poll即可
4.6.1 NetworkClient 的poll方法
Kafka的網路設計之後我們再提及,如果閱讀這裡有壓力的話,之後再回頭看這裡就很好懂了。
--- 步驟1:封裝了一個拉取後設資料請求
long metadataTimeout = metadataUpdater.maybeUpdate(now);
複製程式碼
點進去 maybeUpdate 瞧瞧
![](https://i.iter01.com/images/229dcbc52571f6c6be2757031b6beb1979b91d1390917a8f354117bbe10e6266.png)
繼續點選,能看到一個封裝好的request,這個請求完成之後下一句程式碼就是doSend
![](https://i.iter01.com/images/42288c7cc9f79cc68b9ca7950598ef2f13a7c463fe0645010a3de3af43be2b4c.png)
點進去,是一個ClientRequest
![](https://i.iter01.com/images/75c08ef0ac7167909612d8d12f8940d5aac2cb76c21b3310fbdf4448956e490f.png)
![](https://i.iter01.com/images/06c45fef05b59c74e8f0d543fff950451a28fee14217305bbf9c5ecfd62a1adc.png)
之後我們點send方法,說真的我都有點頭暈了,此時會跳到Selectable.java裡面,發現send是個抽象方法,實現是Selector.java
![](https://i.iter01.com/images/4581ebd1ac93a62a508c5f07a4b16a5b58f10a363ee1e24b35f362e3d5c377ed.png)
這裡所有的引數我們都先不看,下篇我會展開,我們現在只想找到獲取後設資料的那個東西,差不多240行會看到它的send方法
![](https://i.iter01.com/images/48efab40648a3da8e86dcd87d23442161697e63635ff63894da49e1813250366.png)
點進去setSend,會跳轉到KafkaChannel的setSend方法
![](https://i.iter01.com/images/a886aee8428a42fb2286d51a643efc0b5b353e0021d2053424230faabceb2e07.png)
看到沒有,連selectionKey都是一模一樣的,如果在這裡不能理解的話,請跳轉NIO篇的NIO非阻塞式網路通訊那裡去複習一下哦
退回到networkClient的poll方法
--- 步驟2:執行網路IO的操作
![](https://i.iter01.com/images/102cc41c582d78b33ae7b30c871ff4f1516fe4ac0bf7bfda55510f59710cb501.png)
這個部分全部都是NIO的知識,在這裡我就不展開了。因為是那種不難但是跳來跳去的問題,截起圖來稍嫌麻煩。如果有存在疑問的話,歡迎交流,這裡我就跳過這些NIO的步驟了。我們之後會看到一個writeTo方法把請求傳送出去給服務端。知道這麼回事就差不多了
---步驟3:接收響應並處理
上面請求發出去了我們自然是要接收服務端返回的響應的
![](https://i.iter01.com/images/fbb3446727bf0c5bf0503334527a622c7db07bebaed24f00ff08fcce9571e90f.png)
點進去handleCompletedSends
![](https://i.iter01.com/images/fd2502879fbec6540dadccf32c66d16929e5aa87d5318d924dc79bd0583dfc06.png)
maybeHandleCompletedReceive就是處理響應的方法
![](https://i.iter01.com/images/244ff12d6ca59985c1cedafcc7021d5c4c80cd2021cb164361e8606ba0e03f71.png)
4.6.2 處理響應的邏輯及後設資料獲取
![](https://i.iter01.com/images/482cbcf05118080abc60d9f54ea34c153caccda422bd59d0aae796ce9f0657e2.png)
MetadataResponse response = new MetadataResponse(body);
複製程式碼
因為服務端傳送回來的也是一個二進位制的資料結構,所以生產者在這裡要對它進行解析,並封裝成一個MetadataResponse物件
Cluster cluster = response.cluster();
複製程式碼
響應裡面會帶有後設資料的資訊,現在進行獲取cluster物件了
![](https://i.iter01.com/images/860b00a10637692457ebb777197b26441fad4fd176da571f08907a46bed48199.png)
後面開始進行判斷,如果cluster.nodes().size() > 0,那就已經成功獲取到後設資料物件了,此時update,這個方法點進去你也會看到,version=version+=1,版本號加一了。關鍵點是後面還會有一句notifyAll()方法,把剛剛同步等待後設資料資訊的執行緒喚醒,讓程式碼退出while迴圈。
所以到此,就是一個完整的獲取到後設資料的過程了。
finally
真的是寫到後面自己都頭暈腦脹了,這種原始碼型別的說明起來非常吃力,跳來跳去,也是希望大家能夠有所收穫吧,一直到現在,我們再看看這個圖
![](https://i.iter01.com/images/1002684805c7abb1000f1475a52ec65104959f4721e8a26251645933934acc9f.png)
連序列化都還沒展開,任重而道遠啊···???
下一篇會把Kafka的網路設計給展開,感興趣的朋友可以關注(公眾號:說出你的願望吧)一下哦,覺得文章還可以的可以點個小贊,謝謝。
![](https://i.iter01.com/images/d4dd621bc2ee2368da8f031f4d3ca13ce62c34737835fc433cc51465cb148e4d.jpg)