如果只是為了開發 Kafka 應用程式,或者只是在生產環境使用 Kafka,那麼瞭解 Kafka 的內部工作原理不是必須的。不過,瞭解 Kafka 的內部工作原理有助於理解 Kafka 的行為,也利用快速診斷問題。下面我們來探討一下這三個問題
- Kafka 是如何進行復制的
- Kafka 是如何處理來自生產者和消費者的請求的
- Kafka 的儲存細節是怎樣的
如果感興趣的話,就請花費你一些時間,耐心看完這篇文章。
叢集成員間的關係
我們知道,Kafka 是執行在 ZooKeeper 之上的,因為 ZooKeeper 是以叢集形式出現的,所以 Kafka 也可以以叢集形式出現。這也就涉及到多個生產者和多個消費者如何協調的問題,這個維護叢集間的關係也是由 ZooKeeper 來完成的。如果你看過我之前的文章(真的,關於 Kafka 入門看這一篇就夠了),你應該會知道,Kafka 叢集間會有多個 主機(broker)
,每個 broker 都會有一個 broker.id
,每個 broker.id 都有一個唯一的識別符號用來區分,這個識別符號可以在配置檔案裡手動指定,也可以自動生成。
Kafka 可以通過 broker.id.generation.enable 和 reserved.broker.max.id 來配合生成新的 broker.id。
broker.id.generation.enable引數是用來配置是否開啟自動生成 broker.id 的功能,預設情況下為true,即開啟此功能。自動生成的broker.id有一個預設值,預設值為1000,也就是說預設情況下自動生成的 broker.id 從1001開始。
Kafka 在啟動時會在 ZooKeeper 中 /brokers/ids
路徑下注冊一個與當前 broker 的 id 相同的臨時節點。Kafka 的健康狀態檢查就依賴於此節點。當有 broker 加入叢集或者退出叢集時,這些元件就會獲得通知。
- 如果你要啟動另外一個具有相同 ID 的 broker,那麼就會得到一個錯誤 —— 新的 broker 會試著進行註冊,但不會成功,因為 ZooKeeper 裡面已經有一個相同 ID 的 broker。
- 在 broker 停機、出現分割槽或者長時間垃圾回收停頓時,broker 會從 ZooKeeper 上斷開連線,此時 broker 在啟動時建立的臨時節點會從 ZooKeeper 中移除。監聽 broker 列表的 Kafka 元件會被告知該 broker 已移除。
- 在關閉 broker 時,它對應的節點也會消失,不過它的 ID 會繼續存在其他資料結構中,例如主題的副本列表中,副本列表複製我們下面再說。在完全關閉一個 broker 之後,如果使用相同的 ID 啟動另一個全新的 broker,它會立刻加入叢集,並擁有一個與舊 broker 相同的分割槽和主題。
Broker Controller 的作用
我們之前在講 Kafka Rebalance 重平衡的時候,提過一個群組協調器,負責協調群組間的關係,那麼 broker 之間也有一個控制器元件(Controller),它是 Kafka 的核心元件。它的主要作用是在 ZooKeeper 的幫助下管理和協調整個 Kafka 叢集,叢集中的每個 broker 都可以稱為 controller,但是在 Kafka 叢集啟動後,只有一個 broker 會成為 Controller 。既然 Kafka 叢集是依賴於 ZooKeeper 叢集的,所以有必要先介紹一下 ZooKeeper 是什麼,可以參考作者的這一篇文章(ZooKeeper不僅僅是註冊中心,你還知道有哪些?)詳細瞭解,在這裡就簡單提一下 znode
節點的問題。
ZooKeeper 的資料是儲存在節點上的,每個節點也被稱為znode
,znode 節點是一種樹形的檔案結構,它很像 Linux 作業系統的檔案路徑,ZooKeeper 的根節點是 /
。
znode 根據資料的持久化方式可分為臨時節點和永續性節點。永續性節點不會因為 ZooKeeper 狀態的變化而消失,但是臨時節點會隨著 ZooKeeper 的重啟而自動消失。
znode 節點有一個 Watcher
機制:當資料發生變化的時候, ZooKeeper 會產生一個 Watcher 事件,並且會傳送到客戶端。Watcher 監聽機制是 Zookeeper 中非常重要的特性,我們基於 Zookeeper 上建立的節點,可以對這些節點繫結監聽事件,比如可以監聽節點資料變更、節點刪除、子節點狀態變更等事件,通過這個事件機制,可以基於 ZooKeeper 實現分散式鎖、叢集管理等功能。
控制器的選舉
Kafka 當前選舉控制器的規則是:Kafka 叢集中第一個啟動的 broker 通過在 ZooKeeper 裡建立一個臨時節點 /controller
讓自己成為 controller 控制器。其他 broker 在啟動時也會嘗試建立這個節點,但是由於這個節點已存在,所以後面想要建立 /controller 節點時就會收到一個 節點已存在 的異常。然後其他 broker 會在這個控制器上註冊一個 ZooKeeper 的 watch 物件,/controller
節點發生變化時,其他 broker 就會收到節點變更通知。這種方式可以確保只有一個控制器存在。那麼只有單獨的節點一定是有個問題的,那就是單點問題
。
如果控制器關閉或者與 ZooKeeper 斷開連結,ZooKeeper 上的臨時節點就會消失。叢集中的其他節點收到 watch 物件傳送控制器下線的訊息後,其他 broker 節點都會嘗試讓自己去成為新的控制器。其他節點的建立規則和第一個節點的建立原則一致,都是第一個在 ZooKeeper 裡成功建立控制器節點的 broker 會成為新的控制器,那麼其他節點就會收到節點已存在的異常,然後在新的控制器節點上再次建立 watch 物件進行監聽。
控制器的作用
那麼說了這麼多,控制是什麼呢?控制器的作用是什麼呢?或者說控制器的這麼一個元件
被設計用來幹什麼?彆著急,接下來我們就要說一說。
Kafka 被設計為一種模擬狀態機的多執行緒控制器,它可以作用有下面這幾點
-
控制器相當於部門(叢集)中的部門經理(broker controller),用於管理部門中的部門成員(broker)
-
控制器是所有 broker 的一個監視器,用於監控 broker 的上線和下線
-
在 broker 當機後,控制器能夠選舉新的分割槽 Leader
-
控制器能夠和 broker 新選取的 Leader 傳送訊息
再細分一下可以具體分為如下 5 點
主題管理
: Kafka Controller 可以幫助我們完成對 Kafka 主題建立、刪除和增加分割槽的操作,簡而言之就是對分割槽擁有最高行使權。
換句話說,當我們執行kafka-topics 指令碼時,大部分的後臺工作都是控制器來完成的。
-
分割槽重分配
: 分割槽重分配主要是指,kafka-reassign-partitions 指令碼提供的對已有主題分割槽進行細粒度的分配功能。這部分功能也是控制器實現的。 -
Prefered 領導者選舉
: Preferred 領導者選舉主要是 Kafka 為了避免部分 Broker 負載過重而提供的一種換 Leader 的方案。 -
叢集成員管理
: 主要管理 新增 broker、broker 關閉、broker 當機 -
資料服務
: 控制器的最後一大類工作,就是向其他 broker 提供資料服務。控制器上儲存了最全的叢集後設資料資訊,其他所有 broker 會定期接收控制器發來的後設資料更新請求,從而更新其記憶體中的快取資料。這些資料我們會在下面討論
當控制器發現一個 broker 離開叢集(通過觀察相關 ZooKeeper 路徑),控制器會收到訊息:這個 broker 所管理的那些分割槽需要一個新的 Leader。控制器會依次遍歷每個分割槽,確定誰能夠作為新的 Leader,然後向所有包含新 Leader 或現有 Follower 的分割槽傳送訊息,該請求訊息包含誰是新的 Leader 以及誰是 Follower 的資訊。隨後,新的 Leader 開始處理來自生產者和消費者的請求,Follower 用於從新的 Leader 那裡進行復制。
這就很像外包公司的一個部門,這個部門就是專門出差的,每個人在不同的地方辦公,但是中央總部有一個部門經理,現在部門經理突然離職了。公司不打算外聘人員,決定從部門內部選一個能力強的人當領導,然後當上領導的人需要向自己的組員傳送訊息,這條訊息就是任命訊息和明確他管理了哪些人,大家都知道了,然後再各自給部門幹活。
當控制器發現一個 broker 加入叢集時,它會使用 broker ID 來檢查新加入的 broker 是否包含現有分割槽的副本。如果有控制器就會把訊息傳送給新加入的 broker 和 現有的 broker。
上面這塊關於分割槽複製的內容我們接下來會說到。
broker controller 資料儲存
上面我們介紹到 broker controller 會提供資料服務,用於儲存大量的 Kafka 叢集資料。如下圖
可以對上面儲存資訊歸類,主要分為三類
- broker 上的所有資訊,包括 broker 中的所有分割槽,broker 所有分割槽副本,當前都有哪些執行中的 broker,哪些正在關閉中的 broker 。
- 所有主題資訊,包括具體的分割槽資訊,比如領導者副本是誰,ISR 集合中有哪些副本等。
- 所有涉及運維任務的分割槽。包括當前正在進行 Preferred 領導者選舉以及分割槽重分配的分割槽列表。
Kafka 是離不開 ZooKeeper的,所以這些資料資訊在 ZooKeeper 中也儲存了一份。每當控制器初始化時,它都會從 ZooKeeper 上讀取對應的後設資料並填充到自己的快取中。
broker controller 故障轉移
我們在前面說過,第一個在 ZooKeeper 中的 /brokers/ids
下建立節點的 broker 作為 broker controller,也就是說 broker controller 只有一個,那麼必然會存在單點失效問題。kafka 為考慮到這種情況提供了故障轉移
功能,也就是 Fail Over
。如下圖
最一開始,broker1 會搶先註冊成功成為 controller,然後由於網路抖動或者其他原因致使 broker1 掉線,ZooKeeper 通過 Watch 機制覺察到 broker1 的掉線,之後所有存活的 brokers 開始競爭成為 controller,這時 broker3 搶先註冊成功,此時 ZooKeeper 儲存的 controller 資訊由 broker1 -> broker3,之後,broker3 會從 ZooKeeper 中讀取後設資料資訊,並初始化到自己的快取中。
注意:ZooKeeper 中儲存的不是快取資訊,broker 中儲存的才是快取資訊。
broker controller 存在的問題
在 Kafka 0.11 版本之前,控制器的設計是相當繁瑣的。我們上面提到過一句話:Kafka controller 被設計為一種模擬狀態機的多執行緒控制器,這種設計其實是存在一些問題的
- controller 狀態的更改由不同的監聽器並罰執行,因此需要進行很複雜的同步,並且容易出錯而且難以除錯。
- 狀態傳播不同步,broker 可能在時間不確定的情況下出現多種狀態,這會導致不必要的額外的資料丟失
- controller 控制器還會為主題刪除建立額外的 I/O 執行緒,導致效能損耗
- controller 的多執行緒設計還會訪問共享資料,我們知道,多執行緒訪問共享資料是執行緒同步最麻煩的地方,為了保護資料安全性,控制器不得不在程式碼中大量使用ReentrantLock 同步機制,這就進一步拖慢了整個控制器的處理速度。
broker controller 內部設計原理
在 Kafka 0.11 之後,Kafka controller 採用了新的設計,把多執行緒的方案改成了單執行緒加事件佇列的方案。如下圖所示
主要所做的改變有下面這幾點
第一個改進是增加了一個 Event Executor Thread
,事件執行執行緒,從圖中可以看出,不管是 Event Queue 事件佇列還是 Controller context 控制器上下文都會交給事件執行執行緒進行處理。將原來執行的操作全部建模成一個個獨立的事件,傳送到專屬的事件佇列中,供此執行緒消費。
第二個改進是將之前同步的 ZooKeeper 全部改為非同步操作
。ZooKeeper API 提供了兩種讀寫的方式:同步和非同步。之前控制器操作 ZooKeeper 都是採用的同步方式,這次把同步方式改為非同步,據測試,效率提升了10倍。
第三個改進是根據優先順序處理請求,之前的設計是 broker 會公平性的處理所有 controller 傳送的請求。什麼意思呢?公平性難道還不好嗎?在某些情況下是的,比如 broker 在排隊處理 produce 請求,這時候 controller 發出了一個 StopReplica 的請求,你會怎麼辦?還在繼續處理 produce 請求嗎?這個 produce 請求還有用嗎?此時最合理的處理順序應該是,賦予 StopReplica 請求更高的優先順序,使它能夠得到搶佔式的處理。
副本機制
複製功能是 Kafka 架構的核心功能,在 Kafka 文件裡面 Kafka 把自己描述為 一個分散式的、可分割槽的、可複製的提交日誌服務。複製之所以這麼關鍵,是因為訊息的持久儲存非常重要,這能夠保證在主節點當機後依舊能夠保證 Kafka 高可用。副本機制也可以稱為備份機制(Replication)
,通常指分散式系統在多臺網路互動的機器上儲存有相同的資料備份/拷貝。
Kafka 使用主題來組織資料,每個主題又被分為若干個分割槽,分割槽會部署在一到多個 broker 上,每個分割槽都會有多個副本,所以副本也會被儲存在 broker 上,每個 broker 可能會儲存成千上萬個副本。下圖是一個副本複製示意圖
如上圖所示,為了簡單我只畫出了兩個 broker ,每個 broker 指儲存了一個 Topic 的訊息,在 broker1 中分割槽0 是Leader,它負責進行分割槽的複製工作,把 broker1 中的分割槽0複製一個副本到 broker2 的主題 A 的分割槽0。同理,主題 A 的分割槽1也是一樣的道理。
副本型別分為兩種:一種是 Leader(領導者)
副本,一種是Follower(跟隨者)
副本。
Leader 副本
Kafka 在建立分割槽的時候都要選舉一個副本,這個選舉出來的副本就是 Leader 領導者副本。
Follower 副本
除了 Leader 副本以外的副本統稱為 Follower 副本
,Follower 不對外提供服務。下面是 Leader 副本的工作方式
這幅圖需要注意以下幾點
- Kafka 中,Follower 副本也就是追隨者副本是不對外提供服務的。這就是說,任何一個追隨者副本都不能響應消費者和生產者的請求。所有的請求都是由領導者副本來處理。或者說,所有的請求都必須傳送到 Leader 副本所在的 broker 中,Follower 副本只是用做資料拉取,採用
非同步拉取
的方式,並寫入到自己的提交日誌中,從而實現與 Leader 的同步 - 當 Leader 副本所在的 broker 當機後,Kafka 依託於 ZooKeeper 提供的監控功能能夠實時感知到,並開啟新一輪的選舉,從追隨者副本中選一個作為 Leader。如果當機的 broker 重啟完成後,該分割槽的副本會作為 Follower 重新加入。
首領的另一個任務是搞清楚哪個跟隨者的狀態與自己是一致的。跟隨者為了保證與領導者的狀態一致,在有新訊息到達之前先嚐試從領導者那裡複製訊息。為了與領導者保持一致,跟隨者向領導者發起獲取資料的請求,這種請求與消費者為了讀取訊息而傳送的資訊是一樣的。
跟隨者向領導者傳送訊息的過程是這樣的,先請求訊息1,然後再接收到訊息1,在時候到請求1之後,傳送請求2,在收到領導者給傳送給跟隨者之前,跟隨者是不會繼續傳送訊息的。這個過程如下
跟隨者副本在收到響應訊息前,是不會繼續傳送訊息,這一點很重要。通過檢視每個跟隨者請求的最新偏移量,首領就會知道每個跟隨者複製的進度。如果跟隨者在10s 內沒有請求任何訊息,或者雖然跟隨者已經傳送請求,但是在10s 內沒有收到訊息,就會被認為是不同步
的。如果一個副本沒有與領導者同步,那麼在領導者掉線後,這個副本將不會稱為領導者,因為這個副本的訊息不是全部的。
與之相反的,如果跟隨者同步的訊息和領導者副本的訊息一致,那麼這個跟隨者副本又被稱為同步的副本
。也就是說,如果領導者掉線,那麼只有同步的副本能夠稱為領導者。
關於副本機制我們說了這麼多,那麼副本機制的好處是什麼呢?
- 能夠立刻看到寫入的訊息,就是你使用生產者 API 成功向分割槽寫入訊息後,馬上使用消費者就能讀取剛才寫入的訊息
- 能夠實現訊息的冪等性,啥意思呢?就是對於生產者產生的訊息,在消費者進行消費的時候,它每次都會看到訊息存在,並不會存在訊息不存在的情況
同步複製和非同步複製
我在學習副本機制的時候,有個疑問,既然領導者副本和跟隨者副本是傳送 - 等待
機制的,這是一種同步的複製方式,那麼為什麼說跟隨者副本同步領導者副本的時候是一種非同步操作呢?
我認為是這樣的,跟隨者副本在同步領導者副本後會把訊息儲存在本地 log 中,這個時候跟隨者會給領導者副本一個響應訊息,告訴領導者自己已經儲存成功了,同步複製的領導者會等待所有的跟隨者副本都寫入成功後,再返回給 producer 寫入成功的訊息。而非同步複製是領導者副本不需要關心跟隨者副本是否寫入成功,只要領導者副本自己把訊息儲存到本地 log ,就會返回給 producer 寫入成功的訊息。下面是同步複製和非同步複製的過程
同步複製
- producer 通知 ZooKeeper 識別領導者
- producer 向領導者寫入訊息
- 領導者收到訊息後會把訊息寫入到本地 log
- 跟隨者會從領導者那裡拉取訊息
- 跟隨者向本地寫入 log
- 跟隨者向領導者傳送寫入成功的訊息
- 領導者會收到所有的跟隨者傳送的訊息
- 領導者向 producer 傳送寫入成功的訊息
非同步複製
和同步複製的區別在於,領導者在寫入本地log之後,直接向客戶端傳送寫入成功訊息,不需要等待所有跟隨者複製完成。
ISR
Kafka動態維護了一個同步狀態的副本的集合(a set of In-Sync Replicas),簡稱ISR
,ISR 也是一個很重要的概念,我們之前說過,追隨者副本不提供服務,只是定期的非同步拉取領導者副本的資料而已,拉取這個操作就相當於是複製,ctrl-c + ctrl-v
大家肯定用的熟。那麼是不是說 ISR 集合中的副本訊息的數量都會與領導者副本訊息數量一樣呢?那也不一定,判斷的依據是 broker 中引數 replica.lag.time.max.ms
的值,這個引數的含義就是跟隨者副本能夠落後領導者副本最長的時間間隔。
replica.lag.time.max.ms 引數預設的時間是 10秒,如果跟隨者副本落後領導者副本的時間不超過 10秒,那麼 Kafka 就認為領導者和跟隨者是同步的。即使此時跟隨者副本中儲存的訊息要小於領導者副本。如果跟隨者副本要落後於領導者副本 10秒以上的話,跟隨者副本就會從 ISR 被剔除。倘若該副本後面慢慢地追上了領導者的進度,那麼它是能夠重新被加回 ISR 的。這也表明,ISR 是一個動態調整的集合,而非靜態不變的。
Unclean 領導者選舉
既然 ISR 是可以動態調整的,那麼必然會出現 ISR 集合中為空的情況,由於領導者副本是一定出現在 ISR 集合中的,那麼 ISR 集合為空必然說明領導者副本也掛了,所以此時 Kafka 需要重新選舉一個新的領導者,那麼該如何選舉呢?現在你需要轉變一下思路,我們上面說 ISR 集合中一定是與領導者同步的副本,那麼不再 ISR 集合中的副本一定是不與領導者同步的副本了,也就是不再 ISR 列表中的跟隨者副本會丟失一些訊息。如果你開啟 broker 端引數 unclean.leader.election.enable
的話,下一個領導者就會在這些非同步的副本中選舉。這種選舉也叫做Unclean 領導者選舉
。
如果你接觸過分散式專案的話你一定知道 CAP 理論,那麼這種 Unclean 領導者選舉其實是犧牲了資料一致性,保證了 Kafka 的高可用性。
你可以根據你的實際業務場景決定是否開啟 Unclean 領導者選舉,一般不建議開啟這個引數,因為資料的一致性要比可用性重要的多。
Kafka 請求處理流程
broker 的大部分工作是處理客戶端、分割槽副本和控制器傳送給分割槽領導者的請求。這種請求一般都是請求/響應
式的,我猜測你接觸最早的請求/響應的方式應該就是 HTTP 請求了。事實上,HTTP 請求可以是同步可以是非同步的。一般正常的 HTTP 請求都是同步的,同步方式最大的一個特點是提交請求->等待伺服器處理->處理完畢返回 這個期間客戶端瀏覽器不能做任何事。而非同步方式最大的特點是 請求通過事件觸發->伺服器處理(這是瀏覽器仍然可以作其他事情)-> 處理完畢。
那麼我也可以說同步請求就是順序處理的,而非同步請求的執行方式則不確定,因為非同步需要建立多個執行執行緒,而每個執行緒的執行順序不同。
這裡需要注意一點,我們只是使用 HTTP 請求來舉例子,而 Kafka 採用的是 TCP 基於 Socket 的方式進行通訊
那麼這兩種方式有什麼缺點呢?
我相信聰明的你應該能馬上想到,同步的方式最大的缺點就是吞吐量太差
,資源利用率極低,由於只能順序處理請求,因此,每個請求都必須等待前一個請求處理完畢才能得到處理。這種方式只適用於請求傳送非常不頻繁的系統
。
非同步的方式的缺點就是為每個請求都建立執行緒的做法開銷極大,在某些場景下甚至會壓垮整個服務。
響應式模型
說了這麼半天,Kafka 採用同步還是非同步的呢?都不是,Kafka 採用的是一種 響應式(Reactor)模型
,那麼什麼是響應式模型呢?簡單的說,Reactor 模式是事件驅動架構的一種實現方式,特別適合應用於處理多個客戶端併發向伺服器端傳送請求的場景,如下圖所示
Kafka 的 broker 端有個 SocketServer元件,類似於處理器,SocketServer 是基於 TCP 的 Socket 連線的,它用於接受客戶端請求,所有的請求訊息都包含一個訊息頭,訊息頭中都包含如下資訊
- Request type (也就是 API Key)
- Request version(broker 可以處理不同版本的客戶端請求,並根據客戶版本做出不同的響應)
- Correlation ID --- 一個具有唯一性的數字,用於標示請求訊息,同時也會出現在響應訊息和錯誤日誌中(用於診斷問題)
- Client ID --- 用於標示傳送請求的客戶端
broker 會在它所監聽的每一個埠上執行一個 Acceptor
執行緒,這個執行緒會建立一個連線,並把它交給 Processor(網路執行緒池)
, Processor 的數量可以使用 num.network.threads
進行配置,其預設值是3,表示每臺 broker 啟動時會建立3個執行緒,專門處理客戶端傳送的請求。
Acceptor 執行緒會採用輪詢
的方式將入棧請求公平的傳送至網路執行緒池中,因此,在實際使用過程中,這些執行緒通常具有相同的機率被分配到待處理請求佇列
中,然後從響應佇列
獲取響應訊息,把它們傳送給客戶端。Processor 網路執行緒池中的請求 - 響應的處理還是比較複雜的,下面是網路執行緒池中的處理流程圖
Processor 網路執行緒池接收到客戶和其他 broker 傳送來的訊息後,網路執行緒池會把訊息放到請求佇列中,注意這個是共享請求佇列
,因為網路執行緒池是多執行緒機制的,所以請求佇列的訊息是多執行緒共享的區域,然後由 IO 執行緒池進行處理,根據訊息的種類判斷做何處理,比如 PRODUCE
請求,就會將訊息寫入到 log 日誌中,如果是FETCH
請求,則從磁碟或者頁快取中讀取訊息。也就是說,IO執行緒池是真正做判斷,處理請求的一個元件。在IO 執行緒池處理完畢後,就會判斷是放入響應佇列
中還是 Purgatory
中,Purgatory 是什麼我們下面再說,現在先說一下響應佇列,響應佇列是每個執行緒所獨有的,因為響應式模型中不會關心請求發往何處,因此把響應回傳的事情就交給每個執行緒了,所以也就不必共享了。
注意:IO 執行緒池可以通過 broker 端引數
num.io.threads
來配置,預設的執行緒數是8,表示每臺 broker 啟動後自動建立 8 個IO 處理執行緒。
請求型別
下面是幾種常見的請求型別
生產請求
我在 真的,關於 Kafka 入門看這一篇就夠了 文章中提到過 acks
這個配置項的含義
簡單來講就是不同的配置對寫入成功的界定是不同的,如果 acks = 1,那麼只要領導者收到訊息就表示寫入成功,如果acks = 0,表示只要領導者傳送訊息就表示寫入成功,根本不用考慮返回值的影響。如果 acks = all,就表示領導者需要收到所有副本的訊息後才表示寫入成功。
在訊息被寫入分割槽的首領後,如果 acks 配置的值是 all
,那麼這些請求會被儲存在 煉獄(Purgatory)
的緩衝區中,直到領導者副本發現跟隨者副本都複製了訊息,響應才會傳送給客戶端。
獲取請求
broker 獲取請求的方式與處理生產請求的方式類似,客戶端傳送請求,向 broker 請求主題分割槽中特定偏移量的訊息,如果偏移量存在,Kafka 會採用 零複製
技術向客戶端傳送訊息,Kafka 會直接把訊息從檔案中傳送到網路通道中,而不需要經過任何的緩衝區,從而獲得更好的效能。
客戶端可以設定獲取請求資料的上限和下限,上限
指的是客戶端為接受足夠訊息分配的記憶體空間,這個限制比較重要,如果上限太大的話,很有可能直接耗盡客戶端記憶體。下限
可以理解為攢足了資料包再傳送的意思,這就相當於專案經理給程式設計師分配了 10 個bug,程式設計師每次改一個 bug 就會向專案經理彙報一下,有的時候改好了有的時候可能還沒改好,這樣就增加了溝通成本和時間成本,所以下限值得就是程式設計師你改完10個 bug 再向我彙報!!!如下圖所示
如圖你可以看到,在拉取訊息
---> 訊息
之間是有一個等待訊息積累這麼一個過程的,這個訊息積累你可以把它想象成超時時間,不過超時會跑出異常,訊息積累超時後會響應回執。延遲時間可以通過 replica.lag.time.max.ms
來配置,它指定了副本在複製訊息時可被允許的最大延遲時間。
後設資料請求
生產請求和響應請求都必須傳送給領導者副本,如果 broker 收到一個針對某個特定分割槽的請求,而該請求的首領在另外一個 broker 中,那麼傳送請求的客戶端會收到非分割槽首領
的錯誤響應;如果針對某個分割槽的請求被髮送到不含有領導者的 broker 上,也會出現同樣的錯誤。Kafka 客戶端需要把請求和響應傳送到正確的 broker 上。這不是廢話麼?我怎麼知道要往哪傳送?
事實上,客戶端會使用一種 後設資料請求
,這種請求會包含客戶端感興趣的主題列表,服務端的響應訊息指明瞭主題的分割槽,領導者副本和跟隨者副本。後設資料請求可以傳送給任意一個 broker,因為所有的 broker 都會快取這些資訊。
一般情況下,客戶端會把這些資訊快取,並直接向目標 broker 傳送生產請求和相應請求,這些快取需要隔一段時間就進行重新整理,使用metadata.max.age.ms
引數來配置,從而知道後設資料是否發生了變更。比如,新的 broker 加入後,會觸發重平衡,部分副本會移動到新的 broker 上。這時候,如果客戶端收到 不是首領
的錯誤,客戶端在傳送請求之前重新整理後設資料快取。
Kafka 重平衡流程
我在 真的,關於 Kafka 入門看這一篇就夠了 中關於消費者描述的時候大致說了一下消費者組和重平衡之間的關係,實際上,歸納為一點就是讓組內所有的消費者例項就消費哪些主題分割槽達成一致。
我們知道,一個消費者組中是要有一個群組協調者(Coordinator)
的,而重平衡的流程就是由 Coordinator 的幫助下來完成的。
這裡需要先宣告一下重平衡發生的條件
- 消費者訂閱的任何主題發生變化
- 消費者數量發生變化
- 分割槽數量發生變化
- 如果你訂閱了一個還尚未建立的主題,那麼重平衡在該主題建立時發生。如果你訂閱的主題發生刪除那麼也會發生重平衡
- 消費者被群組協調器認為是
DEAD
狀態,這可能是由於消費者崩潰或者長時間處於執行狀態下發生的,這意味著在配置合理時間的範圍內,消費者沒有向群組協調器傳送任何心跳,這也會導致重平衡的發生。
在瞭解重平衡之前,你需要知道這兩個角色
群組協調器(Coordinator)
:群組協調器是一個能夠從消費者群組中收到所有消費者傳送心跳訊息的 broker。在最早期的版本中,後設資料資訊是儲存在 ZooKeeper 中的,但是目前後設資料資訊儲存到了 broker 中。每個消費者組都應該和群組中的群組協調器同步。當所有的決策要在應用程式節點中進行時,群組協調器可以滿足 JoinGroup
請求並提供有關消費者組的後設資料資訊,例如分配和偏移量。群組協調器還有權知道所有消費者的心跳,消費者群組中還有一個角色就是領導者,注意把它和領導者副本和 kafka controller 進行區分。領導者是群組中負責決策的角色,所以如果領導者掉線了,群組協調器有權把所有消費者踢出組。因此,消費者群組的一個很重要的行為是選舉領導者,並與協調器讀取和寫入有關分配和分割槽的後設資料資訊。
消費者領導者
: 每個消費者群組中都有一個領導者。如果消費者停止傳送心跳了,協調者會觸發重平衡。
在瞭解重平衡之前,你需要知道狀態機是什麼
Kafka 設計了一套消費者組狀態機(State Machine)
,來幫助協調者完成整個重平衡流程。消費者狀態機主要有五種狀態它們分別是 Empty、Dead、PreparingRebalance、CompletingRebalance 和 Stable。
瞭解了這些狀態的含義之後,下面我們用幾條路徑來表示一下消費者狀態的輪轉
消費者組一開始處於 Empty
狀態,當重平衡開啟後,它會被置於 PreparingRebalance
狀態等待新消費者的加入,一旦有新的消費者加入後,消費者群組就會處於 CompletingRebalance
狀態等待分配,只要有新的消費者加入群組或者離開,就會觸發重平衡,消費者的狀態處於 PreparingRebalance 狀態。等待分配機制指定好後完成分配,那麼它的流程圖是這樣的
在上圖的基礎上,當消費者群組都到達 Stable
狀態後,一旦有新的消費者加入/離開/心跳過期,那麼觸發重平衡,消費者群組的狀態重新處於 PreparingRebalance 狀態。那麼它的流程圖是這樣的。
在上圖的基礎上,消費者群組處於 PreparingRebalance 狀態後,很不幸,沒人玩兒了,所有消費者都離開了,這時候還可能會保留有消費者消費的位移資料,一旦位移資料過期或者被重新整理,那麼消費者群組就處於 Dead
狀態了。它的流程圖是這樣的
在上圖的基礎上,我們分析了消費者的重平衡,在 PreparingRebalance
或者 CompletingRebalance
或者 Stable
任意一種狀態下發生位移主題分割槽 Leader 發生變更,群組會直接處於 Dead 狀態,它的所有路徑如下
這裡面需要注意兩點:
一般出現 Required xx expired offsets in xxx milliseconds 就表明Kafka 很可能就把該組的位移資料刪除了
只有 Empty 狀態下的組,才會執行過期位移刪除的操作。
重平衡流程
上面我們瞭解到了消費者群組狀態的轉化過程,下面我們真正開始介紹 Rebalance
的過程。重平衡過程可以從兩個方面去看:消費者端和協調者端,首先我們先看一下消費者端
從消費者看重平衡
從消費者看重平衡有兩個步驟:分別是 消費者加入組
和 等待領導者分配方案
。這兩個步驟後分別對應的請求是 JoinGroup
和 SyncGroup
。
新的消費者加入群組時,這個消費者會向協調器傳送 JoinGroup
請求。在該請求中,每個消費者成員都需要將自己消費的 topic 進行提交,我們上面描述群組協調器中說過,這麼做的目的就是為了讓協調器收集足夠的後設資料資訊,來選取消費者組的領導者。通常情況下,第一個傳送 JoinGroup 請求的消費者會自動稱為領導者。領導者的任務是收集所有成員的訂閱資訊,然後根據這些資訊,制定具體的分割槽消費分配方案。如圖
在所有的消費者都加入進來並把後設資料資訊提交給領導者後,領導者做出分配方案併傳送 SyncGroup
請求給協調者,協調者負責下發群組中的消費策略。下圖描述了 SyncGroup 請求的過程
當所有成員都成功接收到分配方案後,消費者組進入到 Stable 狀態,即開始正常的消費工作。
從協調者來看重平衡
從協調者角度來看重平衡主要有下面這幾種觸發條件,
- 新成員加入組
- 組成員主動離開
- 組成員崩潰離開
- 組成員提交位移
我們分別來描述一下,先從新成員加入組開始
####新成員加入組
我們討論的場景消費者叢集狀態處於Stable
等待分配的過程,這時候如果有新的成員加入組的話,重平衡的過程
從這個角度來看,協調者的過程和消費者類似,只是剛剛從消費者的角度去看,現在從領導者的角度去看
組成員離開
組成員離開消費者群組指的是消費者例項呼叫 close()
方法主動通知協調者它要退出。這裡又會有一個新的請求出現 LeaveGroup()請求
。如下圖所示
組成員崩潰
組成員崩潰是指消費者例項出現嚴重故障,當機或者一段時間未響應,協調者接收不到消費者的心跳,就會被認為是組成員崩潰
,崩潰離組是被動的,協調者通常需要等待一段時間才能感知到,這段時間一般是由消費者端引數 session.timeout.ms 控制的。如下圖所示
重平衡時提交位移
這個過程我們就不再用圖形來表示了,大致描述一下就是 消費者傳送 JoinGroup 請求後,群組中的消費者必須在指定的時間範圍內提交各自的位移,然後再開啟正常的 JoinGroup/SyncGroup 請求傳送。
如果大家認可我,請幫我點個贊,謝謝各位了。我們下篇技術文章見。
文章參考:
《Kafka 權威指南》
learning.oreilly.com/library/vie…
《極客時間-Kafka核心技術與實戰》