簡介
本文章對應(yīng)spring cloud的版本為(Dalston.SR4),具體內(nèi)容如下:
- 開啟Zuul功能
- 通過源碼了解Zuul的一次轉(zhuǎn)發(fā)
- 怎么開啟zuul的重試機(jī)制
- Edgware.RC1版本的優(yōu)化
開啟Zuul的功能
首先如何使用spring cloud zuul完成路由轉(zhuǎn)發(fā)的功能,這個(gè)問題很簡單,只需要進(jìn)行如下準(zhǔn)備工作即可:
- 注冊中心(Eureka Server)
- zuul(同時(shí)也是Eureka Client)
- 應(yīng)用服務(wù)(同時(shí)也是Eureka Client)
我們希望zuul和后端的應(yīng)用服務(wù)同時(shí)都注冊到Eureka Server上,當(dāng)我們訪問Zuul的某一個(gè)地址時(shí),對應(yīng)其實(shí)訪問的是后端應(yīng)用的某個(gè)地址,從而從這個(gè)地址返回一段內(nèi)容,并展現(xiàn)到瀏覽器上。
注冊中心(Eureka Server)
創(chuàng)建一個(gè)Eureka Server只需要在主函數(shù)上添加@EnableEurekaServer,并在properties文件進(jìn)行簡單配置即可,具體內(nèi)容如下:
1
2
3
4
5
6
7
8
|
@EnableEurekaServer @RestController @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication. class , args); } } |
1
2
3
|
server.port=8761 eureka.client.register-with-eureka=false eureka.client.fetch-registry=false |
Zuul
主函數(shù)添加@EnableZuulProxy注解(因?yàn)榧蒃ureka,需要另外添加@EnableDiscoveryClient注解)。并配置properties文件,具體內(nèi)容如下所示:
1
2
3
4
5
6
7
8
|
@EnableZuulProxy @EnableDiscoveryClient @SpringBootApplication public class ZuulDemoApplication { /** * 省略代碼... */ } |
1
2
3
4
5
|
server.port=8081 spring.application.name=ZUUL-CLIENT zuul.routes.api-a.serviceId=EUREKA-CLIENT zuul.routes.api-a.path=/api-a/** eureka.client.service-url.defaultZone=http://localhost:8761/eureka |
應(yīng)用服務(wù)
1
2
3
4
5
6
7
8
9
10
11
12
|
@RestController @EnableEurekaClient @SpringBootApplication public class EurekaClientApplication { public static void main(String[] args) { SpringApplication.run(EurekaClientApplication. class , args); } @RequestMapping (value = "/hello" ) public String index() { return "hello spring..." ; } } |
1
2
|
spring.application.name=EUREKA-CLIENT eureka.client.service-url.defaultZone=http://localhost:8761/eureka |
三個(gè)工程全部啟動(dòng),這時(shí)當(dāng)我們訪問localhost:8081/api-a/hello時(shí),你會(huì)看到瀏覽器輸出的內(nèi)容是hello spring...
通過源碼了解Zuul的一次轉(zhuǎn)發(fā)
接下來我們通過源碼層面來了解下,一次轉(zhuǎn)發(fā)內(nèi)部都做了哪些事情。
首先我們查看Zuul的配置類ZuulProxyAutoConfiguration在這個(gè)類中有一項(xiàng)工作是初始化Zuul默認(rèn)自帶的Filter,其中有一個(gè)Filter很重要,它就是RibbonRoutingFilter。它主要是完成請求的路由轉(zhuǎn)發(fā)。接下來我們看下他的run方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); try { RibbonCommandContext commandContext = buildCommandContext(context); ClientHttpResponse response = forward(commandContext); setResponse(response); return response; } catch (ZuulException ex) { throw new ZuulRuntimeException(ex); } catch (Exception ex) { throw new ZuulRuntimeException(ex); } } |
可以看到進(jìn)行轉(zhuǎn)發(fā)的方法是forward,我們進(jìn)一步查看這個(gè)方法,具體內(nèi)容如下:
省略部分代碼
1
2
3
4
5
6
7
8
9
10
|
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception { RibbonCommand command = this .ribbonCommandFactory.create(context); try { ClientHttpResponse response = command.execute(); return response; } catch (HystrixRuntimeException ex) { return handleException(info, ex); } } |
ribbonCommandFactory指的是HttpClientRibbonCommandFactory這個(gè)類是在RibbonCommandFactoryConfiguration完成初始化的(觸發(fā)RibbonCommandFactoryConfiguration的加載動(dòng)作是利用ZuulProxyAutoConfiguration類上面的@Import標(biāo)簽),具體代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
@Configuration @ConditionalOnRibbonHttpClient protected static class HttpClientRibbonConfiguration { @Autowired (required = false ) private Set<ZuulFallbackProvider> zuulFallbackProviders = Collections.emptySet(); @Bean @ConditionalOnMissingBean public RibbonCommandFactory<?> ribbonCommandFactory( SpringClientFactory clientFactory, ZuulProperties zuulProperties) { return new HttpClientRibbonCommandFactory(clientFactory, zuulProperties, zuulFallbackProviders); } } |
知道了這個(gè)ribbonCommandFactory具體的實(shí)現(xiàn)類(HttpClientRibbonCommandFactory),接下來我們看看它的create方法具體做了那些事情
1
2
3
4
5
6
7
8
9
10
|
@Override public HttpClientRibbonCommand create( final RibbonCommandContext context) { ZuulFallbackProvider zuulFallbackProvider = getFallbackProvider(context.getServiceId()); final String serviceId = context.getServiceId(); final RibbonLoadBalancingHttpClient client = this .clientFactory.getClient( serviceId, RibbonLoadBalancingHttpClient. class ); client.setLoadBalancer( this .clientFactory.getLoadBalancer(serviceId)); return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties, zuulFallbackProvider, clientFactory.getClientConfig(serviceId)); } |
這個(gè)方法按照我的理解主要做了以下幾件事情:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Override public HttpClientRibbonCommand create( final RibbonCommandContext context) { /** *獲取所有ZuulFallbackProvider,即當(dāng)Zuul *調(diào)用失敗后的降級方法 */ ZuulFallbackProvider = xxxxx /** *創(chuàng)建處理請求轉(zhuǎn)發(fā)類,該類會(huì)利用 *Apache的Http client進(jìn)行請求的轉(zhuǎn)發(fā) */ RibbonLoadBalancingHttpClient = xxxxx /** *將降級方法、處理請求轉(zhuǎn)發(fā)類、以及其他一些內(nèi)容 *包裝成HttpClientRibbonCommand(這個(gè)類繼承了HystrixCommand) */ return new HttpClientRibbonCommand(xxxxx); } |
到這里我們很清楚的知道了RibbonRoutingFilter類的forward方法中RibbonCommand command = this.ribbonCommandFactory.create(context);這一行代碼都做了哪些內(nèi)容.
接下來調(diào)用的是command.execute();方法,通過剛剛的分析我們知道了command其實(shí)指的是HttpClientRibbonCommand,同時(shí)我們也知道HttpClientRibbonCommand繼承了HystrixCommand所以當(dāng)執(zhí)行command.execute();時(shí)其實(shí)執(zhí)行的是HttpClientRibbonCommand的run方法。查看源碼我們并沒有發(fā)現(xiàn)run方法,但是我們發(fā)現(xiàn)HttpClientRibbonCommand直接繼承了AbstractRibbonCommand。所以其實(shí)執(zhí)行的是AbstractRibbonCommand的run方法,接下來我們看看run方法里面都做了哪些事情:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override protected ClientHttpResponse run() throws Exception { final RequestContext context = RequestContext.getCurrentContext(); RQ request = createRequest(); RS response = this .client.executeWithLoadBalancer(request, config); context.set( "ribbonResponse" , response); if ( this .isResponseTimedOut()) { if (response != null ) { response.close(); } } return new RibbonHttpResponse(response); } |
可以看到在run方法中會(huì)調(diào)用client的executeWithLoadBalancer方法,通過上面介紹我們知道client指的是RibbonLoadBalancingHttpClient,而RibbonLoadBalancingHttpClient里面并沒有executeWithLoadBalancer方法。(這里面會(huì)最終調(diào)用它的父類AbstractLoadBalancerAwareClient的executeWithLoadBalancer方法。)
具體代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public T executeWithLoadBalancer( final S request, final IClientConfig requestConfig) throws ClientException { /** * 創(chuàng)建一個(gè)RetryHandler,這個(gè)很重要它是用來 * 決定利用RxJava的Observable是否進(jìn)行重試的標(biāo)準(zhǔn)。 */ RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig); /** * 創(chuàng)建一個(gè)LoadBalancerCommand,這個(gè)類用來創(chuàng)建Observable * 以及根據(jù)RetryHandler來判斷是否進(jìn)行重試操作。 */ LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder() .withLoadBalancerContext( this ) .withRetryHandler(handler) .withLoadBalancerURI(request.getUri()) .build(); try { /** *command.submit()方法主要是創(chuàng)建了一個(gè)Observable(RxJava) *并且為這個(gè)Observable設(shè)置了重試次數(shù),這個(gè)Observable最終 *會(huì)回調(diào)AbstractLoadBalancerAwareClient.this.execute() *方法。 */ return command.submit( new ServerOperation<T>() { @Override public Observable<T> call(Server server) { URI finalUri = reconstructURIWithServer(server, request.getUri()); S requestForServer = (S) request.replaceUri(finalUri); try { return Observable.just(AbstractLoadBalancerAwareClient. this .execute(requestForServer, requestConfig)); } catch (Exception e) { return Observable.error(e); } } }) .toBlocking() .single(); } catch (Exception e) { Throwable t = e.getCause(); if (t instanceof ClientException) { throw (ClientException) t; } else { throw new ClientException(e); } } } |
下面針對于每一塊內(nèi)容做詳細(xì)說明:
首先getRequestSpecificRetryHandler(request, requestConfig);這個(gè)方法其實(shí)調(diào)用的是RibbonLoadBalancingHttpClient的getRequestSpecificRetryHandler方法,這個(gè)方法主要是返回一個(gè)RequestSpecificRetryHandler
1
2
3
4
5
6
7
8
9
10
|
@Override public RequestSpecificRetryHandler getRequestSpecificRetryHandler(RibbonApacheHttpRequest request, IClientConfig requestConfig) { /** *這個(gè)很關(guān)鍵,請注意該類構(gòu)造器中的前兩個(gè)參數(shù)的值 *正因?yàn)橐婚_始我也忽略了這兩個(gè)值,所以后續(xù)給我造 *成一定的干擾。 */ return new RequestSpecificRetryHandler( false , false , RetryHandler.DEFAULT, requestConfig); } |
接下來創(chuàng)建LoadBalancerCommand并將上一步獲得的RequestSpecificRetryHandler作為參數(shù)內(nèi)容。
最后調(diào)用LoadBalancerCommand的submit方法。該方法內(nèi)容太長具體代碼細(xì)節(jié)就不在這里貼出了,按照我個(gè)人的理解,只貼出相應(yīng)的偽代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public Observable<T> submit( final ServerOperation<T> operation) { //相同server的重試次數(shù)(去除首次請求) final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer(); //集群內(nèi)其他Server的重試個(gè)數(shù) final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer(); /** *創(chuàng)建一個(gè)Observable(RxJava),selectServer()方法是 *利用Ribbon選擇一個(gè)Server,并將其包裝成Observable */ Observable<T> o = selectServer().concatMap( new Func1<Server, Observable<T>>() { @Override public Observable<T> call( final Server server) { /** *這里會(huì)回調(diào)submit方法入?yún)erverOperation類的call方法, */ return operation.call(server).doOnEach( new Observer<T>() {} } } if (maxRetrysSame > 0 ) o = o.retry(retryPolicy(maxRetrysSame, true )); if (maxRetrysNext > 0 && server == null ) o = o.retry(retryPolicy(maxRetrysNext, false )); return o.onErrorResumeNext( new Func1<Throwable, Observable<T>>() { @Override public Observable<T> call(Throwable e) { /** *轉(zhuǎn)發(fā)請求失敗時(shí),會(huì)進(jìn)入此方法。通過此方法進(jìn)行判斷 *是否超過重試次數(shù)maxRetrysSame、maxRetrysNext。 */ } }); } |
operation.call()方法最終會(huì)調(diào)用RibbonLoadBalancingHttpClient的execute方法,該方法內(nèi)容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@Override public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, final IClientConfig configOverride) throws Exception { /** * 組裝參數(shù)(RequestConfig) */ final RequestConfig.Builder builder = RequestConfig.custom(); IClientConfig config = configOverride != null ? configOverride : this .config; builder.setConnectTimeout(config.get( CommonClientConfigKey.ConnectTimeout, this .connectTimeout)); builder.setSocketTimeout(config.get( CommonClientConfigKey.ReadTimeout, this .readTimeout)); builder.setRedirectsEnabled(config.get( CommonClientConfigKey.FollowRedirects, this .followRedirects)); final RequestConfig requestConfig = builder.build(); if (isSecure(configOverride)) { final URI secureUri = UriComponentsBuilder.fromUri(request.getUri()) .scheme( "https" ).build().toUri(); request = request.withNewUri(secureUri); } final HttpUriRequest httpUriRequest = request.toRequest(requestConfig); /** * 發(fā)送轉(zhuǎn)發(fā)請求 */ final HttpResponse httpResponse = this .delegate.execute(httpUriRequest); /** * 返回結(jié)果 */ return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI()); } |
可以看到上面方法主要做的就是組裝請求參數(shù)(包括各種超時(shí)時(shí)間),然后發(fā)起轉(zhuǎn)發(fā)請求,最終獲取相應(yīng)結(jié)果。
說到這里,zuul轉(zhuǎn)發(fā)一次請求的基本原理就說完了。讓我們再回顧下整個(gè)流程。
- zuul的轉(zhuǎn)發(fā)是通過RibbonRoutingFilter這個(gè)Filter進(jìn)行操作的。
- 在轉(zhuǎn)發(fā)之前,zuul利用Hystrix將此次轉(zhuǎn)發(fā)請求包裝成一個(gè)HystrixCommand,正應(yīng)為這樣才使得zuul具有了降級(Fallback)的功能,同時(shí)HystrixCommand是具備超時(shí)時(shí)間的(默認(rèn)是1s)。而且Zuul默認(rèn)采用的隔離級別是信號量模式。
- 在HystrixCommand內(nèi)部zuul再次將請求包裝成一個(gè)Observable,(有關(guān)RxJava的知識請參照其官方文檔)。并且為Observable設(shè)置了重試次數(shù)。
事實(shí)真的是這樣嗎?當(dāng)我看到源碼中為Observable設(shè)置重試次數(shù)的時(shí)候,我以為這就是zuul的重試邏輯。遺憾的是我的想法是錯(cuò)誤的。還記得上面我說的getRequestSpecificRetryHandler(request, requestConfig);這個(gè)方法嗎?(不記得的同學(xué)可以回過頭來再看下),這個(gè)方法返回的是RequestSpecificRetryHandler這個(gè)類,而且在創(chuàng)建該類時(shí),構(gòu)造器的前兩個(gè)參數(shù)都為false。(這一點(diǎn)非常重要)。這兩個(gè)參數(shù)分別是okToRetryOnConnectErrors和okToRetryOnAllErrors。
我原本的想法是這個(gè)請求被包裝成Observable,如果這次請求因?yàn)槌瑫r(shí)出現(xiàn)異常或者其他異常,這樣就會(huì)觸發(fā)Observable的重試機(jī)制(RxJava),但是事實(shí)并非如此,為什么呢?原因就是上面的那兩個(gè)參數(shù),當(dāng)出現(xiàn)了超時(shí)異常的時(shí)候,在觸發(fā)重試機(jī)制之前會(huì)調(diào)用RequestSpecificRetryHandler的isRetriableException()方法,該方法的作用是用來判斷是否執(zhí)行重試動(dòng)作,具體代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Override public boolean isRetriableException(Throwable e, boolean sameServer) { //此時(shí)該值為false if (okToRetryOnAllErrors) { return true ; } else if (e instanceof ClientException) { ClientException ce = (ClientException) e; if (ce.getErrorType() == ClientException.ErrorType.SERVER_THROTTLED) { return !sameServer; } else { return false ; } } else { //此時(shí)該值為false return okToRetryOnConnectErrors && isConnectionException(e); } } |
說道這里zuul轉(zhuǎn)發(fā)一次請求的基本原理大概了解了,同時(shí)也驗(yàn)證了一個(gè)事實(shí)就是實(shí)現(xiàn)zuul進(jìn)行重試的邏輯并不是Observable的重試機(jī)制。那么問題來了?是什么使zuul具有重試功能的呢?
怎么開啟zuul的重試機(jī)制
開啟Zuul重試的功能在原有的配置基礎(chǔ)上需要額外進(jìn)行以下設(shè)置:
- 在pom中添加spring-retry的依賴(maven工程)
- 設(shè)置zuul.retryable=true(該參數(shù)默認(rèn)為false)
具體properties文件內(nèi)容如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
server.port=8081 spring.application.name=ZUUL-CLIENT #路由信息 zuul.routes.api-a.serviceId=EUREKA-CLIENT zuul.routes.api-a.path=/api-a/** #是否開啟重試功能 zuul.retryable=true #同一個(gè)Server重試的次數(shù)(除去首次) ribbon.MaxAutoRetries=3 #切換相同Server的次數(shù) ribbon.MaxAutoRetriesNextServer=0 eureka.client.service-url.defaultZone=http://localhost:8761/eureka |
為了模擬出Zuul重試的功能,需要對后端應(yīng)用服務(wù)進(jìn)行改造,改造后的內(nèi)容如下:
1
2
3
4
5
6
7
8
9
10
|
@RequestMapping (value = "/hello" ) public String index() { System.out.println( "request is coming..." ); try { Thread.sleep( 100000 ); } catch (InterruptedException e) { System.out.println( "線程被打斷... " + e.getMessage()); } return "hello spring ..." ; } |
通過使用Thread.sleep(100000)達(dá)到Zuul轉(zhuǎn)發(fā)超時(shí)情況(Zuul默認(rèn)連接超時(shí)未2s、read超時(shí)時(shí)間為5s),從而觸發(fā)Zuul的重試功能。這時(shí)候在此訪問localhost:8081/api-a/hello時(shí),查看應(yīng)用服務(wù)后臺,會(huì)發(fā)現(xiàn)最終打印三次"request is coming..."
通過現(xiàn)象看本質(zhì),接下來簡單介紹下Zuul重試的原理。首先如果你工程classpath中存在spring-retry,那么zuul在初始化的時(shí)候就不會(huì)創(chuàng)建RibbonLoadBalancingHttpClient而是創(chuàng)建RetryableRibbonLoadBalancingHttpClient具體源代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@ConditionalOnClass (name = "org.apache.http.client.HttpClient" ) @ConditionalOnProperty (name = "ribbon.httpclient.enabled" , matchIfMissing = true ) public class HttpClientRibbonConfiguration { @Value ( "${ribbon.client.name}" ) private String name = "client" ; @Bean @ConditionalOnMissingBean (AbstractLoadBalancerAwareClient. class ) @ConditionalOnMissingClass (value = "org.springframework.retry.support.RetryTemplate" ) public RibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( IClientConfig config, ServerIntrospector serverIntrospector, ILoadBalancer loadBalancer, RetryHandler retryHandler) { RibbonLoadBalancingHttpClient client = new RibbonLoadBalancingHttpClient( config, serverIntrospector); client.setLoadBalancer(loadBalancer); client.setRetryHandler(retryHandler); Monitors.registerObject( "Client_" + this .name, client); return client; } @Bean @ConditionalOnMissingBean (AbstractLoadBalancerAwareClient. class ) @ConditionalOnClass (name = "org.springframework.retry.support.RetryTemplate" ) public RetryableRibbonLoadBalancingHttpClient retryableRibbonLoadBalancingHttpClient( IClientConfig config, ServerIntrospector serverIntrospector, ILoadBalancer loadBalancer, RetryHandler retryHandler, LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory) { RetryableRibbonLoadBalancingHttpClient client = new RetryableRibbonLoadBalancingHttpClient( config, serverIntrospector, loadBalancedRetryPolicyFactory); client.setLoadBalancer(loadBalancer); client.setRetryHandler(retryHandler); Monitors.registerObject( "Client_" + this .name, client); return client; } } |
所以請求到來需要轉(zhuǎn)發(fā)的時(shí)候(AbstractLoadBalancerAwareClient類中executeWithLoadBalancer方法會(huì)調(diào)用AbstractLoadBalancerAwareClient.this.execute())其實(shí)調(diào)用的是RetryableRibbonLoadBalancingHttpClient的execute方法(而不是沒有重試時(shí)候RibbonLoadBalancingHttpClient的execute方法),源碼內(nèi)容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
@Override public RibbonApacheHttpResponse execute( final RibbonApacheHttpRequest request, final IClientConfig configOverride) throws Exception { final RequestConfig.Builder builder = RequestConfig.custom(); IClientConfig config = configOverride != null ? configOverride : this .config; builder.setConnectTimeout(config.get( CommonClientConfigKey.ConnectTimeout, this .connectTimeout)); builder.setSocketTimeout(config.get( CommonClientConfigKey.ReadTimeout, this .readTimeout)); builder.setRedirectsEnabled(config.get( CommonClientConfigKey.FollowRedirects, this .followRedirects)); final RequestConfig requestConfig = builder.build(); final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create( this .getClientName(), this ); RetryCallback retryCallback = new RetryCallback() { @Override public RibbonApacheHttpResponse doWithRetry(RetryContext context) throws Exception { //on retries the policy will choose the server and set it in the context //extract the server and update the request being made RibbonApacheHttpRequest newRequest = request; if (context instanceof LoadBalancedRetryContext) { ServiceInstance service = ((LoadBalancedRetryContext)context).getServiceInstance(); if (service != null ) { //Reconstruct the request URI using the host and port set in the retry context newRequest = newRequest.withNewUri( new URI(service.getUri().getScheme(), newRequest.getURI().getUserInfo(), service.getHost(), service.getPort(), newRequest.getURI().getPath(), newRequest.getURI().getQuery(), newRequest.getURI().getFragment())); } } if (isSecure(configOverride)) { final URI secureUri = UriComponentsBuilder.fromUri(newRequest.getUri()) .scheme( "https" ).build().toUri(); newRequest = newRequest.withNewUri(secureUri); } HttpUriRequest httpUriRequest = newRequest.toRequest(requestConfig); final HttpResponse httpResponse = RetryableRibbonLoadBalancingHttpClient. this .delegate.execute(httpUriRequest); if (retryPolicy.retryableStatusCode(httpResponse.getStatusLine().getStatusCode())) { if (CloseableHttpResponse. class .isInstance(httpResponse)) { ((CloseableHttpResponse)httpResponse).close(); } throw new RetryableStatusCodeException(RetryableRibbonLoadBalancingHttpClient. this .clientName, httpResponse.getStatusLine().getStatusCode()); } return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI()); } }; return this .executeWithRetry(request, retryPolicy, retryCallback); } |
executeWithRetry方法內(nèi)容如下:
1
2
3
4
5
6
7
8
|
private RibbonApacheHttpResponse executeWithRetry(RibbonApacheHttpRequest request, LoadBalancedRetryPolicy retryPolicy, RetryCallback<RibbonApacheHttpResponse, IOException> callback) throws Exception { RetryTemplate retryTemplate = new RetryTemplate(); boolean retryable = request.getContext() == null ? true : BooleanUtils.toBooleanDefaultIfNull(request.getContext().getRetryable(), true ); retryTemplate.setRetryPolicy(retryPolicy == null || !retryable ? new NeverRetryPolicy() : new RetryPolicy(request, retryPolicy, this , this .getClientName())); return retryTemplate.execute(callback); } |
按照我的理解,主要邏輯如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@Override public RibbonApacheHttpResponse execute( final RibbonApacheHttpRequest request, final IClientConfig configOverride) throws Exception { /** *創(chuàng)建RequestConfig(請求信息) */ final RequestConfig requestConfig = builder.build(); final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create( this .getClientName(), this ); /** * 創(chuàng)建RetryCallbck的實(shí)現(xiàn)類,用來完成重試邏輯 */ RetryCallback retryCallback = new RetryCallback() {}; //創(chuàng)建Spring-retry的模板類,RetryTemplate。 RetryTemplate retryTemplate = new RetryTemplate(); /** *設(shè)置重試規(guī)則,即在什么情況下進(jìn)行重試 *什么情況下停止重試。源碼中這部分存在 *一個(gè)判斷,判斷的根據(jù)就是在zuul工程 *的propertris中配置的zuul.retryable *該參數(shù)內(nèi)容為true才可以具有重試功能。 */ retryTemplate.setRetryPolicy(xxx); /** *發(fā)起請求 */ return retryTemplate.execute(callback); } |
到此為止我們不僅知道了zuul路由一次請求的整體過程,也明確了zuul因后端超時(shí)而觸發(fā)重試的原理。可是似乎還存在著一個(gè)問題,就是超時(shí)問題。前面說過zuul把路由請求這個(gè)過程包裝成一個(gè)HystrixCommnd,而在我的propertries文件中并沒有設(shè)置Hystrix的超時(shí)時(shí)間(默認(rèn)時(shí)間為1s),而read的超時(shí)時(shí)間是5s(前面源碼部分介紹過)。這里就會(huì)有人問,因?yàn)樽钔鈱邮遣捎肏ystrix,而Hystrix此時(shí)已經(jīng)超時(shí)了,為什么還允許它內(nèi)部繼續(xù)使用spring-retry進(jìn)行重試呢?帶著這個(gè)問題我查看了官方GitHub上的issues,發(fā)現(xiàn)有人對此問題提出過疑問。作者給出的回復(fù)是Hystrix超時(shí)的時(shí)候并不會(huì)打斷內(nèi)部重試的操作。
其實(shí)說實(shí)話這塊內(nèi)容我并不是很理解(可能是因?yàn)镠ystrix源碼了解較少),帶著這個(gè)問題我給作者發(fā)了一封郵件,郵件對話內(nèi)容如下:
我的(英語水平不好,大家見諒):
I want to confirm two issues with you, First of all zuul retry only spring-retry exists and zuul.retry this parameter is true to take effect? The second problem is that if my classpath spring-retry at the same time I let zuul.retry this parameter is true, which means that at this time zuul have a retry mechanism, then why when Hystrix time-out can not interrupt the spring- retry it. thank you very much
作者的回復(fù)(重點(diǎn)):
Zuul will retry failed requests IF Spring Retry is on the classpath and the property zuul.retryable is set to true. The retry is happening within the hystrix command, so if hystrix times out than a response is returned. Right now there is no mechanism to stop further retries from happening if hystrix times out before all the retries are exhauted.
On Thu, Nov 16, 2017 at 8:40 AM 李剛 [email protected] wrote:
雖然得到了作者的確認(rèn),但是這部分內(nèi)容始終還是沒有完全理解,后續(xù)還要看看Hystrix的源碼。
Edgware.RC1版本的優(yōu)化
在Edgware.RC1版本中,作者修改了代碼并不使用Ribbon的默認(rèn)值而是將ConnectTimeout以及ReadTimeout都賦值為1S),,同時(shí)調(diào)整Hystrix的超時(shí)時(shí)間,時(shí)間為(2S).具體信息內(nèi)容如下:
https://github.com/spring-cloud/spring-cloud-netflix/pull/2261
同時(shí)作者也闡明了利用Hystrix包裝使用Ribbon時(shí)關(guān)于超時(shí)時(shí)間的設(shè)置規(guī)則(以下內(nèi)容來自GitHub):
When using Hystrix commands that wrap Ribbon clients you want to make sure your Hystrix timeout is configured to be longer than the configured Ribbon timeout, including any potential
retries that might be made. For example, if your Ribbon connection timeout is one second and
the Ribbon client might retry the request three times, than your Hystrix timeout should
be slightly more than three seconds.
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持服務(wù)器之家
原文鏈接:http://blog.didispace.com/spring-cloud-zuul-retry-detail/