阿湯博客前兩篇文章《SpringCloud Zuul(Ribbon)重試配置不生效解決辦法》和《SpringCloud Feign重試不生效問題排查》已經介紹了Ribbon和Feign重試不生效的原因,且已經給出了解決辦法。
但是在經過了兩天的實際測試后發現,不同的超時時間配置、重試機制和熔斷時間,都會影響重試的實際效果。這里分幾個場景:
場景一:請求直接通過Zuul調用服務A,而此服務A的接口沒有其他服務的Feign調用,這種場景比較簡單,主要受Zuul的ribbon配置影響。

Zuul的重試參數配置:
ribbon: ConnectTimeout: 1000 ReadTimeout: 1000 MaxAutoRetries: 0 MaxAutoRetriesNextServer: 1 OkToRetryOnAllOperations: false hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 4000 zuul: retryable: true
這里A服務節點健康狀態分為兩種情況
情況一:
當A服務節點都出現故障,此時請求首先通過Zuul負載均衡訪問任意A服務節點比如A1,A1節點訪問超時,然后觸發MaxAutoRetriesNextServer=1的重試請求A2,然后A2節點返回超時,最后瀏覽器響收到非200狀態的返回,請求總用時ribbon.ReadTimeout * (1+MaxAutoRetriesNextServer)=2s左右,如下圖:

情況二:
當A2節點出現故障的時候,此時請求首先通過Zuul負載均衡訪問任意A服務節點,如果剛好此時負載到A2,那么請求超時觸發MaxAutoRetriesNextServer=1的重試請求A1,然后A1返回正常結果,最后瀏覽器響收到200狀態的正常結果,請求總用時為ribbon.ReadTimeout + A1處理請求的時間,大約1s左右。實際上當A2響應的失敗請求到達一定的數量或者百分比之后會觸發熔斷,那么在熔斷時間內,請求不會轉發到A2,熔斷的默認配置如下:
#當在配置時間窗口內達到此數量的失敗后,進行短路。默認20個
hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
#短路多久以后開始嘗試是否恢復,默認5s
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5
#出錯百分比閾值,當達到此閾值后,開始短路。默認50%
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50%
當把參數MaxAutoRetries更改為1時:
上面的情況一定會有4次超時請求,即A1節點超時,首先觸發MaxAutoRetries=1本節點A1重試和MaxAutoRetriesNextServer=1的A2節點重試,然后A2節點返回超時,再次觸發MaxAutoRetries=1本節點A2的重試,所以A1和A2都有2次處理請求,最后瀏覽器返回的請求總時間為:ribbon.ReadTimeout * (1+MaxAutoRetriesNextServer)*(1+MaxAutoRetries)=4s左右。

情況二最多會有3次請求,第一次請求故障節點A2,會有本節點A2的一次重試并超時,和A1節點的重試,并訪問正常結果,此時請求總用時為ribbon.ReadTimeout *(MaxAutoRetries +MaxAutoRetriesNextServer)+ A1處理請求的時間=2s左右。
場景二:這種場景實際使用過程中并不多見,這里主要為了測試Feign默認配置。

服務A已引入Spring Retry組件配置如下:
${spring.application.name}:
ribbon:
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 1
OkToRetryOnAllOperations: false
NFLoadBalancerRuleClassName: AvailabilityFilteringRule
feign:
client:
config:
default:
connectTimeout: 1000
readTimeout: 1000
這里簡單說下測試結果,不做詳細分析:
1、服務B節點都故障:
沒有配置ribbon的情況下,B1節點處理1次,重試1次,兩次失敗以后B1對B2重試1次,B2對B2重試一1次,或者訪問(B1/B2)1次,然后重試3次,然而和之前查的資料feign默認重試5次并不相同,最后總的超時請求時間為 feign.readTimeout * 4。
按照上面的配置以后,請求訪問任意節點一次,然后對另外一個節點進行重試一次,總的超時請求時間為feign.readTimeout * 2。
另外實際測試結果不管怎么更改服務A配置里面的ribbon.MaxAutoRetries和ribbon.MaxAutoRetriesNextServer次數,重試結果都不變,后續再對源碼進行分析。
2、服務B有一個節點故障的情況這里也不多做介紹,實際場景并不多見。
場景三:生產過程中大部分場景都類似此場景,即多服務之間的多跨度調用。

由于此場景比較復雜,而且影響重試效果的配置比較多,這里以網關的MaxAutoRetries和MaxAutoRetriesNextServer配置不變,并且B1節點故障,B2節點正常;分為A服務是否引入spring-retry組件,Zuul網關的ribbon.ReadTimeout等于和大于服務端feign.client.config.default.readTimeout4種情況測試分析。
Zuul重試參數配置如下(hystrix的時間,根據readtimeout的值動態變化,后面不另做說明,公式為(ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1)):
ribbon: ConnectTimeout: 1000 ReadTimeout: 1000 MaxAutoRetries: 0 MaxAutoRetriesNextServer: 1 OkToRetryOnAllOperations: false NFLoadBalancerRuleClassName: AvailabilityFilteringRule hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 4000 zuul: retryable: true
服務A的配置如下:
${spring.application.name}:
ribbon:
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 1
OkToRetryOnAllOperations: false
NFLoadBalancerRuleClassName: AvailabilityFilteringRule
feign:
client:
config:
default:
connectTimeout: 1000
readTimeout: 1000
情況一:服務A未引入spring-retry組件,Zuul網關的ribbon.ReadTimeout等于服務端feign.client.config.default.readTimeout 都為1s。
1、此種情況,小概率會出現A1訪問B1超時,觸發Zuul ribbon重試A2,A2又通過Feign訪問到B1的情況(因為前面介紹過,B1失敗請求到達一定的數量或者百分比之后會觸發熔斷),通過JMeter并發測試5組,大概會有一組中的1個請求會出現超時的情況如下圖,其他時候第一次訪問B1超時,重試的時候A服務不會再負載到B1服務。
2、單次訪問測試和JMeter并發測試,得到的結果一致。


情況二:服務A未引入spring-retry組件,Zuul網關的ribbon.ReadTimeout=3000 大于服務端 feign.client.config.default.readTimeout =1000的值。
1、單次訪問的時候,當A服務feign負載到B1,瀏覽器馬上就返回了500錯誤,總耗時1s,而且通過B1和B2服務的日志觀察并沒有進行重試。
2、通過JMeter并發測試5組,即使有些請求超過1s(表示負載到了B1),但是實際返回的狀態碼是200,說明已經進行了重試,并且5組1萬次請求,未出現非200狀態的請求,如下圖:
3、得出結論高并發的時候重試邏輯并非和低頻訪問得到的結果一致,這個只有等空閑的時候研究源碼才知道其中的處理邏輯。


情況三:服務A引入spring-retry組件,Zuul網關的ribbon.ReadTimeout=3000 大于服務端 feign.client.config.default.readTimeout =1000的值。
1、單次訪問的時候,當A1服務feign負載到B1返回超時,馬上A1就會重試訪問B2,隨后瀏覽器馬上就返回了請求成功,總耗時1s,觀察B1和B2服務的日志,的確進行了重試,觀察A1和A2服務日志,調用B1的時候并沒有返回接口超時的日志,也說明了A1進行的重試調用了B1,所以不管怎么樣,瀏覽器都會返回正常結果。
2、通過JMeter并發測試5組,實際得到結果,也是和單次訪問一樣,100%請求都會返回正常結果,但是單次請求最大時間卻達到了2s,加上1s的請求,基本上占了整個請求數量的50%以上。
3、通過JMeter的聚合結果和之前情況一和情況二對比,性能下降了7倍左右,QPS只有23-28左右如下圖,而前面的實測QPS基本保持在150以上。


情況四:服務A引入spring-retry組件,Zuul網關的ribbon.ReadTimeout等于服務端feign.client.config.default.readTimeout 都為1s。
1、單次訪問的時候,只有A服務第一次就負載到B2節點瀏覽器100%才會返回正常結果,A服務第一次訪問到B1超時,剛好A服務重試訪問B2返回了結果,此時剛好zuul還沒觸發A服務的重試,瀏覽器才會返回正常結果,其他情況瀏覽器都會返回500錯誤,而且B服務大概率都會產生2、3或者4次請求。
2、通過JMeter并發測試5組,實際得到結果,也是和單次訪問一樣,返回成功的請求占很少一部分,基本在5%左右,如下圖:


通過上面四種情況的測試,得出結論:
1、高并發的時候開啟feign的重試,當有一個服務某個節點故障時,會嚴重影響系統性能。
2、服務端開啟feign重試機制后,如果網關ribbon超時時間和服務feign超時時間設置不當,當某個服務某一個節點出現故障時,會嚴重影響請求返回的成功率。
3、建議不要開啟服務端的feign的重試機制,只要重試就會產生多余的請求,影響系統性能,所以建議把ribbon.MaxAutoRetries設置為0。
4、當服務端未開啟feign重試時,建議ribbon.ReadTimeout和feign.client.config.default.readTimeout設置為一樣,這樣即使某個服務某個節點故障,即便是高并發或者低頻訪問,都不會對訪問返回成功率和性能造成太大的影響。
5、而實際生產過程中,一般A服務的接口都不止調用一個B服務,甚至可能同時調用C、D、E服務;B服務可能還會調用F服務等等非常復雜,所以情況二,實際過程中也可以把ribbon.ReadTimeout設置稍微大于feign.client.config.default.readTimeout,模擬某個服務某個節點故障,進行并發性能測試對比。


