字節跳動新一代雲原生消息隊列實踐

2024年2月6日 24点热度 0人点赞

上文我們了解了在字節跳動內部業務快速增長的推動下,經典消息隊列 Kafka 的劣勢開始逐漸暴露,在彈性、規模、成本及運維方面都無法滿足業務需求。因此字節消息隊列團隊研發了計算存儲分離的雲原生消息引擎 BMQ,在極速擴縮容及吞吐上都有非常好的表現。本文將繼續從整體技術架構開始,介紹字節自研的雲原生消息引擎的分層架構在數據存儲模型、運維等角度的優勢及挑戰。

一文了解字節跳動消息隊列演進之路

雲原生消息引擎 BMQ 架構

從整體來看,BMQ 與 Kafka 架構最大的不同在於 BMQ 是存算分離的架構,相較於 Kafka 將數據存儲在本地磁盤,BMQ 將數據存儲在了分佈式的存儲系統。在 BMQ 內部,主要有四個模塊:Proxy,Broker,Coordinator 和 Controller。我們依次來看一下這些模塊的主要工作:

  • Proxy 負責接收所有用戶的請求,對於生產請求,Proxy 會將其轉發給對應的 Broker;對於消費者相關的請求,例如 commit offset,join group 等,Proxy 會將其轉發給對應的 Coordinator;對於讀請求 Proxy 會直接處理,並將結果返回給客戶端。
  • BMQ 的 Broker 與 Kafka 的 Broker 略有不同,它主要負責寫入請求的處理,其餘請求交給了 Proxy 和 Coordinator 處理。
  • Coordinator 與 Kafka 版本最大的差別在於我們將其從 Broker 中獨立,作為單獨的進程提供服務。這樣的好處是讀寫流量與消費者協調的資源可以完全隔離,不會互相影響。另外 Coordinator 可以獨立擴縮容,以應對不同集群的情況。
  • Controller 承擔組件心跳管理、負載均衡、故障檢測及控制命令接入的工作。因為 BMQ 將數據放在分佈式存儲系統上,因此無需管理數據副本,相較於 Kafka 省去了 ISR 相關的管理。Controller 可以更加專註地關註集群整體流量均衡及故障檢測。

在 BMQ 中用戶所有請求都會由 Proxy 接入,因此 BMQ 的 Metadata 中的 ‘Broker’ 信息實際上填寫的是 BMQ 中 Proxy 的信息,客戶端根據 Metadata 請求將生產和消費等請求發送到對應的 Proxy,再由 Proxy 處理或轉發。這樣的架構有助於 BMQ 做更多的容錯工作。例如在 Broker 重啟時,Proxy 可以感知到相關錯誤並進行退避重試,避免將異常直接暴露給客戶端;此外我們可以監控 Proxy 在訪問其他組件時產生的錯誤,進行一些自動的故障診斷,並將故障節點自動隔離,避免對用戶產生影響

分層架構的優勢

分層架構的優勢是顯而易見的,BMQ 作為計算層無狀態,可以實現秒級的擴縮容或故障機替換。在故障場景下,例如交換機故障或機房故障,可以秒級將流量調度到健康節點恢復服務。

數據存儲模型

在分層之後數據存儲模型上的優勢,主要體現在 BMQ 中,一個 Partition 的數據會和 Kafka 一樣被切分為若幹個 Segment,Kafka 中的這些 Segment 都會被存儲在同一塊磁盤上,而在 BMQ 中,因為數據存儲在分佈式存儲中,每一個 Segment 也都被存儲在存儲池中不同的磁盤上。從上圖中可以明顯看出,BMQ 的存儲模型很好的解決了熱點問題。即使 Partition 間數據大小或訪問吞吐差別很大,被切割成 Segment 後都能均勻地分散在存儲池中。

接下來我們通過一個例子進一步感受池化存儲的優勢。

在 Kafka 的使用中,我們經常會有回溯數據的需求,以上圖中的數據分佈為例,例如業務有需求回溯 Partition 1 全部的數據,高吞吐的 IO 會影響磁盤的性能,在 Kafka 存儲模型中與 Partition 1 Leader 同在一塊磁盤的 Partition 3 Follower 就會受到影響,使得 Partition 3 處於 Under Replica 的狀態。這個狀態會持續到用戶將 Partition 全部數據回溯完成。

而在 BMQ 的存儲模型中,Partition 1 的數據分散在不同磁盤上,熱點會隨著用戶的回溯進程轉移,不會持續影響同一塊磁盤。且對於回溯訪問的磁盤,僅有已經存儲在該磁盤的其他 Segment 剛好被用戶消費時,或有新的 Segment 要寫入該磁盤的時候會受影響。此外我們也可以通過一些策略避免寫入有熱點訪問的磁盤來降低熱點訪問對新寫入的影響。總結來看,Kafka 存儲模型下,熱點訪問對同磁盤其他訪問的影響大、持續長、且優化空間不大;而 BMQ 的池化存儲模型中,熱點影響范圍小、持續時間短,並且可以通過一些策略優化進一步降低影響。

運維及故障影響

從運維角度來看,BMQ 的存儲模型也有非常大的優勢。無論重啟、替換、擴容還是縮容,Kafka 都需要數據拷貝。以擴容為例,新擴容的 Broker 需要作為 Partition 的 follower,將數據從 leader 所在 Broker 拷貝至本地,全部拷貝完成後新 Broker 才可以晉升為 leader 提供服務。而矛盾的地方在於,當業務流量上漲急需擴容時,Broker 已經沒有多餘的帶寬來支持拷貝數據了。而 BMQ 所依賴的分佈式存儲系統則沒有這個問題,同樣以擴容為例,新擴充進來的存儲節點可以立即提供讀寫服務,無需做額外的數據拷貝,不會對原有存儲造成額外壓力。而在替換和縮容場景,分佈式存儲依然需要一些數據拷貝來補齊副本,但對業務影響會小很多。因為數據存儲是分散的,因此拷貝的 IO 也會分散在多臺存儲上。

從故障影響角度分析,以兩副本的配置為例,在 Kafka 場景下,任意兩臺 Broker 宕機都會造成某個 Partition 無法讀寫,且數據全部丟失。在 BMQ 的存儲模型下,任意兩臺存儲節點的異常都不會影響新寫入的數據,因為隻要存活的存儲節點可以支持寫入流量,新寫入的數據就可以選擇剩餘健康的存儲節點寫入。對於已經存入的數據,兩臺存儲節點宕機會導致同時存在這兩臺機器上的 Segment 無法讀取,若這個 Segment 是最近寫入的尚未被消費的,則會影響這部分數據的消費,但若這個 Segment 剛好是一個歷史數據,沒有消費者需要,那就不會對業務產生實際影響。

分層架構的挑戰

上面我們討論了分層架構帶來的優勢,下面要來分析下挑戰以及 BMQ 的解決方案。分層存儲之後 BMQ 訪問數據的代價增加了,訪問存儲在分佈式系統上的數據延時會比直接讀取本地磁盤稍高,並且我們也需要考慮對分佈式存儲系統元信息及存儲節點的壓力情況。下面我們來分別看一下 BMQ 在生產和消費這兩條鏈路上是如何克服這些困難的。

生產

首先介紹一下 BMQ 數據寫入的流程。上文介紹過 Broker 主要負責數據寫入的節點,由 Controller 負責將 Partition 分配到各個 Broker 上。因為 Kafka 協議中 Partition 內部的數據是有序的,因此每個 Partition 隻會在唯一一個 Broker 上調度。Controller 調度的時候也會綜合考慮 Broker 的負載及 Partition 的流量等因素,最終做到 Broker 之間的負載均衡。

如上圖所示,當一個 Partition 被調度到 Broker 上之後,便開始了它的生命周期。首先 Partition 會進行 Recover,即從上一個 Checkpoint 恢復數據,並將最終結果保存,這樣做是避免因意外宕機導致用戶已經寫入成功的數據丟失。之後 Partition 便會創建一個新的 Segment 開始寫入數據,期間會寫入索引等信息。當文件長度到達配置長度,或者文件寫入持續到達配置時間後會被關閉,存儲相關元信息,並開啟一個新的 Segment 寫入。依次循環,直到 Controller 將 Partition 從這個 Broker 調度走,或發生異常 Partition 退出。

我們可以看到在狀態集中有一個 Failover 節點,這個節點是 BMQ 降低分佈式存儲延時毛刺的關鍵。每一次寫入 BMQ 會先將數據放入一個 Inflight Buffer 中,之後通過異步調用分佈式存儲的 Flush 接口持久化數據。若 Flush 在預期時間內返回成功,那麼 Inflight Buffer 數據中的數據會被清除,同時返回給用戶寫入成功的回應。但若因為網絡或者慢節點問題導致寫入超時,那麼 Broker 會直接創建一個新的 Segment 文件,將 Inflight Buffer 中的數據直接寫入新的文件,並在後臺異步將之前的 Segment 文件關閉。對於異步關閉的這個文件,元信息隻會包含成功返回的數據長度,最後超時的部分則不會被記錄,這樣即使超時數據最終確實寫入了分佈式存儲,也不會被用戶讀取造成數據重復,這一整個過程就是我們說的 Failover。

為什麼通過切一個文件就能解決這個問題呢?這也與存儲模型有關。Kafka 因為一個 Partition 數據均被存儲在一塊磁盤上,那麼若是因為磁盤異常引起的延時抖動,無論如何切換文件都是不能解決的。但是在 BMQ 中,每個 Segment 都是一個文件,而每個文件的多個副本都會隨機地分佈在整個存儲池中。那麼若存儲池中有少數慢節點,隨機切換一個節點大概率可以繞過故障的節點。因此,在慢節點問題及偶發的磁盤熱點問題上,BMQ 可以更加靈活地規避,降低這些問題對用戶的影響。

當然,BMQ 的分層架構對於底層的分佈式存儲系統也提出了較高的要求。火山引擎上分佈式存儲系統由 C 實現,是一個高性能的分佈式文件系統。能夠提供 5w QPS 寫入及 15w QPS 讀取的元信息訪問能力;寫入訪問延時 p99 約在 10 毫秒左右,讀取延時 p99 為亞毫秒級別;並且單集群可以承載 50 億文件。同時在數據寫入方面對寫入延時也做了很多優化,包括慢節點的檢測和規避、利用 NVMe 加速的多介質存儲功能等。

消費

當一個消費請求到達 Kafka Broker,Broker 會查看當前是否有足夠多的已寫入數據返回給消費者,如果條件滿足則會讀取數據並返回。這個流程非常簡單清晰,但這個流程不能直接照搬到 BMQ,因為 BMQ 底層是分佈式存儲系統,如果對於每個請求都直接從存儲層讀取數據,那麼對於分佈式存儲系統的元信息和數據節點都是極大地壓力,並且延時也會變得非常高。因此直接處理消費請求的 BMQ Proxy 針對讀流程設計了多個緩存機制

第一個緩存系統非常直觀,我們稱之為 Message Cache。顧名思義,這個緩存存儲的是消息數據。Message Cache 會將每個 Partition 末尾的一部分數據從遠端讀取回來,並緩存在內存中,以供消費者讀取。若這個 Partition 有多個消費組,那麼理想情況下,他們隻會產生一次分佈式文件系統的實際數據讀取,其餘請求均會從 Proxy 內存中直接獲取數據。不同於 Kafka 依賴於 Page Cache,BMQ 的 Message Cache 擁有豐富的淘汰策略以應對不同的生產消費場景,使得緩存命中率更高

當然,不是所有的請求都能夠完美的命中 Message Cache,一些消費者會因為消費資源不足或業務需求消費一些較老的數據,而這部分數據無法被 Message Cache 覆蓋。如果在這種請求發生時 Proxy 直接讀取分佈式存儲系統則會對其造成一次元數據的訪問,當請求變多時分佈式存儲系統的元數據節點將不堪重負。因此 Proxy 設計來 File Cache 來應對這種情況。Proxy 會緩存某個 Segment 的文件句柄,即這個 Segment 所對應文件的文件句柄。因為 Kafka 的消費場景下,用戶大多數情況都是順序消費,因此一個消費請求這一次所訪問的文件很大概率是上一次請求訪問過的文件。線上實踐效果來看,File Cache 可以幫助我們減少 70% 對後端存儲的元信息訪問請求

在 BMQ 擁有優越的消費性能上也需要強大的分佈式存儲系統的加持。除了上一節提到的高性能的元數據節點,也需要存儲系統支持讀取的慢節點檢測,即如果當前讀取的節點延時較高,Client 端會自動切換另外一個節點讀取。再加上 NVMe 分層存儲的加速,BMQ 可以以較低延時達到非常理想的消費吞吐。

總結與展望

總體來看,分層架構給 BMQ 帶來了極大的性能收益及可運維性的提升,同時也給我們帶了來很多的挑戰。BMQ 也通過不停的探索和優化,成功克服了這些困難,很好的支撐了業務的發展。在線上實踐中,目前我們承接了 TB/s 級別的入流及數十 TB/s 級別的的峰值吞吐,其中最大 Topic 峰值達到數百 GB/s 入流和 TB/s 級別的出流吞吐。

當然,我們也在思考如何在各種場景下持續優化雲原生消息引擎能力為用戶帶來更加極致的使用體驗。對於一些特定的場景,探索將 Proxy 和 Broker 融合,在降低部署成本的同時提供更加極致的讀寫延時體驗。未來,我們也將持續優化自動檢測能力,使它更智能、更準確判斷故障的同時更快地隔離異常節點,縮短影響時間,持續為 BMQ 的穩定性保駕護航。此外我們也在探索更加極致的彈性能力,在保障租戶吞吐能力的同時,可以根據流量潮汐自動擴縮容實例,實現極致地降本增效。後續,我們還會介紹更多技術能力,敬請期待。


火山引擎雲原生消息引擎 BMQ 基於雲原生全托管服務,支持靈活動態的擴縮容和流批一體化計算,能夠有效地處理大數據量級的實時流數據,幫助用戶構建數據處理的“中樞神經系統”,廣泛應用於日志收集、數據聚合、離線數據分析等業務場景。