上一篇文章Eureka(六)——服务消费的例子中,实现了对服务名为eureka-client
的/dc
接口的调用。由于RestTemplate
被@LoadBalanced
修饰,所以它具备客户端负载均衡的能力,当请求真正发起的时候,url中的服务名会根据负载均衡策略从服务清单中挑选出一个实例来进行访问。
1 |
|
大多数情况下,上面的例子没有任何问题,但是总有一些意外发生,比如:有一个实例发生了故障而该情况还没有被服务治理机制及时发现和清除,这时候客户端访问该节点的时候自然会失败。所以,为了构件更为健壮的应用系统,我们希望当请求失败的时候能够有一定策略的重试机制,而不是直接返回失败。
重试机制实现
在application.yml
中加入以下配置:
1 | spring: |
pom.xml中引入spring-retry包:
1 | <dependency> |
很多文章中都值说了上面的两步,实测发现无法实现超时重试机制,因为上面的配置不会影响到RestTemplate
的超时时间,因此会一直等待服务返回,而不会重新尝试连接另外的服务实例。
如果要对RestTemplate
设置超时时间,我们需要使用如下方式设置:
1 |
|
根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries
配置),如果不行,就换一个实例进行访问,如果还是不行,再换一次实例访问(更好次数由MaxAutoRetriesNextServer
配置),如果依然不行,返回失败信息。
重试机制原理
支持重试机制的拦截器在LoadBalancerAutoConfiguration
中定义:
1 |
|
因为我们引入了spring-retry包,存在RetryTemplate
类,因此会初始化RetryInterceptorAutoConfiguration
类中的RetryLoadBalancerInterceptor
和RestTemplateCustomizer
实例。
重试机制的拦截功能在RetryLoadBalancerInterceptor.intercept
方法中实现。它创建RetryTemplate
,然后调用其execute
方法。
RetryTemplate
RetryTemplate
是实现重试机制的模板类。execute
方法调用doExecute
方法,doExecute
方法中实现重试机制。主要代码如下:
1 | protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback, |
doExecute
方法主体是一个循环,循环的判断是调用canRetry
方法,canRetry
方法实际调用RetryPolicy
的canRetry
方法。
canRetry
此处RetryPolicy
的实现类是InterceptorRetryPolicy
,它的canRetry
方法如下:
1 | public boolean canRetry(RetryContext context) { |
- 如果
if
判断为真,说明当前还没有作任何请求。于是选择一个服务实例,并返回true
。 - 否则,说明上一次的请求发生错误,调用
LoadBalancedRetryPolicy
的canRetryNextServer
来判断是否需要尝试下一个服务实例
LoadBalancedRetryPolicy
的实例是RibbonLoadBalancedRetryPolicy
,它的canRetryNextServer
方法如下所示:
1 | public boolean canRetryNextServer(LoadBalancedRetryContext context) { |
canRetryNextServer
方法判断nextServerCount
是否小于我们配置的MaxAutoRetriesNextServer
,并且canRetry
方法是否返回true
。
如果请求方法是GET
,或者我们配置OkToRetryOnAllOperations
为true
,canRetry
方法返回true
。
doWithRetry
回到RetryTemplate.doExecute
方法,进入while循环体之后,调用retryCallback.doWithRetry
方法。该方法在RetryLoadBalancerInterceptor.intercept
方法中定义,代码如下:
1 | ServiceInstance serviceInstance = null; |
可以看到,doWithRetry
方法调用RibbonLoadBalancerClient
的execute
方法向服务发生请求。如果请求发生异常,execute
方法会抛出这个异常。
处理请求异常
当RetryTemplate.doWithRetry
方法抛出异常,RetryTemplate.doExecute
方法中会捕获这个异常,然后进行一系列处理,包括调用registerThrowable
方法注册异常、判断是否重新抛出异常等等。
其中registerThrowable
方法最终会调用RibbonLoadBalancedRetryPolicy
方法:
1 | public void registerThrowable(LoadBalancedRetryContext context, Throwable throwable) { |
首先判断是否有一个引发异常的回路
然后调用canRetrySameServer
方法和canRetryNextServer
判断是否要选择下一个服务实例。如果是的话调用RibbonLoadBalancerClient.choose
方法选择下一个服务实例。
canRetrySameServer
方法的代码如下:
1 | public boolean canRetrySameServer(LoadBalancedRetryContext context) { |
它判断sameServerCount
是否小于我们配置的MaxAutoRetries
,并且canRetry
方法是否返回true
。
canRetryNextServer
方法的代码如下:
1 | public boolean canRetryNextServer(LoadBalancedRetryContext context) { |
它判断nextServerCount
是否小于我们配置的MaxAutoRetriesNextServer
,并且canRetry
方法是否返回true
。
最后判断sameServerCount
是否大于等于我们配置的MaxAutoRetries
,并且canRetry
方法是否返回true
:
- 如果是的话,说明要切换服务实例,于是将
sameServerCount
设为0,nextServerCount加1 - 否则,说明还是请求相同的服务实例,于是将
sameServerCount
加1
执行完异常处理后,RetryTemplate.doExecute
方法重新执行while循环:在相同的服务实例上再次发送请求,或者切换到下一个服务实例发送请求。
总结
Ribbon的重试机制核心类是RetryTemplate
,它捕获请求的异常,通过一个循环来重新请求相同的服务实例或者切换到下一个服务实例发送请求,以达到重试的效果。
http://blog.didispace.com/spring-cloud-ribbon-failed-retry/
http://www.itmuch.com/spring-cloud-sum/spring-cloud-timeout/