聊聊 Apache Kafka 移除 ZK Proposals
Zookeeper 和 KRaft
這里有一篇 Kafka 功能改進(jìn)的 proposal 原文。要了解移除 ZK 的原因,可以仔細(xì)看看該文章。以下是對該文章的翻譯。
動機
目前,Kafka 使用 Zookeeper 保存與分區(qū)(patitions)、brokers 相關(guān)的元數(shù)據(jù),以及選舉 Kafka 控制器(某個 broker)。我們將移除對 Zookeeper 的依賴。如此一來,Kafka 在管理元數(shù)據(jù)方面,將獲得更好的可擴展性和魯棒性,同時支持更多的分區(qū)。在部署、配置 Kafka 方面,也將得到極大的簡化。
將元數(shù)據(jù)視為 Event Log
我們常說將狀態(tài)做為事件流管理的好處。一個在流中描述消費者位置的數(shù)字:offset。消費者通過回放 offset 之后的事件,就能獲取最新狀態(tài)。日志建立一套清晰、有序的事件機制,并確保每個消費者能獲取到自己的時間線。
雖然我們的用戶享受這些便利,但是忽略了 Kafka 本身。我們將作用到元數(shù)據(jù)的變更看作彼此孤立,互不相干。當(dāng)控制器將狀態(tài)變更通知到集群中的其他 broker 時,其他 broker 可能會收到一些變更,但不是全部變更。雖然控制器會重試幾次,但最終會停止重試。這將導(dǎo)致 broker 之間處于不同步的狀態(tài)。
更糟糕的是,雖然 Zookeeper 存儲 record,但是 Zookeeper 中保存的狀態(tài)經(jīng)常與控制器保存在內(nèi)存中的狀態(tài)無法匹配。例如,當(dāng)分區(qū) leader 在 Zookeeper 中變更其 ISR(in-sync Replica)時,通常情況下,控制器會延誤幾秒鐘才能獲知其變更。對于控制器來說,沒有通用的方法追蹤 Zookeeper 的 event log。雖然控制器可以設(shè)置一次性守衛(wèi),但是守衛(wèi)的數(shù)量由于性能問題會受到限制。當(dāng)觸發(fā)守衛(wèi)時,守衛(wèi)不負(fù)責(zé)通知控制器當(dāng)前狀態(tài),僅僅是通知控制器狀態(tài)發(fā)生了變更。同時,控制器重讀 znode,然后設(shè)置一個新的守衛(wèi),但是,從最初守衛(wèi)發(fā)出通知,到控制器完成重讀,重新設(shè)置守衛(wèi)期間,狀態(tài)可能已經(jīng)產(chǎn)生了新的變更。如果不設(shè)置守衛(wèi),控制器將永遠(yuǎn)無法得知變更。某些情況下,重啟控制器是解決狀態(tài)不一致的唯一手段。
元數(shù)據(jù)與其存儲在獨立的系統(tǒng)中,不如存儲在 Kafka 中。這種情況下,控制器狀態(tài)與 Zookeeper 狀態(tài)之間和差異相關(guān)的問題將不復(fù)存在。與其挨個通知 broker,不如讓 broker 們從 event log 中消費元數(shù)據(jù)事件。這樣就確保了元數(shù)據(jù)變更能夠按相同的順序同步到 broker 中。broker 將元數(shù)據(jù)存儲在本地文件中。當(dāng)這些 broker 啟動時,它們只需要從控制器中(某個 broker)中讀取變更,而無需全量讀取狀態(tài)。在這種情況下,我們消耗更少的 CPU 資源就能獲得更多分區(qū)。
簡化部署與配置
Zookeeper 是一套獨立的系統(tǒng),有其配置文件語法,管理工具以及部署模式。這意味著系統(tǒng)管理員為了部署 Kafka,需要學(xué)習(xí)如何管理和部署兩套獨立的分布式系統(tǒng)。這對系統(tǒng)管理員來說,是非常艱巨的任務(wù),尤其是在他們不熟悉部署 Java 服務(wù)的情況下。統(tǒng)一系統(tǒng)將極大地改善運行 Kafka 的初次體驗,并有助于拓寬其應(yīng)用范圍。
由于 Kafka 和 Zookeeper 的配置文件是分離的,因此極易產(chǎn)生錯誤。例如,管理員在 Kafka 中設(shè)置了 SASL(Simple Authentication Security Layer,簡單認(rèn)證安全層),并且錯誤的認(rèn)為對所有在網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù)都做了加密。事實上,還需要在外部系統(tǒng) Zookeeper 中配置加密。統(tǒng)一兩個系統(tǒng)將獲得完整的加密配置模型。
最后,未來我們可能需要支持單節(jié)點 Kafka 模型。對于那些要測試 Kafka 功能的人來說,無需啟動守護進(jìn)程,將提供極大的便利性。移除 Zookeeper 依賴,將使其成為可能。
架構(gòu)
介紹
本 KIP(Kafka Improvement Proposal,Kafka 改進(jìn) Proposal) 展現(xiàn)的是一個可擴展的后 Zookeeper 時代的 Kafka 系統(tǒng)的總體愿景。為了突出重要部分,我忽略了大多數(shù)細(xì)節(jié),比如 RPC 格式、磁盤格式等等。在后續(xù) KIP 中,我們將逐步深入描述細(xì)節(jié)。與 KIP-4 類似,提出總體愿景,后續(xù)的 KIP 中逐步擴充。
總覽
目前,一套 Kafka 集群包括幾個 broker 節(jié)點,Zookeeper 節(jié)點做為一套外部 quorum (投票機制,少數(shù)服從多數(shù))。我們畫了 4 個 broker 節(jié)點和 3 個 Zookeeper 節(jié)點。這是小集群所需的正常配置。控制器(用橙色標(biāo)識)在被選舉后,從 Zoopeeper 的 quorum 中加載其狀態(tài)。從控制器連接其他節(jié)點的線,在 broker 中代表更新控制器推送的消息,比如 LeaderAndIsr、UpdateMetadata 消息。
注意,這張圖有誤導(dǎo)的地方。除控制器以外,其他 broker 也可以與 Zookeeper 通信。因此,每個 broker 都應(yīng)該畫一條連接 ZK 的線。無論如何,畫太多線將導(dǎo)致該圖難以閱讀。該圖還忽略了,在不需要控制器介入的情況下,能夠修改 Zookeeper 中的狀態(tài)的外部命令行工具和工具包。正如上面討論的那樣,這些問題導(dǎo)致了控制器內(nèi)存中狀態(tài)無法真正的反映 Zookeeper 中的持久化狀態(tài)。
在 Proposed 架構(gòu)中,三個控制器節(jié)點取代了 Zookeeper 的 3 個節(jié)點??刂破鞴?jié)點和 broker 節(jié)點在不同的 JVM 中運行??刂破鞴?jié)點為元數(shù)據(jù)分區(qū)選舉一個 leader 節(jié)點,用橙色標(biāo)識。相較于控制器向各個 broker 推送元數(shù)據(jù)更新,在 Proposed 中,各個 broker 從 leader 中拉取元數(shù)據(jù)更新。這就是箭頭指向控制器的原因。
注意,控制器進(jìn)程與 broker 進(jìn)程是邏輯隔離的,它們不必做物理隔離。在某些情況下,將部分或者全部控制器進(jìn)程與 broker 進(jìn)程部署在一個節(jié)點上,有其存在的意義。這和 Zookeeper 進(jìn)程和 Kafka broker 部署在同一個節(jié)點上(目前小型集群的部署方式)類似。通常,各種各樣的部署方式都可能出現(xiàn),包括在同一個 JVM 中運行。
控制器 Quorum
控制器節(jié)點由管理元數(shù)據(jù)日志的 Raft quorum(Raft 選舉機制)組成。該日志包括每次變更集群的元數(shù)據(jù)相關(guān)信息。目前,一切信息都存儲在 Zookeeper 中,比如 topic、partition、ISR、配置等,在新的架構(gòu)中,這些信息都將存在日志中。
通過 Raft 算法,控制器節(jié)點將在它們之間選舉 leader,不需要依賴任何外部系統(tǒng)。元數(shù)據(jù)日志的 leader 被稱作活動的(active)控制器。活動控制器處理所有來自 broker 的 RPC 調(diào)用。follower 控制器(相對 leader 控制來說)從活動控制器中復(fù)制所有寫入的數(shù)據(jù),并且當(dāng)活動控制器故障時,做為熱備(hot standbys)。由于控制器全量追蹤最新狀態(tài),控制器故障切換將不再需要花很多時間轉(zhuǎn)移最新狀態(tài)到新的控制器上。
和 Zookeeper 一樣,Raft 需要大多數(shù)節(jié)點能正常運行,才能正常工作。因此,3 個節(jié)點控制器集群允許一個節(jié)點失效。5 個節(jié)點的控制器集群允許兩個節(jié)點失效,以此類推。
控制器將按周期將元數(shù)據(jù)快照寫入磁盤。雖然在概念上和壓縮相似,但是代碼路徑有些許不同,原因是我們從內(nèi)存中讀取狀態(tài),而不是從磁盤中重讀日志。
管理 broker 元數(shù)據(jù)
不同于控制器將更新推送至各個 broker,這些 broker 將通過新的 MetadataFetch API 從活動控制器拉取更新。
MetadataFetch 與拉取請求類似。就和拉取請求一樣,broker 將記錄最近一次拉取的更新的 offset,并且只從活動控制器請求新的更新。
broker 將拉取到的元數(shù)據(jù)持久化至磁盤。這將使得 broker 啟動的非常快,即使有成百上千分區(qū),甚至上百萬個分區(qū)。(注意,這種持久化是一種優(yōu)化,如果忽略這種優(yōu)化可以提高開發(fā)效率,那么我們可以在第一個版本中忽略它)
大多數(shù)時候,broker 只需要拉取增量狀態(tài)(deltas),而不是全量狀態(tài)。無論如何,如果 broker 的狀態(tài)與活動控制器的狀態(tài)差距過大,或者 broker 完全沒有緩存元數(shù)據(jù),控制器將返回全量元數(shù)據(jù)鏡像,而不是返回一些列的增量數(shù)據(jù)。
broker 按周期從活動控制器中請求元數(shù)據(jù)更新。該請求同時做為心跳發(fā)送,控制器以此得知該 broker 是存活狀態(tài)。
注意,雖然本節(jié)只討論管理 broker 的元數(shù)據(jù),但是管理客戶端的元數(shù)據(jù)對于可伸縮性也很重要。一旦發(fā)送增量元數(shù)據(jù)更新的基礎(chǔ)設(shè)施搭建好后,這些基礎(chǔ)設(shè)施將用于客戶端和 broker。畢竟,一般情形下,客戶端的數(shù)量會大于 broker 的數(shù)量。隨著分區(qū)數(shù)量的增長,客戶端感興趣的分區(qū)也會越多,所以,以增量的方式將元數(shù)據(jù)更新交付給客戶端將變得越來越重要。我們將在接下來的幾個小節(jié)中討論這個問題。
broker 狀態(tài)機
目前,broker 在啟動以后,馬上在 Zookeeper 中注冊自己。注冊的過程完成兩件事:告訴 broker 它是否被選舉為控制器,讓其他節(jié)點知道如何和它聯(lián)系。
在后 Zookeeper 時代的世界里,broker 通過控制器 quorum 注冊自己,而不是 Zookeeper。
當(dāng)前,一個能夠聯(lián)系 Zookeeper ,但由控制器分區(qū)的 broker,能繼續(xù)為用戶的請求提供服務(wù),但不會接收任何元數(shù)據(jù)更新。這將導(dǎo)致一些令人困惑、難以應(yīng)對的情況。例如,一個 producer 通過 acks=1 繼續(xù)發(fā)送數(shù)據(jù)給 leader,但實際上該 leader 已經(jīng)不再是真正的 leader,但是這個失效的 leader 無法接收控制器的 LeaderAndIsrRequest,從而移除 leader 地位。
在后 ZK 時代的世界里,集群的成員關(guān)系集成在元數(shù)據(jù)更新中。如果 broker 無法接收元數(shù)據(jù)更新,將從集群的成員中移除。雖然該 broker 仍然可能被某個特殊的客戶端分區(qū),但如果該 broker 是由控制器分區(qū)的,仍將從集群中移除。
broker 狀態(tài)
Offline
當(dāng) broker 進(jìn)程為 Offline 狀態(tài),它要么沒有啟動,要么在執(zhí)行啟動所需的單節(jié)點任務(wù),比如,初始化 JVM 或者執(zhí)行恢復(fù)日志。
Fenced
當(dāng) broker 處于 Fenced 狀態(tài),它將不再響應(yīng)來自客戶端的 RPC 請求。broker 在啟動后,嘗試?yán)∽钚碌脑獢?shù)據(jù)時,將處于 fenced 狀態(tài)。如果無法聯(lián)系活動控制器,broker 將重新進(jìn)入 fenced 狀態(tài)。發(fā)給客戶端的元數(shù)據(jù)應(yīng)該忽略狀態(tài)為 fenced 的 broker。
Online
當(dāng) broker 狀態(tài)為 online 時,表示該 broker 準(zhǔn)備好響應(yīng)客戶端的請求了。
Stopping
broker 進(jìn)入 stoppoing 狀態(tài)表示它們收到 SIGINT 信號。該信號表明系統(tǒng)管理員要關(guān)閉 broker。
broker 在 stopping 狀態(tài)時,仍在運行,但是我們嘗試將分區(qū) leader 從 broker 中移除。
最后,活動控制器在 MetadataFetchResponse 中添加一串特殊的代碼,要求 broker 進(jìn)入 offline 狀態(tài)?;蛘?,如果 leader 在預(yù)先定義的時間內(nèi)沒有動作,broker 將關(guān)閉。
將已有的 API 遷移到控制器中
之前的很多直接寫入 Zookeeper 的操作將變?yōu)閷懭肟刂破?。例如,變更配置、修改保存默認(rèn)授權(quán)的 ACLs,等等。
新版本的客戶端應(yīng)該將這些操作直接發(fā)給活動控制器。這是一個向后兼容的變更:在新舊集群中都能正常工作。為了兼容老客戶端,這些操作將隨機發(fā)送給 broker,broker 將這些請求轉(zhuǎn)發(fā)給活動控制器。
新的控制器 API
在某些情況下,我們需要創(chuàng)建一個新的 API 替換之前通過 Zookeeper 完成的操作。例如,當(dāng)分區(qū) leader 要修改 in-sync replica 集合時,在后 ZK 時代的世界里,它直接修改 Zookeeper,現(xiàn)在,leader 發(fā)起一個 RPC 請求到活動控制器。
從工具包中移除直接訪問 Zookeeper
目前,一些工具和腳本直接聯(lián)系 Zookeeper。在后 Zookeeper 時代的世界里,這些工具將被 Kafka API 取代。幸運的是,“KIP-4:命令行和中心化管理操作”,在幾年前開始移除直接訪問 Zookeeper,并且快完成了。