Kafka 系統在快手有著很廣泛的應用,隨著其業務的高速發展, Kafka 叢集的規模也成指數增長,目前快手 Kafka 叢集日訊息處理總量達數萬億級別,峰值超過 1 億 /s。與此同時,快手也面臨了很多新問題與技術挑戰。本文整理自快手高階架構師、大資料架構團隊負責人趙健博在 QCon 北京2019 上的演講,他介紹了 Kafka 系統在快手的應用實踐、遇到的問題以及相應的技術的演進過程。
大家好,我是趙健博,來自快手,今天要和大家分享的議題是《快手萬億級別 Kafka 叢集應用實踐與技術演進之路》。先來介紹下我自己,我畢業於中國科學院計算技術研究所,目前負責快手大資料架構團隊,團隊的主要職責是為公司提供資料業務方向的基礎設施,包括資料後端中儲存、計算、排程、AI 相關係統的研發與應用。
本次分享包括三個方向的內容:首先會介紹下業務相關背景;然後會重點介紹快手 Kafka 的技術演進過程;最後,簡單介紹下後續計劃。
業務場景
先來看業務場景,在快手,Kafka 被大規模使用著。從場景上看,我們把 Kafka 分成了 3 類叢集。第一類,線上叢集,Kafka 作為訊息中介軟體,為不同線上業務之間提供非同步訊息通知服務;第二類,LOG 叢集,業務程式直接將 LOG 打給 Kafka,並透過 Kafka 進行傳輸與收集,由於資料不落地,所以這個過程不能出現由於 Kafka 問題導致業務程式受到影響,這對 Kafka 可用性要求很高,另外,LOG 叢集還為重要的實時計算或者模型訓練提供資料來源;第三類,離線叢集,LOG 資料最終的匯聚點,資料會被實時 dump 到 HDFS 中,這部分資料用於離線處理。類似的,離線叢集還為次要的實時計算、實時訓練提供資料來源。
除此之外,作為一個整體,我們還提供了資料 Mirror 服務,用於將資料從線上叢集、LOG 叢集傳輸到離線叢集。之所以將 Kafka 進行了物理叢集的劃分,主要考慮的是為了保障服務質量,控制 Kafka 叢集問題的影響面。
業務規模
再來看下快手 Kafka 叢集相關規模,目前總機器數大概 2000 臺;30 多個叢集;topic 12000 個;一共大概 20 萬 TP(topic partition);每天總處理的訊息數超過 4 萬億條;峰值超過 1 億條;日總資料量,入口達 4PB,出口達 20PB;網路頻寬峰值,入口 1Tbps,出口 4Tbps。
技術演進
接下來,我們來看下技術演進過程。
這個 PPT 展示了快手 Kafka 的技術演進過程,整體上說,包括 4 個階段:
第一個階段:為了支援業務的快速發展,我們首先做了多叢集建設以及增加了 Kafka 平滑擴容功能;
第二個階段,為了保障業務穩定,我們對 Kafka 的可用性進行了改造,經過改造,我們將由於單點當機發現與恢復的時間從 91s 最佳化到 6s 左右,有 15 倍的提升;
第三個階段,為了增加系統的可維護性以及提升讀系統的運維效率,我們對資料 Mirror 服務做了叢集化建設並開發了資源管理平臺;
第四個階段,為了進一步提升 Kafka 的穩定性、效能,我們做了資源隔離、對 cache 進行了改造、並針對消費者進行了智慧限速等。
2019 年,技術迭代還在繼續。接下來,我將會介紹其中 5 點的細節:平滑擴容、Mirror 叢集化、資源隔離、cache 改造、消費智慧限速。
平滑擴容
首先看下平滑擴容功能。在介紹之前,我們先來看下原有版本 Kafka 的擴容流程。假如叢集有 3 個 broker,一共有 4 個 TP,每個 3 副本,均勻分佈。現在要擴容一臺機器,新 broker 加入叢集后需要透過工具進行 TP 的遷移。一共遷移 3 個 TP 的副本到新 broker 上。等遷移結束之後,會重新進行 leader balance,最終的 TP 分佈如圖所示。
從微觀的角度看,TP 從一臺 broker 遷移到另一個 broker 的流程是怎麼樣的呢?我們們來看下 TP3 第三個副本,從 broker1 遷移到 broker4 的過程,如下圖所示,broker4 作為 TP3 的 follower,從 broker1 上最早的 offset 進行獲取資料,直到趕上最新的 offset 為止,新副本被放入 ISR 中,並移除 broker1 上的副本,遷移過程完畢。
但在現有的擴容流程中存有如下問題:資料遷移從 TP3 的最初的 offset 開始複製資料,這會導致大量讀磁碟,消耗大量的 I/O 資源,導致磁碟繁忙,從而造成 produce 操作延遲增長,產生抖動。所以整體遷移流程不夠平滑。我們看下實際的監控到的資料。從中可以看到資料遷移中, broker1 上磁碟讀量增大,磁碟 util 持續打滿,produce 的 P999 的延遲陡增,極其不穩定。
針對這個問題,我們回到 Kafka 遷移的流程上看,理論上 Kafka 是一個快取系統,不需要永久儲存資料,很有可能費了很多工作遷移過來的資料根本就不會被使用,甚至馬上就會被刪除了。從這個角度上看,那麼遷移資料時,為什麼一定要從 partition 最初 offset 開始遷移資料呢?細想想,好像不需要這樣。
所以,解決這個問題的思路就比較簡單了,在遷移 TP 時,直接從 partition 最新的 offset 開始資料遷移,但是要同步保持一段時間,主要是確保所有 consumer 都已經跟得上了。
如圖所示,再來看這個 TP3 的第三個副本從 broker1 遷移到 broker4 的過程。這次 broker4 直接從 broker1 最新的 offset 開始遷移,即 transfer start 這條豎線。此時,因為 consumer1 還沒能跟得上,所以整個遷移過程需要保持一段時間,直到 transfer end 這個點。
這個時候,可以將 TP3 的新副本放到 ISR 中,同時去掉 broker1 上的副本,遷移過程完畢。從這次的遷移過程中看,因為都是讀最近的資料,不會出現讀大量磁碟資料的問題,僅僅多了一個副本的流量,基本對系統無影響。此時,我們再來看下磁碟讀量、磁碟 util、以及 produce 的延遲,從圖中可知基本沒有任何變化。
整體過程非常平滑,可以說透過這種方式很優雅地解決了 Kafka 平滑擴容問題,我們之前也有在晚高峰期間做擴容的情況,但是從 Kafka 整體服務質量上看,對業務沒有任何影響。
這個功能有一個 patch 可用:
https://issues.apache.org/jira/browse/KAFKA-8328
如果大家有需求,可以從這裡看下程式碼。
Mirror 叢集化
接下來分享下我們是如何改進 Mirror 服務,使其具備較好的管理性,提升運維效率的。先來看下目前 Mirror 服務的主要問題。
如圖所示,這個是目前 Kafka 多叢集之間的 Mirror 服務架構。其中 Mirror 服務使用的是 Kafka 自帶的 MirrorMaker ,這個服務存在 2 點問題:
被 Mirror 的 topic 是靜態管理的,運維成本很高,且容易出錯;
一旦有 topic 增加或者減少,以及機器的加入或者退出,都會導致原有正在 Mirror 的資料斷流,這主要是因為經歷了停止服務,再啟動服務的過程;
為了解決這個問題,我們基於 UReplicator,開發了 KReplcator 服務,並替換掉了現有的 MirrorMaker 服務。UReplicator 是 Uber 開源的 Kafka 資料 Mirror 同步服務。
如圖所示,在部署的時候,我們部署了多個 KReplicator cluster,主要為了保障資料同步的穩定性。在實現細節上,我們對 UReplicator 進行了擴充套件,使其可以動態感知不同 Kafka 叢集。這樣只需部署一個 Mirror 叢集,就可以進行不同源叢集以及不同目標叢集的資料同步,而不再需要部署多個 Mirror 叢集。
KReplicator 叢集包括三個模組:
Controller:
用於動態管理 topic、worker 節點的增減;
負責 TP 的分配策略,支援部分 partition 的遷移,這樣新增節點或節點當機會觸發部分 TP 的遷移,不會造成 Mirror 服務的整體斷流,僅僅是一小部分有抖動;
負責 worker 當機的異常恢復;
Worker:
支援動態增加與減少 topic,這樣增加或者減少 topic 避免了對已有 TP 傳輸的影響;
支援同時傳輸多個源叢集到多個目標叢集的資料傳輸能力;
支援將資料 dump 到 HDFS 中;
Zookeeper:
負責協調 controller 與 worker。
有了 KReplicator cluster 管理 Kafka 多叢集間的資料 Mirror,極大減少了我們的運維成本,以及出錯的情況。此外,由於叢集化管理的存在,我們可以快速地對 Mirror 服務進行擴縮容量,以便應業務突發的流量。
資源隔離
在沒有資源隔離之前,我們經常會遇到這樣問題:
不同業務線之間的 topic 會相互影響。如下圖所示,這個 broker 服務兩個業務線的 TP,不同業務線的 TP 會共享一塊磁碟。如果此時,consumer 出現問題,導致消費產生 lag。而 lag 積累會導致讀取磁碟中的資料,進而造成磁碟繁忙。最終,會影響在同一塊磁碟的其他業務線 TP 的寫入。
解決思路也很簡單,就是對不同業務的 topic 進行物理隔離。把不同業務線的 topic 放到不同的 broker,如圖所示,這樣任何業務線產生問題,不會影響其他業務線。這個改動需要對 broker 打上不同的標籤,並在 topic 建立、TP 遷移、當機恢復流程中,增加按照標籤的 TP 分配演算法就可以。
Kafka RPC 佇列缺少隔離,一旦某個 topic 處理慢,會導致所有請求 hang 住。
如圖所示,Kafka RPC 框架中,首先由 accepter 從網路中接受連線,每收到一個連線,都會交給一個網路處理執行緒(processer)處理,processor 在讀取網路中的資料並將請求簡單解析處理後,放到 call 佇列中,RPC 執行緒會從 call 佇列中獲取請求,然後進行 RPC 處理。此時,假如 topic2 的寫入出現延遲,例如是由於磁碟繁忙導致,則會最終將 RPC 執行緒池打滿,進而阻塞 call 佇列,進而打滿網路執行緒池,這樣發到這個 broker 的所有請求都沒法處理了。
解決這個問題的思路也比較直接,需要按照控制流、資料流分離,且資料流要能夠按照 topic 做隔離。首先將 call 佇列按照拆解成多個,並且為每個 call 佇列都分配一個執行緒池。在 call 佇列的配置上,一個佇列單獨處理 controller 請求的佇列(隔離控制流),其餘多個佇列按照 topic 做 hash 的分散開(資料流之間隔離)。如果一個 topic 出現問題,則只會阻塞其中的一個 RPC 處理執行緒池,以及 call 佇列。這裡還有一個需要注意的是 processor 在將請求放入 call 佇列中,如果發現佇列已滿,則需要將請求立即失敗掉(否則還是會被阻塞)。這樣就保障了阻塞一條鏈路,其他的處理鏈路是暢通的。
Cache 改造
接下來重點看下我們對 cache 的改造,還是很有意思的。
我們都知道,Kafka 之所以有如此高的效能,主要依賴於 page cache,producer 的寫操作,broker 會將資料寫入到 page cache 中,隨後 consumer 發起讀操作,如果短時間內 page cache 仍然有效,則 broker 直接從記憶體返回資料,這樣,整體效能吞吐非常高。
但是由於 page cache 是作業系統層面的快取,難於控制,有些時候,會受到汙染,從而導致 Kafka 整體效能的下降。我們來看 2 個例子:
第一個 case:consumer 的 lag 讀會對 page cache 產生汙染。
如圖所示,假如有 2 個 consumer,1 個 producer。其中,藍色 producer 在生產資料,藍色 consumer 正在消費資料,但是他們之間有一定的 lag,導致分別訪問的是不同的 page cache 中的 page。如果一個橙色 consumer 從 topic partition 最初的 offset 開始消費資料的話,會觸發大量讀盤並填充 page cache。其中的 5 個藍色 topic 的 page 資料都被橙色 topic 的資料填充了。另外一方面,剛剛藍色 producer 生產的資料,也已經被沖掉了。此時,如果藍色 consumer 讀取到了藍色 producer 剛剛生產的資料,它不得不再次將剛剛寫入的資料從磁碟讀取到 page cache 中。綜上所述,大 lag 的 consumer 會造成 cache 汙染。在極端情況下,會造成整體的吞吐下降。
第二個 case:follower 也會造成 page cache 的汙染。
在圖中 broker1 機器內部,其中 page cache 中除了包括藍色 producer 寫入的資料之外,還包括橙色 follower 寫入的資料。但是橙色 follower 寫入的資料,在正常的情況下,之後不會再有訪問,這相當於將不需要再被訪問的資料放入了 cache,這是對 cache 的浪費並造成了汙染。所以,很容易想到 Kafka 是否可以自己維護 cache 呢?首先,嚴格按照時間的順序進行 cache,可以避免異常 consumer 的 lag 讀造成的 cache 汙染。其次,控制 follower 的資料不進入 cache,這樣阻止了 follower 對 cache 的汙染,可以進一步提升 cache 的容量。
基於這個想法,我們對 Kafka cache 進行了整體設計,如圖所示。
我們在 broker 中引入了兩個物件:一個是 block cache;另一個是 flush queue。Producer 的寫入請求在 broker 端首先會被以原 message 的形式寫入 flush queue 中,之後再將資料寫入到 block cache 的一個 block 中,之後整個請求就結束了。在 flush queue 中的資料會由其他執行緒非同步地寫入到磁碟中(會經歷 page cache 過程)。而 follower 的處理流程保持和原來一致,從其他 broker 讀到資料之後,直接把資料寫到磁碟(也會經歷 page cache)。這種模式保證了 block cache 中的資料全都是 producer 產生的,不會被 follower 汙染。
對於 consumer 而言,在 broker 接到消費請求後,首先會從 block cache 中檢索資料,如果命中,則直接返回。否則,則從磁碟讀取資料。這樣的讀取模式保障了 consumer 的 cache miss 讀並不會填充 block cache,從而避免了產生汙染,即使有大 lag 的 consumer 讀磁碟,也仍然保證 block cache 的穩定。
接下來,我們看下 block cache 的微觀設計,整個 block cache 由 3 個部分組成:
第一部分:2 類 block pool,維護著空閒的 block 資訊,之所以分成 2 類,主要是因 segment 資料以及 segment 的索引大小不同,統一劃分會導致空間浪費;
第二部分:先進先出的 block 佇列,用於維護 block 生產的時序關係,在觸發淘汰時,會優先淘汰時間上最早的 block;
第三部分:TP+offset 到有效 blocks 的索引,用於快速定位一個 block。一個 block 可以看做是 segment 的一部分。segment 資料以及 segment 索引和 block 的對應關係如圖所示。
最後,還有兩個額外的執行緒:
eliminater 執行緒,用於非同步進行 block cache 淘汰,當然,如果 produce 請求處理時,發現 block cache 滿也會同步進行 cache 淘汰的;
非同步寫執行緒,用於將 flush queue 中的 message 非同步寫入到磁碟中。
這個就是 Kafka cache 的整體設計,可以看出,已經很好地解決了上述的兩個對 cache 汙染的問題了。
來看下我們的測試結果。我們搭建了 5 個 broker 的叢集,其中一個換成了 Kafka cache 的版本。並建立了一個 150 個 partition 的 topic,3 副本。所以算上副本,一共有 450 個 partition,每臺機器上 90 臺 TP。之後我們 Mirror 了一個線上的流量資料,並啟動 150 個 consumer,以總體 lag 450w 條資料開始讀。
從圖中可以看出,原始版本在這種情況下會造成大量的磁碟讀,而 Kafka cache 版本沒有任何磁碟讀取操作,這說明 Kafka cache 版本可以 cache 更多的有效資料,這點是排除了 follower 造成的汙染,經過精細化統計,發現 Kafka cache 有效空間剛好為原版本的 2 倍(正好和同一臺 broker 中 TP 為 leader 和 follower 的數量比一致)。從恢復的時間上看,由於排除了讀磁碟以及多個 consumer 之間讀可能會 cache 產生的汙染,Kafka cache 的版本也比原有版本速度要快了 30%。
除此之外,再看下改進後的 broker,從這個圖中可以看出,produce 整個寫入過程,先是同步寫入記憶體,然後再被非同步刷入磁碟。雖然 page cache 的模式也是類似這種,但是 page cache 會存在一定不穩定性(可能會觸發同步寫盤)。這個是我們上線 Kafka cache 版本前後的 produce p999 的延遲對比。從圖中可以看到:Kafka cache 版本比原來版本的延遲低了很多,且穩定性有極大改進。
智慧限速
剛才在講到資源隔離的時候,我們看到過這樣一個 case。就是如果 consume 操作大量讀磁碟,會影響 produce 操作的延遲。當時我們透過資源物理隔離達到了隔離不同業務線 topic 的目標,避免相互影響。但是對於同一個業務線的 topic 之間還可能會存在相互影響。那麼如何解決 comsumer lag 後讀盤導致 producer 寫入受阻問題呢?我們採用的做法是:當磁碟繁忙時,對 lag 的 consumer 進行限速控制。
如圖所示,整個限速邏輯實現在 RPC 工作執行緒處理的末端,一旦 RPC 處理完畢,則透過限速控制模組進行限速檢測,如果要限速,則確定等待時間,之後放入到 delayed queue 中,否則放到 response queue 中。放入到 delayed queue 中的請求,等待時間達到後,會被 delayed 執行緒放入到 response queue 中。最終在 response queue 中的請求被返回給 consumer。
對於限速控制模組的檢測邏輯則是根據當前請求 topic 所在磁碟是否繁忙,以及這次的 lag 是否超過閾值(不 lag 的消費者不能限速。閾值的設定要憑經驗資料,但後續會和 Kafka cache 進行結構,則可以精確哪些請求是 block cache miss 的,進而進行限速控制)。
Metric 採集執行緒則週期性採集磁碟 metric 等資訊,並給限速決策模組提供資料。實際的限速控制情況從圖中可以看到:在限速開始之前,磁碟 util 達 100%,且 produce 延遲達到數秒。在限速開始之後,進行限速調整階段,等到穩定之後,磁碟 util 達 60% 多,且保持穩定。另一個方面,produce 的時延也達到穩定,且延遲很低。
後續計劃
第一點,由於機房的限制,我們面臨無法叢集內擴容的問題。如果搭建新叢集,勢必會帶來大量的業務遷移過程,這會搞得大家都很痛苦。所以,解決的思路是,是否可以建設支援跨 IDC 的統一大叢集的方案。
第二點,隨著業務規模越來越大,目前 controller 存在一系列的效能問題。極端情況下,會影響系統穩定。接下來會對這塊進行一系列最佳化。
第三點,部分業務線有對事物功能的訴求,我們也會參考高版本的設計,將這個功能增加進來。
第四點,機器的磁碟會出現“半死不活”的情況,這段時間請求會卡死,造成業務的不穩定。我們要想辦法把這種情況解決掉。
總 結
以上就是我今天所要分享的全部內容。簡單回顧下,本次分享第一部分介紹了快手 Kafka 使用場景。第二部分,我重點介紹了快手 Kafka 的 5 點重要改進,分別是:平滑擴容、Mirror 叢集化、資源隔離、cache 改造以及消費智慧限速。並分別介紹了每個改進的緣由、改進思路以及改進後的效果。最後,介紹了快手在 Kafka 系統上的後續計劃。非常感謝大家!