在日常的編碼過程中,無論是和本地服務相關的本機資源交互,還是和本地服務相關的遠程資源甚至是遠程服務進行交付,都可能會遇到失敗(異常),這時候,我們最常見的做法就是重試,本文將和大家介紹一下如何正確實現重試。

什麼是重試

重試:即從新嘗試,以觀察結果是否符合預期。

to try (something) again to see if it is successful, working, or satisfactory。

在生活中,以買彩票爲例,再次嘗試購買彩票有以下幾種情況:

  • 彩票沒中(結果不符合預期)

  • 上次沒帶錢(條件不符合)

  • 彩票門店沒開門(結果異常)

圖形化的表述,可以簡化爲:

什麼是正確的重試

和任何的鍥而不捨都需要向着現實低頭一樣,“重試”也需要有終止條件(即有條件的重試),想象一樣買彩票的場景,如果屢次不中,一直嘗試不停歇,那不是得破產嗎?

在日常的編碼中,我們最常見的做法也是如此,即指定一個重試次數的上限,然後單次請求達到上限後返回。但是這樣做了就沒有問題了嗎?答案當然是否定的。

▐   固定循環次數方式

這是最常見的版本,樣板方法爲:

比如:

這種方式的問題在於: 不帶back of f的重試,對於下游來說會在失敗發生時進一步遇到更多的請求壓力,繼而進一步惡化。

▐   帶固定 delay 的方式

在失敗之後,進行固定間隔的delay, delay 的方式按照是方法本身是異步還是同步的,可以通過定時器或則簡單的Thread.sleep 實現,樣板方法爲:

比如:

這種方式的問題在於: 雖然這次帶了固定間隔的backoff,但是每次重試的間隔固定,此時對於下游資源的衝擊將會變成間歇性的脈衝;特別是當集羣都遇到類似的問題時,步調一致的脈衝,將會最終對資源造成很大的衝擊,並陷入失敗的循環中。

想想一下,一羣鼓手,協調一致地擊鼓時所產生的效果。

▐  帶隨機delay的方式:

和 2 中固定間隔的delay不一樣,現在採用隨機backoff的方式,即具體的delay時間,在一個最小值和最大值之間浮動,樣板代碼如下:

比如:

或則一個類似的異步版本:

這種方式的問題在於:雖然現 在解決了backoff的時間集中的問題,對時間進行了隨機打散,但是依然存在下面的問題:

  • 如果依賴的底層服務持續地失敗,改方法依然會進行固定次數的嘗試,並不能起到很好的保護作用

  • 對結果是否符合預期,是否需要進行重試依賴於異常

  • 無法針對異常進行精細化的控制,如只針部分異常進行重試。

▐   可進行細粒度控制的重試

比如可以針對特定的異常來說,其樣板代碼爲:

一般這個時候,代碼已經相對來說比較複雜了,個人推薦使用resilience4j-retry或則 spring-retry等庫來進行組合,減少自己編寫時維護成本,比如以 resilience4j-retry 爲例,其可以使用配置代碼對重試策略進行細粒度的控制,比如:

RetryConfig config = RetryConfig.custom()

.maxAttempts(2)

.waitDuration(Duration.ofMillis(1000))

.retryOnResult(response -> response.getStatus() == 500)

.retryOnException(e -> e instanceof WebServiceException)

.retryExceptions(IOException.class, TimeoutException.class)

.ignoreExceptions(BunsinessException.class, OtherBunsinessException.class)

.build();

RetryRegistry registry = RetryRegistry.of(config);

Retry retryWithDefaultConfig = registry.retry("name1");

CheckedFunction0<String> retryableSupplier = Retry

.decorateCheckedSupplier(retry, helloWorldService::sayHelloWorld);

這種方式的問題在於: 雖然可以比較好的控制重試策略,但是對於下游資源持續性的失敗,依然沒有很好的解決。當持續的失敗時,對下游也會造成持續性的壓力。一般這種問題的解法,我們日常工作中都是通過一個開關來進行人工斷路,另一個比較好的解法是和斷路器結合。

和斷路器結合

斷路器  在每個家庭中都有,但是在軟件工程上,看到大家應用的並不多。 斷路器模式  一般用在當下遊資源失敗後,但是失敗恢復的時間不固定時,自動地進行探索式地恢復嘗試,並且在遇到較多失敗時,能夠快速自動地斷開,從而避免失敗蔓延的一種模式。

有人將這種模式叫做『熔斷器模式』,其實是錯誤的,能夠「熔斷」的,那是保險絲,而不是斷路器,斷路器來自於電氣工程,如下圖示:

在應用斷路器時,需要對下游資源的每次調用都通過斷路器,對代碼具備一定的結構侵入性。常見的有Hystrix 或 resilience4j .

// Given

CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");


// When I decorate my function

CheckedFunction0<String> decoratedSupplier = CircuitBreaker

.decorateCheckedSupplier(circuitBreaker, () -> "This can be any method which returns: 'Hello");


又或者

def callWithCircuitBreakerCS[T](body: Callable[CompletionStage[T]]): CompletionStage[T]

當斷路器處於開斷狀態時,所有的請求都會直接失敗,不再會對下游資源造成衝擊,並能夠在一段時間後,進行探索式的嘗試,如果沒有達到條件,可以自動地恢復到之前的閉合狀態。

重試的一些其他實現

目前 重試 在 RxJava 、Reactor、Akka-Stream 等中也都有實現,不過所實現的組合子(operator/操作)實現的相對簡單,在實踐中,如果需要得到很好的效果,還需要配合斷路器來進行,從而最大限度地進行保護下游。

對失敗做出反應

反應式宣言 中,也有提到,對對失敗做出反應,系統在遇到失敗時,可以恢復,並隔離失敗的組件,而不是不受控的失敗。系統是否具備回彈性,對於線上正常安全生產有很大的影響。正確地實現“重試”,只是整個大圖中非常小的一環,實際生產中還需要從架構、生產流程、編碼細節處理,監控報警等多種手段入手。

失敗(和“錯誤”相對照)

失敗是一種服務內部的意外事件, 會阻止服務繼續正常地運行。失敗通常會阻止對於當前的、 並可能所有接下來的客戶端請求的響應。和錯誤相對照, 錯誤是意料之中的,並且針各種情況進行了處理( 例如, 在輸入驗證的過程中所發現的錯誤), 將會作爲該消息的正常處理過程的一部分返回給客戶端。而失敗是意料之外的, 並且在系統能夠恢復至(和之前)相同的服務水平之前,需要進行干預。這並不意味着失敗總是致命的(fatal), 雖然在失敗發生之後, 系統的某些服務能力可能會被降低。錯誤是正常操作流程預期的一部分, 在錯誤發生之後, 系統將會立即地對其進行處理, 並將繼續以相同的服務能力繼續運行。失敗的例子有:硬件故障、 由於致命的資源耗盡而引起的進程意外終止,以及導致系統內部狀態損壞的程序缺陷。

回彈性: 系統在出現失敗時依然保持即時響應性。這不僅適用於高可用的、 任務關鍵型系統——任何不具備回彈性的系統都將會在發生失敗之後丟失即時響應性。回彈性是通過複製、 遏制、 隔離以及委託來實現的。失敗的擴散被遏制在了每個組件內部, 與其他組件相互隔離, 從而確保系統某部分的失敗不會危及整個系統,並能獨立恢復。每個組件的恢復都被委託給了另一個(外部的)組件, 此外,在必要時可以通過複製來保證高可用性。(因此)組件的客戶端不再承擔組件失敗的處理。

小結

寫這篇文章和大家分享,拋磚引玉,大家感興趣也可以看看自己負責的應用中目前對於重試的處理,以及一些主流的開源框架或者庫中的處理。

淘系 IM 消息平臺

我們負責阿里新零售領域 IM 消息平臺的建設,通過 IM即時通訊產品(push、聊天機器人、單聊、羣聊、消息號和聊天室)構建連接消費者和商家的溝通和觸達渠道,我們每天服務上億消費者和數百萬商家,處理百億級的消費規模,支撐了直播互動、客服服務、商家羣運營、品牌資訊、營銷推送等電商領域 BC 互通的業務場景;同時,我們在消費者的購物體驗上不斷探索創新——直播、AR試用、遊戲互動,爲新的購物玩法提供靈活穩定的基礎設施,實現阿里電商生態重要支點,爲上百家APP 提供安全、穩定、標準化的電商組件SDK。不斷提升消費這的體驗和活躍,提升商家服務的效率和能力,促進商家業務增長。

聯繫電話:18651806651

郵箱:postbox:: [email protected]

✿  拓展閱讀

作者| 虎鳴

編輯| 橙子君

出品| 阿里巴巴新零售淘系技術

相關文章