[TOC]
控制器組件(Controller),是 Apache Kafka 的核心組件。它的主要作用是在 Apache ZooKeeper 的幫助下管理和協調整個 Kafka 集群。集群中任意一臺 Broker 都能充當控制器的角色,但是,在運行過程中,只能有一個 Broker 成為控制器,行使其管理和協調的職責。接下來,我們將討論Controller原理和內部運行機制。
什么是Controller Broker
在分布式系統中,通常需要有一個協調者,該協調者會在分布式系統發生異常時發揮特殊的作用。在Kafka中該協調者稱之為控制器(Controller),其實該控制器并沒有什么特殊之處,它本身也是一個普通的Broker,只不過需要負責一些額外的工作(追蹤集群中的其他Broker,并在合適的時候處理新加入的和失敗的Broker節點、Rebalance分區、分配新的leader分區等)。值得注意的是:Kafka集群中始終只有一個Controller Broker。
- 控制器(Controller)是Kafka的核心組件,主要作用是在ZK的幫助下管理和協調整個Kafka集群
- 集群中任一Broker都能充當控制器的角色,但在運行過程中,只能有一個Broker成為控制器,行使管理和協調的職責
[zk: localhost:2181(CONNECTED) 1] get /controller
{"version":1,"brokerid":0,"timestamp":"1571311742367"}
cZxid = 0xd68
ctime = Thu Oct 17 19:29:02 CST 2019
mZxid = 0xd68
mtime = Thu Oct 17 19:29:02 CST 2019
pZxid = 0xd68
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x1000209974b0000
dataLength = 54
numChildren = 0
zookeeper
- Kafka控制器重度依賴ZK
- ZK是一個提供高可靠性的分布式協調服務框架
- ZK使用類似于文件系統的樹形結構,根目錄以/開始,結構上的每個節點稱為znode,用來保存一些元數據協調信息
- 如果以znode的持久性來劃分,znode可以分為持久性znode和臨時znode
持久性znode不會因為ZK集群重啟而消失
臨時znode則會與創建該znode的ZK會話綁定,一旦會話結束,該節點會被自動刪除 - ZK賦予客戶端監控znode變更的能力,即所謂的Watch通知功能
- 一旦znode節點被創建、刪除、子節點數量發生變化,znode所存的數據本身發生變更
- ZK會通過節點變更監聽器(ChangeHandler)的方式顯式通知客戶端
- ZK被用來實現集群成員管理、分布式鎖、領導者選舉等功能,Kafka控制器大量使用Watch功能實現對集群的協調管理
/controller節點
- Broker在啟動時,會嘗試去ZK創建/controller節點
- 第一個成功創建/controller節點的Broker會被指定為為控制器
控制器的職責
-
主題管理
- 完成對Kafka主題的創建、刪除以及分區增加的操作
- 執行kafka-topics時,大部分的后臺工作都是由控制器完成的
分區重分配
分區重分配主要是指kafka-reassign-partitions腳本提供的對已有主題分區進行細粒度的分配功能Preferred領導者選舉
Preferred領導者選舉主要是Kafka為了避免部分Broker負載過重而提供的一種換Leader的方案-
集群成員管理
- 自動檢測新增Broker、Broker主動關閉、Broker宕機
- 自動檢測依賴于Watch功能和ZK臨時節點組合實現的
- 控制器會利用Watch機制檢查ZK的/brokers/ids節點下的子節點數量變更
- 當有新Broker啟動后,它會在/brokers/ids/下創建專屬的臨時znode節點
- 一旦創建完畢,ZK會通過Watch機制將消息通知推送給控制器,控制器能夠自動感知這個變化當Broker宕機或者主動關閉后,該Broker與ZK的會話結束,這個znode會被自動刪除
- 當Broker宕機或者主動關閉后,該Broker與ZK的會話結束,這個znode會被自動刪除, ZK的Watch機制會將這一變更推送給控制器
數據服務
- 向其它Broker提供數據服務,控制器上保存了最全的集群元數據信息
- 其它Broker會定期接收控制器發來的元數據更新請求,從而更新其內存中的緩存數據
Controller Broker是如何被選出來的
上一小節解釋了什么是Controller Broker,并且每臺 Broker 都有充當控制器的可能性。那么,控制器是如何被選出來的呢?當集群啟動后,Kafka 怎么確認控制器位于哪臺 Broker 呢?
實際上,Broker 在啟動時,會嘗試去 ZooKeeper 中創建 /controller 節點。Kafka 當前選舉控制器的規則是:第一個成功創建 /controller 節點的 Broker 會被指定為控制器。
Controller Broker的具體作用是什么
Controller Broker的主要職責有很多,主要是一些管理行為,主要包括以下幾個方面:
- 創建、刪除主題,增加分區并分配leader分區
- 集群Broker管理(新增 Broker、Broker 主動關閉、Broker 故障)
- preferred leader選舉
- 分區重分配
處理集群中下線的Broker
當某個Broker節點由于故障離開Kafka群集時,則存在于該Broker的leader分區將不可用(由于客戶端僅對leader分區進行讀寫操作)。為了最大程度地減少停機時間,需要快速找到替代的leader分區。
Controller Broker可以對失敗的Broker做出響應,Controller Broker可以從Zookeeper監聽(zookeeper watch)中獲取通知信息,ZooKeeper 賦予客戶端監控 znode 變更的能力,即所謂的 Watch 通知功能。一旦 znode 節點被創建、刪除,子節點數量發生變化,抑或是 znode 所存的數據本身變更,ZooKeeper 會通過節點變更監聽器 (ChangeHandler) 的方式顯式通知客戶端。
每個 Broker 啟動后,會在zookeeper的 /Brokers/ids 下創建一個臨時 znode。當 Broker 宕機或主動關閉后,該 Broker 與 ZooKeeper 的會話結束,這個 znode 會被自動刪除。同理,ZooKeeper 的 Watch 機制將這一變更推送給控制器,這樣控制器就能知道有 Broker 關閉或宕機了,從而進行后續的協調操作。
Controller將收到通知并對此采取行動,決定哪些Broker上的分區成為leader分區,然后,它會通知每個相關的Broker,要么將Broker上的主題分區變成leader,要么通過LeaderAndIsr請求從新的leader分區中復制數據。
處理新加入到集群中的Broker
通過將Leader分區副本均勻地分布在集群的不同Broker上,可以保障集群的負載均衡。在Broker發生故障時,某些Broker上的分區副本會被選舉為leader,會造成一個Broker上存在多個leader分區副本的情況,由于客戶端只與leader分區副本交互,所以這會給Broker增加額外的負擔,并損害集群的性能和運行狀況。因此,盡快恢復平衡對集群的健康運行是有益的。
Kafka認為leader分區副本最初的分配(每個節點都處于活躍狀態)是均衡的。這些被最初選中的分區副本就是所謂的首選領導者(preferred leaders)。由于Kafka還支持機架感知的leader選舉(rack-aware leader election) ,即嘗試將leader分區和follower分區放置在不同的機架上,以增加對機架故障的容錯能力。因此,leader分區副本的存在位置會對集群的可靠性產生影響。
默認情況下auto.leader.rebalance.enabled為true,表示允許 Kafka 定期地對一些 Topic 分區進行
Leader 重選舉。大部分情況下,Broker的失敗很短暫,這意味著Broker通常會在短時間內恢復。所以當節點離開群集時,與其相關聯的元數據并不會被立即刪除。
當Controller注意到Broker已加入集群時,它將使用Broker ID來檢查該Broker上是否存在分區,如果存在,則Controller通知新加入的Broker和現有的Broker,新的Broker上面的follower分區再次開始復制現有leader分區的消息。為了保證負載均衡,Controller會將新加入的Broker上的follower分區選舉為leader分區。
注意:上面提到的選Leader分區,嚴格意義上是換Leader分區,為了達到負載均衡,可能會造成原來正常的Leader分區被強行變為follower分區。換一次 Leader 代價是很高的,原本向 Leader分區A(原Leader分區) 發送請求的所有客戶端都要切換成向 B (新的Leader分區)發送請求,建議你在生產環境中把這個參數設置成 false。
同步副本(in-sync replica ,ISR)列表
SR中的副本都是與Leader進行同步的副本,所以不在該列表的follower會被認為與Leader是不同步的. 那么,ISR中存在是什么副本呢?首先可以明確的是:Leader副本總是存在于ISR中。 而follower副本是否在ISR中,取決于該follower副本是否與Leader副本保持了“同步”。
始終保證擁有足夠數量的同步副本是非常重要的。要將follower提升為Leader,它必須存在于同步副本列表中。每個分區都有一個同步副本列表,該列表由Leader分區和Controller進行更新。
選擇一個同步副本列表中的分區作為leader 分區的過程稱為clean leader election。注意,這里要與在非同步副本中選一個分區作為leader分區的過程區分開,在非同步副本中選一個分區作為leader的過程稱之為unclean leader election。由于ISR是動態調整的,所以會存在ISR列表為空的情況,通常來說,非同步副本落后 Leader 太多,因此,如果選擇這些副本作為新 Leader,就可能出現數據的丟失。畢竟,這些副本中保存的消息遠遠落后于老 Leader 中的消息。在 Kafka 中,選舉這種副本的過程可以通過Broker 端參數 *unclean.leader.election.enable *控制是否允許 Unclean 領導者選舉。開啟 Unclean 領導者選舉可能會造成數據丟失,但好處是,它使得分區 Leader 副本一直存在,不至于停止對外提供服務,因此提升了高可用性。反之,禁止 Unclean Leader 選舉的好處在于維護了數據的一致性,避免了消息丟失,但犧牲了高可用性。分布式系統的CAP理論說的就是這種情況。
腦裂
如果controller Broker 掛掉了,Kafka集群必須找到可以替代的controller,集群將不能正常運轉。這里面存在一個問題,很難確定Broker是掛掉了,還是僅僅只是短暫性的故障。但是,集群為了正常運轉,必須選出新的controller。如果之前被取代的controller又正常了,他并不知道自己已經被取代了,那么此時集群中會出現兩臺controller。
其實這種情況是很容易發生。比如,某個controller由于GC而被認為已經掛掉,并選擇了一個新的controller。在GC的情況下,在最初的controller眼中,并沒有改變任何東西,該Broker甚至不知道它已經暫停了。因此,它將繼續充當當前controller,這是分布式系統中的常見情況,稱為腦裂。
假如,處于活躍狀態的controller進入了長時間的GC暫停。它的ZooKeeper會話過期了,之前注冊的/controller節點被刪除。集群中其他Broker會收到zookeeper的這一通知。
由于集群中必須存在一個controller Broker,所以現在每個Broker都試圖嘗試成為新的controller。假設Broker 2速度比較快,成為了最新的controller Broker。此時,每個Broker會收到Broker2成為新的controller的通知,由于Broker3正在進行”stop the world”的GC,可能不會收到Broker2成為最新的controller的通知。
等到Broker3的GC完成之后,仍會認為自己是集群的controller,在Broker3的眼中好像什么都沒有發生一樣。
現在,集群中出現了兩個controller,它們可能一起發出具有沖突的命令,就會出現腦裂的現象。如果對這種情況不加以處理,可能會導致嚴重的不一致。所以需要一種方法來區分誰是集群當前最新的Controller。
Kafka是通過使用epoch number(紀元編號,也稱為隔離令牌)來完成的。epoch number只是單調遞增的數字,第一次選出Controller時,epoch number值為1,如果再次選出新的Controller,則epoch number將為2,依次單調遞增。
每個新選出的controller通過Zookeeper 的條件遞增操作獲得一個全新的、數值更大的epoch number 。其他Broker 在知道當前epoch number 后,如果收到由controller發出的包含較舊(較小)epoch number的消息,就會忽略它們,即Broker根據最大的epoch number來區分當前最新的controller。
上圖,Broker3向Broker1發出命令:讓Broker1上的某個分區副本成為leader,該消息的epoch number值為1。于此同時,Broker2也向Broker1發送了相同的命令,不同的是,該消息的epoch number值為2,此時Broker1只聽從Broker2的命令(由于其epoch number較大),會忽略Broker3的命令,從而避免腦裂的發生。
控制器內部設計原理
在Kafka 0.11之前,控制器的設計是相當繁瑣的,導致很多Bug無法修復
-
控制器是多線程的設計,會在內部創建很多個線程
- 控制器需要為每個Broker都創建一個對應的Socket連接,然后再創建一個專屬的線程,用于向這些Broker發送請求
- 控制器連接ZK的會話,也會創建單獨的線程來處理Watch機制的通知回調
- 控制器還會為主題刪除創建額外的IO線程
這些線程還會訪問共享的控制器緩存數據,多線程訪問共享可變數據是維持線程安全的最大難題, 為了保護數據安全性,控制器在代碼中大量使用ReentrantLock同步機制,進一步拖慢整個控制器的處理速度
-
社區在0.11版本重構了控制器的底層設計,把多線程的方案改成了單線程+事件隊列的方案
- 引進了事件處理器,統一處理各種控制器事件
- 控制器將原來執行的操作全部建模成獨立的事件,發送到專屬的事件隊列中,供事件處理器消費
- 單線程:控制器只是把緩存狀態變更方面的工作委托給了這個線程而已
- 優點:控制器緩存中保存的狀態只被一個線程處理,因此不需要重量級的線程同步機制來維護線程安全
-
針對控制器的第二個改進:將之前同步操作ZK全部換成異步操作
- ZK本身的API提供了同步寫和異步寫兩種方式
- 之前控制器操作ZK時使用的是同步API,性能很差
- 當有大量主題分區發生變更時,ZK容易成為系統的瓶頸
-
Kafka從2.2開始,將控制器發送的請求和普通數據類的請求分開,實現控制器請求單獨處理的邏輯
- 之前Broker對接收到的所有請求都一視同仁,不會區別對待
- 如果刪除了某個主題,那么控制器會給主題的所有副本所在的Broker發送StopReplica請求
- 如果此時Broker上有大量Produce請求堆積,那么StopReplica請求只能排隊
- 既然主題都要被刪除了,繼續處理Produce請求就顯得很沒有意義
小結
Kafka Controller,它其實就是一個普通的Broker,除了需要負責一些額外的工作之外,其角色與其他的Broker基本一樣。另外還介紹了Kafka Controller的主要職責,并對其中的一些職責進行了詳細解釋,最后還說明了kafka是如何避免腦裂的。