重試機制是許多現代軟體系統的關鍵元件。它允許我們的系統自動重試失敗的操作,以從暫時性錯誤或網路中斷中恢復。透過自動重試失敗的操作,重試機制可以幫助軟體系統從意外故障中恢復並繼續正常執行。
今天,我們就來看看這些話題:
- 什麼是重試模式?
- 它的用途是什麼?為什麼我們需要在我們的系統中實現它?
- 僅應重試某些請求。重要的是要了解可以重試下游服務中的哪些型別的錯誤,以避免出現業務邏輯問題。
- 當我們向下遊服務重試請求時,失敗後應該等待多長時間才能再次傳送請求?
- 我們將研究從基本到更復雜的重試方法。
什麼是重試模式?
重試是當向下遊服務的請求失敗時傳送相同請求的行為。透過使用重試模式,您將提高系統的下游彈性方面。當呼叫下游服務發生錯誤時,我們的系統會嘗試再次呼叫它,而不是向上遊服務返回錯誤。
那麼,究竟為什麼我們需要這樣做呢?近幾十年來,微服務架構越來越受歡迎。雖然這種方法有很多好處,但微服務架構的缺點之一是在服務之間引入網路通訊。當服務相互通訊時,額外的網路通訊可能會導致網路中出現錯誤(請閱讀分散式計算的謬誤)。每次呼叫其他服務都有可能出現這些錯誤。
此外,無論您使用的是單體架構還是微服務架構,您很有可能仍然需要呼叫不在公司內部網路內的其他服務。在不同網路內呼叫服務意味著您的請求將經過更多網路層並且失敗的可能性更大。
除了網路錯誤之外,您還可能遇到系統錯誤,例如速率限制錯誤、服務停機和處理超時。您收到的錯誤可能適合重試,也可能不適合重試。讓我們進入下一節來更詳細地探討它。
何時重試您的請求
儘管在系統中新增重試機制通常是一個好主意,但並非對下游服務的每個請求都應該重試。作為一個簡單的基準,當您想要重試時應該考慮以下事項:
這是暫時性錯誤嗎?
您需要考慮您收到的錯誤型別是否是暫時的(暫時的)。例如,您可以重試連線超時錯誤,因為它通常只是暫時的,但不是錯誤的請求錯誤,因為您需要更改請求。
是系統錯誤嗎?
當您從下游服務收到錯誤訊息時,它可以歸類為系統錯誤或應用程式錯誤。系統錯誤一般是可以重試的,因為你的請求還沒有被下游服務處理。另一方面,應用程式錯誤通常意味著您的請求有問題,您不應該重試。例如,如果您從下游服務收到錯誤請求錯誤,則無論您重試多少次,您都將始終收到相同的錯誤。
冪等性
即使您從下游服務收到錯誤,它仍然有可能處理您的請求。下游服務可以在處理完主程序後傳送錯誤,但另一個子程序會導致錯誤。冪等API意味著即使API兩次收到相同的請求,它也只會處理第一個請求。我們可以透過在請求中新增一些該請求唯一的 ID 來實現,以便下游服務可以確定是否應該處理該請求。通常,您可以透過方法來區分Request。GET、DELETE、 和PUT通常是冪等的,但POST不是。但是,您需要向服務所有者確認API的冪等性。
重試的成本
當您重試向下遊服務發出請求時,將會有額外的資源使用。額外的資源使用可以是額外的 CPU 使用、阻塞執行緒、額外的記憶體使用、額外的頻寬使用等形式。您需要考慮這一點,特別是如果您的服務預計有大流量。
重試機制的實施成本
許多程式語言已經有一個實現重試機制的庫,但您仍然需要確定重試哪個請求。如果您願意,您還可以建立您的重試機制或每個系統,但是當然,這意味著重試機制的實現成本很高。
整理了一些常見錯誤以及它們是否適合重試:
- 連線超時:您的應用無法連線下游服務;因此,下游服務不知道您的請求,您可以重試。
- 讀取超時:下游應用已處理您的請求,但長時間未返回任何響應。
- 斷路器跳閘:如果您在服務中使用斷路器,則會出現錯誤。您可以重試此類錯誤,因為您的服務尚未將其請求傳送到下游服務。
- 400 - 錯誤請求:此錯誤意味著您對下游服務的請求在驗證後被標記為錯誤請求。您不應該重試此錯誤,因為如果請求相同,它總是會返回相同的錯誤。
- 401 - 未經授權:您需要在傳送請求之前授權。是否可以重試此錯誤將取決於身份驗證方法和錯誤。但一般來說,如果您的請求相同,您總是會得到相同的錯誤。
- 429 - 請求過多:您的請求受到下游服務的速率限制。您可以重試此錯誤,但您應該與下游服務的所有者確認您的請求將受到速率限制的時間。
- 500 - 內部伺服器錯誤:這意味著下游服務已開始處理您的請求,但在中間失敗。通常,重試此錯誤就可以了。
- 503 - 服務不可用:下游服務因停機而不可用。遇到這種錯誤重試一下就可以了。
重試退避期
當您的請求無法到達下游服務時,您的系統將需要等待一段時間才能重試。這段時間稱為重試退避期。
一般來說,呼叫之間的等待時間有三種策略:固定退避、指數退避和隨機退避。他們三個都有各自的優點和缺點。您使用哪一種應取決於您的 API 和服務用例。
1、固定退避
固定退避意味著每次重試請求時,請求之間的延遲始終相同。例如,如果您以 5 秒的退避時間重試兩次,那麼如果第一次呼叫失敗,則將在 5 秒後傳送第二個請求。如果再次失敗,則會在失敗後5秒傳送第三次呼叫。
固定的退避期適合直接來自使用者且需要快速響應的請求。如果請求很重要並且您需要它儘快返回,那麼您可以將退避期設定為無或接近0。
2、指數退避
當下遊服務出現問題時,並不總是能很快恢復。當下遊服務嘗試恢復時,您不想做的就是在短時間內多次命中它。指數退避的工作原理是在每次我們的服務嘗試呼叫下游服務時新增一些額外的退避時間。
例如,我們可以將重試機制配置為 5 秒初始退避,並在每次嘗試時新增 2 作為乘數。這意味著當我們對下游服務的第一次呼叫失敗時,我們的服務將在下一次呼叫之前等待 5 秒。如果第二次呼叫再次失敗,服務將在下一次呼叫之前等待 10 秒而不是 5 秒。
由於其較長的間隔性質,指數退避不適合重試使用者請求。但它非常適合通知、傳送電子郵件或 Webhook 系統等後臺程序。
3、隨機退避
隨機退避 是一種在退避間隔計算中引入隨機性的退避策略。假設您的服務流量激增。然後,您的服務會為每個請求呼叫下游服務,然後您會從中收到錯誤,因為下游服務會被您的請求淹沒。您的服務實現了重試機制,並將在 5 秒內重試請求。但有一個問題:當需要重試請求時,所有請求都會立即重試,您可能會再次從下游服務收到錯誤。利用隨機退避機制引入的隨機性,您可以避免這種情況。
隨機退避策略將透過引入隨機重試值來幫助您的服務將請求平衡到下游服務。假設您將重試機制配置為 5 秒間隔和兩次重試。如果第一次呼叫失敗,可以在 500ms 後嘗試第二次呼叫;如果再次失敗,則可以在 3.8 秒後嘗試第三次。如果許多請求使下游服務失敗,則不會同時重試它們。
在哪裡儲存重試狀態
進行重試時,您需要將重試的狀態儲存在某處。狀態包括已進行的重試次數、要重試的請求以及要儲存的其他後設資料。一般來說,可以使用三個地方來儲存重試狀態,如下所示:
1、執行緒
執行緒是儲存重試狀態最常見的地方。如果您使用具有內建重試機制的庫,它很可能會使用 來Thread儲存狀態。最簡單的方法就是讓執行緒sleep。
讓我們看一個 Java 中的例子:
int retryCount = 0; |
上面的程式碼基本上是在出現異常時sleep執行緒,然後再次呼叫程序。這種方法雖然簡單,但缺點是會阻塞執行緒,導致其他程序無法使用該執行緒。這種方法適用於間隔時間較短的固定延遲策略,如直接響應使用者並需要儘快響應的程序。
訊息傳遞
我們可以使用 RabbitMQ(延遲佇列)等流行的訊息傳遞代理來儲存重試狀態。從上游收到請求後,如果處理失敗(可能是下游服務的原因,也可能不是),可以將訊息釋出到延遲佇列,以便稍後(根據延遲情況)再處理。
使用訊息傳遞儲存重試狀態適用於後臺程序請求,因為上游服務無法直接獲取重試程序的響應。使用這種方法的好處是通常很容易實現,因為代理/庫已經支援重試功能。訊息傳遞作為重試狀態的儲存系統,在分散式系統中也能很好地發揮作用。可能發生的一個問題是,你的服務在等待下一次重試時突然出現停機等問題。透過將重試狀態儲存在訊息代理中,您的服務可以在問題解決後繼續重試。
資料庫
資料庫是儲存重試狀態的最可定製的解決方案,可以使用永續性儲存或記憶體 KV 儲存(如 Redis)。當下遊服務請求失敗時,可以將資料儲存在資料庫中,並使用 cron 作業每秒或每分鐘檢查資料庫,重試失敗的訊息。
雖然這是最可定製的解決方案,但實施成本會很高,因為你需要實現重試機制。您可以在服務中建立該機制,但在重試時會犧牲一些效能,或者為重試目的建立一個全新的服務。
總結
本文探討了什麼是重試模式以及在實施重試模式時應考慮哪些方面。
您需要了解什麼請求以及如何重試。如果重試機制實施得當,將有助於改善使用者體驗和減少所構建服務的執行。但是,如果重試機制不正確,就有可能導致使用者體驗惡化和業務錯誤。您需要了解請求何時可以重試以及如何重試,這樣才能正確實施該機制。
在本文中,我們介紹了重試模式。這種模式提高了系統的下游彈性,但下游彈性的意義遠不止於此。我們可以將重試模式與超時(我們在本文中探討過)和斷路器結合起來,使我們的系統對下游故障更具彈性。