背景
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 一致,而不在代理中引入額外的狀態來處理該問題。
另外,簡潔性是非常重要的,因此期望該方案能夠保持簡單可靠,不引入過多的複雜性。
- 爲了解決該問題,本質上是要將前端的客戶端長輪詢和後端的 Broker 長輪詢解耦開來,並賦予 Proxy 感知後端消息個數的能力,能夠優先選擇有消息的 Broker,避免 false empty response。
- 由於 Pop 消費本身的無狀態屬性,因此期望該方案的設計邏輯和 Pop 一致,而不在 Proxy 引入額外的狀態來處理這個事情。
- 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 指標,來記錄並觀測實時長輪詢的表現和損耗。
- 客戶端發起的長輪詢次數 (is_long_polling)
- pop with notify 次數 (通過現有 rpc metric 統計)
- 首次 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 的長輪詢方案,能夠大量減少用戶側無效請求數量,降低網絡開銷。
作者:紹舒
點擊立即免費試用雲產品 開啓雲上實踐之旅!
本文爲阿里雲原創內容,未經允許不得轉載。