天天看點

RocketMQ 重試機制詳解及最佳實踐

作者:阿裡開發者
本文主要介紹在使用 RocketMQ 時為什麼需要重試與兜底機制,生産者與消費者觸發重試的條件和具體行為,如何在 RocketMQ 中合理使用重試機制,幫助建構彈性,高可用系統的最佳實踐。

作者:斜陽

引言

本文主要介紹在使用 RocketMQ 時為什麼需要重試與兜底機制,生産者與消費者觸發重試的條件和具體行為,如何在 RocketMQ 中合理使用重試機制,幫助建構彈性,高可用系統的最佳實踐。

RocketMQ 的重試機制包括三部分,分别是生産者重試,服務端内部資料複制遇到非預期問題時重試,消費者消費重試。本文中僅讨論生産者重試和消費者消費重試兩種面向使用者側的實作。

RocketMQ 重試機制詳解及最佳實踐

生産者發送重試

RocketMQ 的生産者在發送消息到服務端時,可能會因為網絡問題,服務異常等原因導緻調用失敗,這時候應該怎麼辦?如何盡可能的保證消息不丢失呢?

1. 生産者重試次數

RocketMQ 在用戶端中内置了請求重試邏輯,支援在初始化時配置消息發送最大重試次數(預設為 2 次),失敗時會按照設定的重試次數重新發送。直到消息發送成功,或者達到最大重試次數時結束,并在最後一次失敗後傳回調用錯誤的響應。對于同步發送和異步發送,均支援消息發送重試。

  • 同步發送:調用線程會一直阻塞,直到某次重試成功或最終重試失敗(傳回錯誤碼或抛出異常)。
  • 異步發送:調用線程不會阻塞,但調用結果會通過回調的形式,以異常事件或者成功事件傳回。

2. 生産者重試間隔

在介紹生産者重試前,我們先來了解下流控的概念,流控一般是指服務端壓力過大,容量不足時服務端會限制用戶端收發消息的行為,是服務端自我保護的一種設計。RocketMQ 會根據目前是否觸發了流控而采用不同的重試政策:

非流控錯誤場景:其他觸發條件觸發重試後,均會立即進行重試,無等待間隔。

流控錯誤場景:系統會按照預設的指數退避政策進行延遲重試。

  • 為什麼要引入退避和随機抖動?

如果故障是由過載流控引起的,重試會增加服務端負載,導緻情況進一步惡化,是以用戶端在遇到流控時會在兩次嘗試之間等待一段時間。每次嘗試後的等待時間都呈指數級延長。指數回退可能導緻很長的回退時間,因為指數函數增長很快。指數退避算法通過以下參數控制重試行為,更多資訊,請參見 connection-backoff.md。

INITIAL_BACKOFF:第一次失敗重試前後需等待多久,預設值:1 秒;

MULTIPLIER :指數退避因子,即退避倍率,預設值:1.6;

JITTER :随機抖動因子,預設值:0.2;

MAX_BACKOFF :等待間隔時間上限,預設值:120 秒;

MIN_CONNECT_TIMEOUT :最短重試間隔,預設值:20 秒。

ConnectWithBackoff()
  current_backoff = INITIAL_BACKOFF
  current_deadline = now() + INITIAL_BACKOFF
  while (TryConnect(Max(current_deadline, now() + MIN_CONNECT_TIMEOUT))!= SUCCESS)
    SleepUntil(current_deadline)
    current_backoff = Min(current_backoff * MULTIPLIER, MAX_BACKOFF)
    current_deadline = now() + current_backoff + UniformRandom(-JITTER * current_backoff, JITTER * current_backoff)           

特别說明:對于事務消息,隻會進行透明重試(transparent retries),網絡逾時或異常等場景不會進行重試。

3. 重試帶來的副作用

不停的重試看起來很美好,但也是有副作用的,主要包括兩方面:消息重複,服務端壓力增大

  • 遠端調用的不确定性,因請求逾時觸發消息發送重試流程,此時用戶端無法感覺服務端的處理結果;用戶端進行的消息發送重試可能會導緻消費方重複消費,應該按照使用者ID、業務主鍵等資訊幂等處理消息。
  • 較多的重試次數也會增大服務端的處理壓力。

4. 使用者的最佳實踐是什麼

1)合理設定發送逾時時間,發送的最大次數

發送的最大次數在初始化用戶端時配置在 ClientConfiguration;對于某些實時調用類場景,可能會導緻消息發送請求鍊路被阻塞導緻業務請求整體耗時高或耗時;需要合理評估每次調用請求的逾時時間以及最大重試次數,避免影響全鍊路的耗時。

2)如何保證發送消息不丢失

由于分布式環境的複雜性,例如網絡不可達時 RocketMQ 用戶端發送請求重試機制并不能保證消息發送一定成功。業務方需要捕獲異常,并做好備援保護處理,常見的解決方案有兩種:

    1. 向調用方傳回業務處理失敗;
    2. 嘗試将失敗的消息存儲到資料庫,然後由背景線程定時重試,保證業務邏輯的最終一緻性。

3)關注流控異常導緻無法重試

觸發流控的根本原因是系統容量不足,如果因為突發原因觸發消息流控,且用戶端内置的重試流程執行失敗,則建議執行服務端擴容,将請求調用臨時替換到其他系統進行應急處理。

4)早期版本用戶端如何使用故障延遲機制進行發送重試?

對于 RocketMQ 4.x 和 3.x 以下用戶端開啟故障延遲機制可以用:

producer.setSendLatencyFaultEnable(true)

配置重試次數使用:

producer.setRetryTimesWhenSendFailed()
producer.setRetryTimesWhenSendAsyncFailed()

消費者消費重試

消息中間件做異步解耦時的一個典型問題是如果下遊服務處理消息事件失敗,那應該怎麼做呢?

RocketMQ 的消息确認機制以及消費重試政策可以幫助分析如下問題:

  • 如何保證業務完整處理消息?

消費重試政策可以在設計實作消費者邏輯時保證每條消息處理的完整性,避免部分消息消費異常導緻業務狀态不一緻。

  • 業務應用異常時進行中的消息狀态如何恢複?

當系統出現異常(當機故障)等場景時,進行中的消息狀态如何恢複,消費重試具體行為是什麼。

1. 什麼是消費重試?

  • 什麼時候認為消費失敗?

    消費者在接收到消息後将調用使用者的消費函數執行業務邏輯。如果用戶端傳回消費失敗 ReconsumeLater,抛出非預期異常,或消息處理逾時(包括在 PushConsumer 中排隊逾時),隻要服務端服務端一定時間内沒收到響應,将認為消費失敗。

  • 消費重試是什麼?

    消費者在消費某條消息失敗後,服務端會根據重試政策重新向用戶端投遞該消息。超過一次定數後若還未消費成功,則該消息将不再繼續重試,直接被發送到死信隊列中;

  • 重試過程狀态機:消息在重試流程中的狀态和變化邏輯;
  • 重試間隔:上一次消費失敗或逾時後,下次重新嘗試消費的間隔時間;
  • 最大重試次數:消息可被重試消費的最大次數。

2. 消息重試的場景

需要注意重試是應對異常情況,給予程式再次消費失敗消息的機會,不應該被用作常态化的鍊路。

推薦使用場景:

  • 業務處理失敗,失敗原因跟目前的消息内容相關,預期一段時間後可執行成功;
  • 是一個小機率事件,對于大批的消息隻有很少量的失敗,後面的消息大機率會消費成功,是非常态化的。

正例:消費邏輯是扣減庫存,極少量商品因為樂觀鎖版本沖突導緻扣減失敗,重試一般立刻成功。

錯誤使用場景:

  • 消費處理邏輯中使用消費失敗來做條件判斷的結果分流,是不合理的。

反例:訂單在資料庫中狀态已經是已取消,此時如果收到發貨的消息,處理時不應傳回消費失敗,而應該傳回成功并标記不用發貨。

  • 消費進行中使用消費失敗來做處理速率限流,是不合理的。

    限流的目的是将超出流量的消息暫時堆積在隊列中達到削峰的作用,而不是讓消息進入重試鍊路。

    這種做法會讓消息反複在服務端和用戶端之間傳遞,增大了系統的開銷,主要包括以下方面:

    • RocketMQ 内部重試涉及寫放大,每一次重試将生成新的重試消息,大量重試将帶來嚴重的 IO 壓力;
    • 重試有複雜的退避邏輯,内部實作為梯度定時器,該定時器本身不具備高吞吐的特性,大量重試将導緻重試消息無法及時出隊。重試的間隔将不穩定,将導緻大量重試消息延後消費,即削峰的周期被大幅度延長。

3. 不要以重試替代限流

上述誤用的場景實際上是組合了限流和重試能力來進行削峰,RocketMQ 推薦的削峰最佳手段為組合限流和堆積,業務以保護自身為前提,需要對消費流量進行限流,并利用 RocketMQ 提供的堆積能力将超出業務目前處理的消息滞後消費,以達到削峰的目的。下圖中超過處理能力的消息都應該被堆積在服務端,而不是通過消費失敗進行重試。

RocketMQ 重試機制詳解及最佳實踐

如果不想依賴額外的産品/元件來完成該功能,也可以利用一些本地工具類,比如 Guava 的 RateLimiter 來完成單機限流。如下所示,聲明一個 50 QPS 的 RateLimiter,在消費前以阻塞的方式 acquire 一個令牌,擷取到即處理消息,未擷取到阻塞。

RateLimiter rateLimiter = RateLimiter.create(50);
PushConsumer pushConsumer = provider.newPushConsumerBuilder()
    .setClientConfiguration(clientConfiguration)
    // 設定訂閱組名稱
    .setConsumerGroup(consumerGroup)
    // 設定訂閱的過濾器
    .setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))
    .setMessageListener(messageView -> {
        // 阻塞直到獲得一個令牌,也可以配置一個逾時時間
        rateLimiter.acquire();
        LOGGER.info("Consume message={}", messageView);
        return ConsumeResult.SUCCESS;
    })
    .build();           

4. PushConsumer 消費重試政策

PushConsumer 消費消息時,消息的幾個主要狀态如下:

RocketMQ 重試機制詳解及最佳實踐
  • Ready:已就緒狀态。消息在消息隊列RocketMQ版服務端已就緒,可以被消費者消費;
  • Inflight:進行中狀态。消息被消費者用戶端擷取,處于消費中還未傳回消費結果的狀态;
  • Commit:送出狀态。消費成功的狀态,消費者傳回成功響應即可結束消息的狀态機;
  • DLQ:死信狀态

    消費邏輯的最終兜底機制,若消息一直處理失敗并不斷進行重試,直到超過最大重試次數還未成功,此時消息不會再重試。

    該消息會被投遞至死信隊列。您可以通過消費死信隊列的消息進行業務恢複。

  • 最大重試次數

PushConsumer 的最大重試次數由建立時決定。

例如,最大重試次數為 3 次,則該消息最多可被投遞 4 次,1 次為原始消息,3 次為重試投遞次數。

  • 重試間隔時間
  • 無序消息(非順序消息):重試間隔為階梯時間,具體時間如下:

說明:若重試次數超過 16 次,後面每次重試間隔都為 2 小時。

RocketMQ 重試機制詳解及最佳實踐
  • 順序消息:重試間隔為固定時間,預設為 3 秒。

點選檢視原文,擷取更多福利!

https://developer.aliyun.com/article/1081768?utm_content=g_1000364677

版權聲明:本文内容由阿裡雲實名注冊使用者自發貢獻,版權歸原作者所有,阿裡雲開發者社群不擁有其著作權,亦不承擔相應法律責任。具體規則請檢視《阿裡雲開發者社群使用者服務協定》和《阿裡雲開發者社群知識産權保護指引》。如果您發現本社群中有涉嫌抄襲的内容,填寫侵權投訴表單進行舉報,一經查實,本社群将立刻删除涉嫌侵權内容。

繼續閱讀