1. 概覽
在分布式場景中,Retry 和 Fallback 是最常見的容災方案。
Retry 就是在調用遠程接口失敗時,Client 主動發起重試請求,以期待獲得最終結果,從而完成整個流程
Fallback 是在調用遠程接口失敗時,Client 不進行重試而是調用一個特殊的 fallback 方法,從這個方法中獲取結果,使流程能夠繼續下去
那 Retry 和 Fallback 該怎么抉擇呢?
1.1. 背景
首先,先看下 Retry 和 Fallback 都是怎么幫助流程進行自我恢復的。
1.1.1. Retry
現在有一個生單流程:
核心流程如下:
根據商品信息創建訂單
將訂單保存到數據庫
如果發生網絡抖動,將導致生單失敗。
在調用商品服務獲取商品時,由于網絡異常,接口調用失敗
由于無法獲取商品信息,生單流程被異常中斷
由于生單流程太過重要,系統需盡最大努力保障用戶能夠完成下單操作,那針對網絡抖動這個問題,可以通過 Retry 進行修復。
在第一次獲取商品信息時,由于網絡問題導致獲取失敗
系統不會直接拋出異常,而是在等待一段時間后,重新發起第二次請求,也就是 Retry 操作
網絡恢復,第二次請求成功獲取商品信息
流程繼續運行,最終完成用戶生單
Retry 機制非常適合服務短時間不可用,或某個服務節點異常 這類場景。
1.1.2. Fallback
一個生單驗證接口,主流程如下:
調用商品服務的接口獲取商品信息
根據商品和用戶信息判斷用戶是否能夠購買該商品
同樣,假設在訪問商品服務時出現網絡異常:
由于無法獲取商品信息,從而導致整個驗證流程被異常中斷,用戶操作被迫終止。
聰明的你估計會說那就使用 Retry 呀,是的:
如果是短時不可用,通過 Retry 機制便可以恢復流程。
但,如果是商品服務壓力過大,響應時間過長呢?比如,商品服務流量激增,導致 DB CPU 飆升,出現大量的慢 SQL,這時觸發了系統的 Retry 會是怎樣?
在獲取商品失敗后,系統自動觸發 Retry 機制
由于是商品服務本身出了問題,第二次請求仍舊失敗
服務又觸發了第三次請求,仍未獲取結果
達到最大重試次數,仍舊無法獲取商品,只能通過異常中斷用戶請求
通過 Retry 機制未能將流程從異常中恢復過來,也給下游的 商品服務 造成了巨大傷害。
商品服務壓力大,響應時間長
上游系統由于超時觸發自動重試
自動重試增大了對商品服務的調用
商品服務請求量更大,更難以從故障中恢復
這就是常說的“讀放大”,假設用戶驗證是否能夠購買請求的請求量為 n,那極端情況下 商品服務的請求量為 3n (其中 2n 是由 Retry 機制造成)
此時,Retry 就不是一個好的方案。我們先退回業務場景進行思考,如果無法獲取商品,驗證接口是否可以直接放行,先讓用戶完成購買?
如果,這個業務假設能夠接受的話,那就到了 Fallback 上場的時候了。
調用商品服務獲取商品信息失敗
系統不會進行重試,而是觸發 fallback 機制
fallback 會調用指定的一個方法,并將返回值作為遠程接口的返回值
接下來的流程使用 fallback 方法的返回值完成業務邏輯
1.1.3. 場景思考
同樣是對商品服務接口(同一個接口)的調用,在不同的場景需要使用不同的策略用以恢復業務流程,通常情況下:
Command 場景優先使用 Retry
這種流量即為重要,最好能保障流程的完整性
通常寫流量比較小,小范圍 Retry 不會對下游系統造成巨大影響
Query 場景優選使用 Fallabck
大多數展示場景,哪怕部分信息沒有獲取到對整體的影響也比較小
通常讀場景流量較高,Retry 對下游系統的傷害不容忽視
那面對一個遠程接口被多個場景使用,我們該怎么處理呢?
提供兩組接口,一個具有 Retry 能力,一個具有 Fallback 能力,由使用方根據業務場景進行選擇?
還是…
1.2. 目標
遠程接口具備 Retry 和 Fallback 能力
能夠根據上下文不同場景,在發生調用異常時動態選擇 Retry 或 Fallback 進行流程恢復
2. 快速入門
2.1. 準備環境
項目主要依賴 spring retry 和 lego starter首先,引入 spring-retry 依賴
org.springframework.retry spring-retry
此次,引入 lego-starter 依賴
com.geekhalo.lego lego-starter 0.1.17
最后新建 RetryConfiguration 以開啟 Retry 能力
@EnableRetry @Configuration publicclassRetryConfiguration{ }
2.2. 構建 ActionTypeProvider
在完成基本配置后,需要準備一個 ActionTypeProvider 用以提供上下文信息。ActionTypeProvider 接口定義如下:
publicinterfaceActionTypeProvider{ ActionTypeget(); } publicenumActionType{ COMMAND,QUERY }
通常情況下,我們會使用 ThreadLocal 組件將 ActionType 存儲于線程上下文,在使用時從上下中獲取相關信息。
publicclassActionContext{ privatestaticfinalThreadLocalACTION_TYPE_THREAD_LOCAL=newThreadLocal<>(); publicstaticvoidset(ActionTypeactionType){ ACTION_TYPE_THREAD_LOCAL.set(actionType); } publicstaticActionTypeget(){ returnACTION_TYPE_THREAD_LOCAL.get(); } publicstaticvoidclear(){ ACTION_TYPE_THREAD_LOCAL.remove(); } }
有了上下文之后,ActionBasedActionTypeProvider 直接從 Context 中獲取 ActionType 具體如下
@Component publicclassActionBasedActionTypeProviderimplementsActionTypeProvider{ @Override publicActionTypeget(){ returnActionContext.get(); } }
上下文中的 ActionType 又是怎么進行管理的呢,包括信息綁定和信息清理?最常用的方式便是:
提供一個注解,在方法上添加注解用于對 ActionType 的配置;
提供一個攔截器,對方法調用進行攔截。方法調用前,從注解中獲取配置信息并綁定到上下文;方法調用后,主動清理上下文信息;
核心實現為:
@Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public@interfaceAction{ ActionTypetype(); } @Aspect @Component @Order(Integer.MIN_VALUE) publicclassActionAspect{ @Pointcut("@annotation(com.geekhalo.lego.faultrecovery.smart.Action)") publicvoidpointcut(){ } @Around(value="pointcut()") publicObjectaction(ProceedingJoinPointjoinPoint)throwsThrowable{ MethodSignaturemethodSignature=(MethodSignature)joinPoint.getSignature(); Actionannotation=methodSignature.getMethod().getAnnotation(Action.class); ActionContext.set(annotation.type()); try{ returnjoinPoint.proceed(); }finally{ ActionContext.clear(); } } }
在這些組件的幫助下,我們只需在方法上基于 @Action 注解進行標記,便能夠將 ActionType 綁定到上下文。
2.3. 使用 @SmartFault
在將 ActionType 綁定到上下文之后,接下來要做的便是對 遠程接口 進行配置。遠程接口的配置工作主要由 @SmartFault 來完成。其核心配置項包括:
配置項 | 含義 | 默認配置 |
---|---|---|
recover | fallback 方法名稱 | |
maxRetry | 最大重試次數 | 3 |
include | 觸發重試的異常類型 | |
exclude | 不需要重新的異常類型 |
接下來,看一個 demo
@Service @Slf4j @Getter publicclassRetryService3{ privateintcount=0; privateintretryCount=0; privateintfallbackCount=0; privateintrecoverCount=0; publicvoidclean(){ this.retryCount=0; this.fallbackCount=0; this.recoverCount=0; } /** *Command請求,啟動重試機制 */ @Action(type=ActionType.COMMAND) @SmartFault(recover="recover") publicLongretry(Longinput)throwsThrowable{ this.retryCount++; returndoSomething(input); } /** *Query請求,啟動Fallback機制 */ @Action(type=ActionType.QUERY) @SmartFault(recover="recover") publicLongfallback(Longinput)throwsThrowable{ this.fallbackCount++; returndoSomething(input); } @Recover publicLongrecover(Throwablee,Longinput){ this.recoverCount++; log.info("recover-{}",input); returninput; } privateLongdoSomething(Longinput){ //偶數拋出異常 if(count++%2==0){ log.info("Error-{}",input); thrownewRuntimeException(); } log.info("Success-{}",input); returninput; } }
測試代碼如下:
@SpringBootTest(classes=DemoApplication.class) publicclassRetryService3Test{ @Autowired privateRetryService3retryService; @BeforeEach publicvoidsetup(){ retryService.clean(); } @Test publicvoidretry()throwsThrowable{ for(inti=0;i100;?i++){ ????????????retryService.retry(i?+?0L); ????????} ????????Assertions.assertTrue(retryService.getRetryCount()?>0); Assertions.assertTrue(retryService.getRecoverCount()==0); Assertions.assertTrue(retryService.getFallbackCount()==0); } @Test publicvoidfallback()throwsThrowable{ for(inti=0;i100;?i++){ ????????????retryService.fallback(i?+?0L); ????????} ????????Assertions.assertTrue(retryService.getRetryCount()?==?0); ????????Assertions.assertTrue(retryService.getRecoverCount()?>0); Assertions.assertTrue(retryService.getFallbackCount()>0); } }
運行 retry 測試,日志如下:
[main]c.g.l.c.f.smart.SmartFaultExecutor:actiontypeisCOMMAND [main]c.g.l.faultrecovery.smart.RetryService3:Error-0 [main]c.g.l.c.f.smart.SmartFaultExecutor:Retrymethodpublicjava.lang.Longcom.geekhalo.lego.faultrecovery.smart.RetryService3.retry(java.lang.Long)throwsjava.lang.Throwableuse[0] [main]c.g.l.faultrecovery.smart.RetryService3:Success-0
可見,當 action type 為 COMMAND 時:
第一次調用時,觸發異常,打印: Error-0
此時 SmartFaultExecutor 主動進行重試,打印: Retry method xxxx
方法重試成功,RetryService3 打印: Success-0
方法主動進行重試,流程從異常中恢復,處理過程和效果符合預期。
運行 fallback 測試,日志如下:
[main]c.g.l.c.f.smart.SmartFaultExecutor:actiontypeisQUERY [main]c.g.l.faultrecovery.smart.RetryService3:Error-0 [main]c.g.l.c.f.smart.SmartFaultExecutor:recoverFromERRORformethodReflectiveMethodInvocation:publicjava.lang.Longcom.geekhalo.lego.faultrecovery.smart.RetryService3.fallback(java.lang.Long)throwsjava.lang.Throwable;targetisofclass[com.geekhalo.lego.faultrecovery.smart.RetryService3] [main]c.g.l.faultrecovery.smart.RetryService3:recover-0
可見,當 action type 為 QUERY 時:
第一次調用時,觸發異常,打印: Error-0
SmartFaultExecutor 執行 Fallback 策略,打?。簉ecover From ERROR for method xxxx
調用RetryService3的 recover 方法,獲取最終返回值。RetryService3 打?。簉ecover-0
異常后自動執行 fallback,將流程從異常中恢復過來,處理過程和效果符合預期。
3. 設計&擴展
3.1 核心設計
整體流程如下:
ActionAspect 從 @Action 中讀取配置信息,將請求類型綁定到線程上下文
然后執行正常業務邏輯
當調用 @SmartFault 注解的方法時,會被 SmartFaultMethodInterceptor 攔截器攔截
攔截器通過 ActionTypeProvider 獲取當前的 ActionType
根據 ActionType 對請求進行路由
如果是 COMMAND 操作,將使用 RetryTemplate 執行請求,在發生異常時,通過重試配置進行請求重發,從而最大限度的獲得遠程結果
如果是 QUERY 操作,將使用 FallbackTemplate(重試次數為0的 RetryTemplate)執行請求,當發生異常時,調用 fallback 方法,執行配置的 recover 方法,直接使用返回結果
獲取遠程結果后,執行后續的業務邏輯
最后,ActionAspect 將 ActionType 從線程上下文中移除。
審核編輯:劉清
-
cpu
+關注
關注
68文章
10901瀏覽量
212770 -
SQL
+關注
關注
1文章
773瀏覽量
44228 -
數據庫
+關注
關注
7文章
3845瀏覽量
64618
原文標題:容災方案:Retry 和 Fallback 該怎么抉擇?
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論