Kafka實戰寶典:Kafka的控制器controller詳解

WindyQin發表於2020-09-21

一、控制器簡介

控制器元件(Controller),是 Apache Kafka 的核心元件。它的主要作用是在 Apache ZooKeeper 的幫助下管理和協調整個 Kafka 叢集。叢集中任意一臺 Broker 都能充當控制器的角色,但是,在執行過程中,只能有一個 Broker 成為控制器,行使其管理和協調的職責。換句話說,每個正常運轉的 Kafka 叢集,在任意時刻都有且只有一個控制器。官網上有個名為 activeController 的 JMX 指標,可以幫助我們實時監控控制器的存活狀態。這個 JMX 指標非常關鍵,你在實際運維操作過程中,一定要實時檢視這個指標的值。下面,我們就來詳細說說控制器的原理和內部執行機制。

二、控制器的原理和內部執行機制

ZooKeeper介紹

     在開始之前,我先簡單介紹一下 Apache ZooKeeper 框架。要知道,控制器是重度依賴 ZooKeeper 的,因此,我們有必要花一些時間學習下 ZooKeeper 是做什麼的。Apache ZooKeeper 是一個提供高可靠性的分散式協調服務框架。它使用的資料模型類似於檔案系統的樹形結構,根目錄也是以“/”開始。該結構上的每個節點被稱為 znode,用來儲存一些後設資料協調資訊。如果以 znode 永續性來劃分,znode 可分為永續性 znode 和臨時 znode。永續性 znode 不會因為 ZooKeeper 叢集重啟而消失,而臨時 znode 則與建立該 znode 的 ZooKeeper 會話繫結,一旦會話結束,該節點會被自動刪除。
     ZooKeeper 賦予客戶端監控 znode 變更的能力,即所謂的 Watch 通知功能。一旦 znode 節點被建立、刪除,子節點數量發生變化,抑或是 znode 所存的資料本身變更,ZooKeeper 會通過節點變更監聽器 (ChangeHandler) 的方式顯式通知客戶端。
    依託於這些功能,ZooKeeper 常被用來實現叢集成員管理、分散式鎖、領導者選舉等功能。Kafka 控制器大量使用 Watch 功能實現對叢集的協調管理。我們一起來看一張圖片,它展示的是 Kafka 在 ZooKeeper 中建立的 znode 分佈。你不用瞭解每個 znode 的作用,但你可以大致體會下 Kafka 對 ZooKeeper 的依賴。

Kafka實戰寶典:Kafka的控制器controller詳解

控制器是如何被選出來的

    你一定很想知道,控制器是如何被選出來的呢?我們剛剛在前面說過,每臺 Broker 都能充當控制器,那麼,當叢集啟動後,Kafka 怎麼確認控制器位於哪臺 Broker 呢?
實際上,Broker 在啟動時,會嘗試去 ZooKeeper 中建立 /controller 節點。Kafka 當前選舉控制器的規則是:第一個成功建立 /controller 節點的 Broker 會被指定為控制器。

控制器是做什麼的

    我們經常說,控制器是起協調作用的元件,那麼,這裡的協調作用到底是指什麼呢?我想了一下,控制器的職責大致可以分為 5 種,我們一起來看看。

1.主題管理(建立、刪除、增加分割槽)

    這裡的主題管理,就是指控制器幫助我們完成對 Kafka 主題的建立、刪除以及分割槽增加的操作。換句話說,當我們執行kafka-topics 指令碼時,大部分的後臺工作都是控制器來完成的。關於 kafka-topics 指令碼,我會在專欄後面的內容中,詳細介紹它的使用方法。

2.分割槽重分配

     分割槽重分配主要是指,kafka-reassign-partitions 指令碼(關於這個指令碼,後面我也會介紹)提供的對已有主題分割槽進行細粒度的分配功能。這部分功能也是控制器實現的。

3.Preferred 領導者選舉

    Preferred 領導者選舉主要是 Kafka 為了避免部分 Broker 負載過重而提供的一種換 Leader 的方案。在專欄後面說到工具的時候,我們再詳談 Preferred 領導者選舉,這裡你只需要瞭解這也是控制器的職責範圍就可以了。

4.叢集成員管理

    這是控制器提供的第 4 類功能,包括自動檢測新增 Broker、Broker 主動關閉及被動當機。這種自動檢測是依賴於前面提到的 Watch 功能和 ZooKeeper 臨時節點組合實現的。比如,控制器元件會利用Watch 機制檢查 ZooKeeper 的 /brokers/ids 節點下的子節點數量變更。目前,當有新 Broker 啟動後,它會在 /brokers 下建立專屬的 znode 節點。一旦建立完畢,ZooKeeper 會通過 Watch 機制將訊息通知推送給控制器,這樣,控制器就能自動地感知到這個變化,進而開啟後續的新增 Broker 作業。
    偵測 Broker 存活性則是依賴於剛剛提到的另一個機制:臨時節點。每個 Broker 啟動後,會在 /brokers/ids 下建立一個臨時 znode。當 Broker 當機或主動關閉後,該 Broker 與 ZooKeeper 的會話結束,這個 znode 會被自動刪除。同理,ZooKeeper 的 Watch 機制將這一變更推送給控制器,這樣控制器就能知道有 Broker 關閉或當機了,從而進行“善後”。

5.資料服務

    控制器的最後一大類工作,就是向其他 Broker 提供資料服務。控制器上儲存了最全的叢集後設資料資訊,其他所有 Broker 會定期接收控制器發來的後設資料更新請求,從而更新其記憶體中的快取資料。

控制器儲存了什麼資料 

Kafka實戰寶典:Kafka的控制器controller詳解
圖中幾乎把我們能想到的所有 Kafka 叢集的資料都囊括進來了。這裡面比較重要的資料有:
    所有主題資訊。包括具體的分割槽資訊,比如領導者副本是誰,ISR 集合中有哪些副本等。
    所有 Broker 資訊。包括當前都有哪些執行中的 Broker,哪些正在關閉中的 Broker 等。
    所有涉及運維任務的分割槽。包括當前正在進行 Preferred 領導者選舉以及分割槽重分配的分割槽列表。

    值得注意的是,這些資料其實在 ZooKeeper 中也儲存了一份。每當控制器初始化時,它都會從 ZooKeeper 上讀取對應的後設資料並填充到自己的快取中。有了這些資料,控制器就能對外提供資料服務了。這裡的對外主要是指對其他 Broker 而言,控制器通過向這些 Broker 傳送請求的方式將這些資料同步到其他 Broker 上。

控制器故障轉移

    我們在前面強調過,在 Kafka 叢集執行過程中,只能有一臺 Broker 充當控制器的角色,那麼這就存在單點失效(Single Point of Failure)的風險,Kafka 是如何應對單點失效的呢?答案就是,為控制器提供故障轉移功能,也就是說所謂的 Failover。
    故障轉移指的是,當執行中的控制器突然當機或意外終止時,Kafka 能夠快速地感知到,並立即啟用備用控制器來代替之前失敗的控制器。這個過程就被稱為 Failover,該過程是自動完成的,無需你手動干預。
Kafka實戰寶典:Kafka的控制器controller詳解
    最開始時,Broker 0 是控制器。當 Broker 0 當機後,ZooKeeper 通過 Watch 機制感知到並刪除了 /controller 臨時節點。之後,所有存活的 Broker 開始競選新的控制器身份。Broker 3 最終贏得了選舉,成功地在 ZooKeeper 上重建了 /controller 節點。之後,Broker 3 會從 ZooKeeper 中讀取叢集後設資料資訊,並初始化到自己的快取中。至此,控制器的 Failover 完成,可以行使正常的工作職責了。

控制器內部設計原理

    在 Kafka 0.11 版本之前,控制器的設計是相當繁瑣的,程式碼更是有些混亂,這就導致社群中很多控制器方面的 Bug 都無法修復。控制器是多執行緒的設計,會在內部建立很多個執行緒。比如,控制器需要為每個 Broker 都建立一個對應的 Socket 連線,然後再建立一個專屬的執行緒,用於向這些 Broker 傳送特定請求。如果叢集中的 Broker 數量很多,那麼控制器端需要建立的執行緒就會很多。另外,控制器連線 ZooKeeper 的會話,也會建立單獨的執行緒來處理 Watch 機制的通知回撥。除了以上這些執行緒,控制器還會為主題刪除建立額外的 I/O 執行緒。
    比起多執行緒的設計,更糟糕的是,這些執行緒還會訪問共享的控制器快取資料。我們都知道,多執行緒訪問共享可變資料是維持執行緒安全最大的難題。為了保護資料安全性,控制器不得不在程式碼中大量使用ReentrantLock 同步機制,這就進一步拖慢了整個控制器的處理速度。
    鑑於這些原因,社群於 0.11 版本重構了控制器的底層設計,最大的改進就是,把多執行緒的方案改成了單執行緒加事件佇列的方案。我直接使用社群的一張圖來說明。

Kafka實戰寶典:Kafka的控制器controller詳解
    從這張圖中,我們可以看到,社群引入了一個事件處理執行緒,統一處理各種控制器事件,然後控制器將原來執行的操作全部建模成一個個獨立的事件,傳送到專屬的事件佇列中,供此執行緒消費。這就是所謂的單執行緒 + 佇列的實現方式。
值得注意的是,這裡的單執行緒不代表之前提到的所有執行緒都被“幹掉”了,控制器只是把快取狀態變更方面的工作委託給了這個執行緒而已。
    這個方案的最大好處在於,控制器快取中儲存的狀態只被一個執行緒處理,因此不再需要重量級的執行緒同步機制來維護執行緒安全,Kafka 不用再擔心多執行緒併發訪問的問題,非常利於社群定位和診斷控制器的各種問題。事實上,自 0.11 版本重構控制器程式碼後,社群關於控制器方面的 Bug 明顯少多了,這也說明了這種方案是有效的。
    針對控制器的第二個改進就是,將之前同步操作 ZooKeeper 全部改為非同步操作。ZooKeeper 本身的 API 提供了同步寫和非同步寫兩種方式。之前控制器操作 ZooKeeper 使用的是同步的 API,效能很差,集中表現為,當有大量主題分割槽發生變更時,ZooKeeper 容易成為系統的瓶頸。新版本 Kafka 修改了這部分設計,完全摒棄了之前的同步 API 呼叫,轉而採用非同步 API 寫入 ZooKeeper,效能有了很大的提升。根據社群的測試,改成非同步之後,ZooKeeper 寫入提升了 10 倍!

三、社群工作

    除了以上這些,社群最近又釋出了一個重大的改進!之前 Broker 對接收的所有請求都是一視同仁的,不會區別對待。這種設計對於控制器傳送的請求非常不公平,因為這類請求應該有更高的優先順序。
    舉個簡單的例子,假設我們刪除了某個主題,那麼控制器就會給該主題所有副本所在的 Broker 傳送一個名為StopReplica的請求。如果此時 Broker 上存有大量積壓的 Produce 請求,那麼這個 StopReplica 請求只能排隊等。如果這些 Produce 請求就是要向該主題傳送訊息的話,這就顯得很諷刺了:主題都要被刪除了,處理這些 Produce 請求還有意義嗎?此時最合理的處理順序應該是,賦予 StopReplica 請求更高的優先順序,使它能夠得到搶佔式的處理。
    這在 2.2 版本之前是做不到的。不過自 2.2 開始,Kafka 正式支援這種不同優先順序請求的處理。簡單來說,Kafka 將控制器傳送的請求與普通資料類請求分開,實現了控制器請求單獨處理的邏輯。鑑於這個改進還是很新的功能,具體的效果我們就拭目以待吧。
    當你覺得控制器元件出現問題時,比如主題無法刪除了,或者重分割槽 hang 住了,你不用重啟 Kafka Broker 或控制器。有一個簡單快速的方式是,去 ZooKeeper 中手動刪除 /controller 節點。具體命令是 rmr /controller。這樣做的好處是,既可以引發控制器的重選舉,又可以避免重啟 Broker 導致的訊息處理中斷。

 

相關文章