RocketMQ 5.0 無狀態實時性消費詳解

背景

RocketMQ 5.0 版本引入了 Proxy 模塊、無狀態 pop 消費機制和 gRPC 協議等創新功能,同時還推出了一種全新的客戶端類型:SimpleConsumer。

SimpleConsumer 客戶端採用了無狀態的 pop 機制,徹底解決了在客戶端發佈消息、上下線時可能出現的負載均衡問題。然而,這種新機制也帶來了一個新的挑戰:當客戶端數量較少且消息數量較少時,可能會出現消息消費延時的情況。

在當前的消息產品中,消費普遍使用了長輪詢機制,即客戶端向服務端發送一個超時時間相對較長的請求,該請求會一直掛起,除非隊列中存在消息或該請求到達設定的長輪詢時間。

然而,在引入 Proxy 之後,目前的長輪詢機制出現了一個問題。客戶端層面的長輪詢和 Proxy 與 Broker 內部的長輪詢之間互相耦合,也就是說,一次客戶端對 Proxy 的長輪詢只對應一次 Proxy 對 Broker 的長輪詢。因此,在以下情況下會出現問題:當客戶端數量較少且後端存在多個可用的 Broker 時,如果請求到達了沒有消息的 Broker,就會觸發長輪詢掛起邏輯。此時,即使另一臺 Broker 存在消息,由於請求掛在了另一個 Broker 上,也無法拉取到消息。這導致客戶端無法實時接收到消息,即 false empty response。

這種情況可能導致以下現象:用戶發送一條消息後,再次發起消費請求,但該請求卻無法實時拉取到消息。這種情況對於消息傳遞的實時性和可靠性產生了不利影響。

AWS 的文檔裏也有描述此等現象,他們的解決方案是通過查詢是所有的後端服務,減少 false empty response。

其他產品

在設計方案時,首先是需要目前存在的消息商業化產品是如何處理該問題的。

MNS 採取了以下策略,主要是將長輪詢時間切割爲多個短輪詢時間片,以儘可能覆蓋所有的 Broker。

首先,在長輪詢時間內,會對後端的 Broker 進行多次請求。其次,當未超過短輪詢配額時,優先使用短輪詢消費請求來與 Broker 進行通信,否則將使用長輪詢,其時間等於客戶端的長輪詢時間。此外,考慮到過多的短輪詢可能會導致 CPU 和網絡資源消耗過多的問題,因此在短輪詢超過一定數量且剩餘時間充足時,最後一次請求將轉爲長輪詢。

然而,上述策略雖以儘可能輪詢完所有的 Broker 爲目標,但並不能解決所有問題。當輪詢時間較短或 Broker 數量較多時,無法輪詢完所有的 Broker。即使時間足夠充足的情況下,也有可能出現時間錯位的情況,即在短輪詢請求結束後,纔有消息在該 Broker 上就緒,導致無法及時取回該消息。

解法

技術方案

首先,需要明確該問題的範圍和條件。該問題只會在客戶端數量較少且請求較少的情況下出現。當客戶端數量較多且具備充足的請求能力時,該問題不會出現。因此,理想情況是設計一個自適應的方案,能夠在客戶端數量較多時不引入額外成本來解決該問題。

爲了解決該問題,關鍵在於將前端的客戶端長輪詢和後端的 Broker 長輪詢解耦,並賦予 Proxy 感知後端消息個數的能力,使其能夠優先選擇有消息的 Broker,避免 false empty response。

考慮到 Pop 消費本身的無狀態屬性,期望設計方案的邏輯與 Pop 一致,而不在代理中引入額外的狀態來處理該問題。

另外,簡潔性是非常重要的,因此期望該方案能夠保持簡單可靠,不引入過多的複雜性。

  1. 爲了解決該問題,本質上是要將前端的客戶端長輪詢和後端的 Broker 長輪詢解耦開來,並賦予 Proxy 感知後端消息個數的能力,能夠優先選擇有消息的 Broker,避免 false empty response。
  2. 由於 Pop 消費本身的無狀態屬性,因此期望該方案的設計邏輯和 Pop 一致,而不在 Proxy 引入額外的狀態來處理這個事情。
  3. Simplicity is ALL,因此期望這個方案簡單可靠。

我們使用了 NOTIFICATION,可以獲取到後端是否有尚未消費的消息。擁有了上述後端消息情況的信息,就能夠更加智能地指導 Proxy 側的消息拉取。

通過重構 NOTIFICATION,我們對其進行了一些改進,以更好地適應這個方案的要求。

pop with notify

一個客戶端的請求可以被抽象爲一個長輪詢任務,該輪詢任務由通知任務和請求任務組成。

通知任務的目的是獲取 Broker 是否存在可消費的消息,對應的是 Notification 請求;而請求任務的目的是消費 Broker 上的消息,對應的是 Pop 請求。

首先,長輪詢任務會執行一次 Pop 請求,以確保在消息積壓的情況下能夠高效處理。如果成功獲取到消息,則會正常返回結果並結束任務。如果沒有獲取到消息,並且還有剩餘的輪詢時間,則會向每個 Broker 提交一個異步通知任務。

在任務通知返回時,如果不存在任何消息,長輪詢任務將被標記爲已完成狀態。然而,如果相關的 Broker 存在消息,該結果將被添加到隊列中,並且消費任務將被啓動。該隊列的目的在於緩存多個返回結果,以備將來的重試之需。對於單機代理而言,只要存在一個通知結果返回消息,Proxy 即可進行消息拉取操作。然而,在實際的分佈式環境中,可能會存在多個代理,因此即使通知結果返回消息存在,也不能保證客戶端能夠成功拉取消息。因此,該隊列的設計旨在避免發生這種情況。

消費任務會從上述隊列中獲取結果,若無結果,則直接返回。這是因爲只有在通知任務返回該 Broker 存在消息時,消費任務纔會被觸發。因此,若消費任務無法獲取結果,可推斷其他併發的消費任務已經處理了該消息。

消費任務從隊列獲取到結果後,會進行加鎖,以確保一個長輪詢任務只有一個正在進行的消費任務,以避免額外的未被處理的消息。

如果獲取到消息或長輪詢時間結束,該任務會被標記完成並返回結果。但如果沒有獲取到消息(可能是其他客戶端的併發操作),則會繼續發起該路由所對應的異步通知任務,並嘗試進行消費。

自適應切換

考慮到當請求較多時,無需採用 pop with notify 機制,可使用原先的 pop 長輪詢 broker 方案,但是需要考慮的是,如何在兩者之間進行自適應切換。目前是基於當前 Proxy 統計的 pop 請求數做判斷,當請求數少於某一值時,則認爲當前請求較少,使用 pop with notify;反之則使用 pop 長輪詢。

由於上述方案基於的均爲單機視角,因此當消費請求在 proxy 側不均衡時,可能會導致判斷條件結果有所偏差。

Metric

爲了之後進一步調優長輪詢和觀察長輪詢的效果,我們設計了一組 metric 指標,來記錄並觀測實時長輪詢的表現和損耗。

  1. 客戶端發起的長輪詢次數 (is_long_polling)
  2. pop with notify 次數 (通過現有 rpc metric 統計)
  3. 首次 pop 請求命中消息次數 (未觸發 notify) (is_short_polling_hit)

使用方式

在使用時需明確長輪詢和短輪詢的區分,可以參考 AWS 的定義,當輪詢時間大於 0 時,長輪詢生效。

可以看到需明確一個長輪詢最小時間,因爲長輪詢時間過小時無意義,AWS 的最小值採取了 1 秒。

在目前版本的 Apache RocketMQ 服務端中,採用了最小 5 秒的限制,即需超過 5 秒才能觸發長輪詢,該值可在 ProxyConfig#grpcClientConsumerMinLongPollingTimeoutMillis 中配置或修改。

對於 SimpleConsumer 而言,可以通過 awaitDuration 字段來調整長輪詢時間。

SimpleConsumer consumer = provider.newSimpleConsumerBuilder()
    .setClientConfiguration(clientConfiguration)
    .setConsumerGroup(consumerGroup)
    // set await duration for long-polling.
    .setAwaitDuration(awaitDuration)
    .setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))
    .build();

總結

通過如上方案,我們成功設計了一套基於無狀態消費方式的實時消費方案,在做到客戶端無狀態消費的同時,還能夠避免 false empty response,保證消費的實時性,同時,相較於原先 PushConsumer 的長輪詢方案,能夠大量減少用戶側無效請求數量,降低網絡開銷。

作者:紹舒

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章