山西推广公司,seo收费,怎么选择优秀的网站建设公司,网页设计html5简介
本文章对应spring cloud的版本为(Dalston.SR4)#xff0c;具体内容如下#xff1a;
开启Zuul功能通过源码了解Zuul的一次转发怎么开启zuul的重试机制Edgware.RC1版本的优化
开启Zuul的功能
首先如何使用spring cloud zuul完成路由转发的功能#xff0c;这个问题很简…简介
本文章对应spring cloud的版本为(Dalston.SR4)具体内容如下
开启Zuul功能通过源码了解Zuul的一次转发怎么开启zuul的重试机制Edgware.RC1版本的优化
开启Zuul的功能
首先如何使用spring cloud zuul完成路由转发的功能这个问题很简单只需要进行如下准备工作即可
注册中心(Eureka Server)zuul(同时也是Eureka Client)应用服务(同时也是Eureka Client)
我们希望zuul和后端的应用服务同时都注册到Eureka Server上当我们访问Zuul的某一个地址时对应其实访问的是后端应用的某个地址从而从这个地址返回一段内容并展现到浏览器上。
注册中心(Eureka Server)
创建一个Eureka Server只需要在主函数上添加EnableEurekaServer并在properties文件进行简单配置即可具体内容如下
EnableEurekaServerRestControllerSpringBootApplicationpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }}server.port8761eureka.client.register-with-eurekafalseeureka.client.fetch-registryfalseZuul
主函数添加EnableZuulProxy注解(因为集成Eureka需要另外添加EnableDiscoveryClient注解)。并配置properties文件具体内容如下所示
EnableZuulProxyEnableDiscoveryClientSpringBootApplicationpublic class ZuulDemoApplication { /** * 省略代码... */}server.port8081spring.application.nameZUUL-CLIENTzuul.routes.api-a.serviceIdEUREKA-CLIENTzuul.routes.api-a.path/api-a/**eureka.client.service-url.defaultZonehttp://localhost:8761/eureka应用服务
RestControllerEnableEurekaClientSpringBootApplicationpublic class EurekaClientApplication { public static void main(String[] args) { SpringApplication.run(EurekaClientApplication.class, args); } RequestMapping(value /hello) public String index() { return hello spring...; }}spring.application.nameEUREKA-CLIENTeureka.client.service-url.defaultZonehttp://localhost:8761/eureka三个工程全部启动这时当我们访问localhost:8081/api-a/hello时你会看到浏览器输出的内容是hello spring...
通过源码了解Zuul的一次转发
接下来我们通过源码层面来了解下一次转发内部都做了哪些事情。
首先我们查看Zuul的配置类ZuulProxyAutoConfiguration在这个类中有一项工作是初始化Zuul默认自带的Filter其中有一个Filter很重要它就是RibbonRoutingFilter。它主要是完成请求的路由转发。接下来我们看下他的run方法
Overridepublic 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); }}可以看到进行转发的方法是forward我们进一步查看这个方法具体内容如下省略部分代码
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这个类是在RibbonCommandFactoryConfiguration完成初始化的(触发RibbonCommandFactoryConfiguration的加载动作是利用ZuulProxyAutoConfiguration类上面的Import标签)具体代码如下
ConfigurationConditionalOnRibbonHttpClientprotected static class HttpClientRibbonConfiguration { Autowired(required false) private SetZuulFallbackProvider zuulFallbackProviders Collections.emptySet(); Bean ConditionalOnMissingBean public RibbonCommandFactory? ribbonCommandFactory( SpringClientFactory clientFactory, ZuulProperties zuulProperties) { return new HttpClientRibbonCommandFactory(clientFactory, zuulProperties, zuulFallbackProviders); }}知道了这个ribbonCommandFactory具体的实现类(HttpClientRibbonCommandFactory)接下来我们看看它的create方法具体做了那些事情
Overridepublic 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));}这个方法按照我的理解主要做了以下几件事情
Overridepublic HttpClientRibbonCommand create(final RibbonCommandContext context) { /** *获取所有ZuulFallbackProvider,即当Zuul *调用失败后的降级方法 */ ZuulFallbackProvider xxxxx /** *创建处理请求转发类该类会利用 *Apache的Http client进行请求的转发 */ RibbonLoadBalancingHttpClient xxxxx /** *将降级方法、处理请求转发类、以及其他一些内容 *包装成HttpClientRibbonCommand(这个类继承了HystrixCommand) */ return new HttpClientRibbonCommand(xxxxx);}到这里我们很清楚的知道了RibbonRoutingFilter类的forward方法中RibbonCommand command this.ribbonCommandFactory.create(context);这一行代码都做了哪些内容.
接下来调用的是command.execute();方法通过刚刚的分析我们知道了command其实指的是HttpClientRibbonCommand同时我们也知道HttpClientRibbonCommand继承了HystrixCommand所以当执行command.execute();时其实执行的是HttpClientRibbonCommand的run方法。查看源码我们并没有发现run方法但是我们发现HttpClientRibbonCommand直接继承了AbstractRibbonCommand。所以其实执行的是AbstractRibbonCommand的run方法接下来我们看看run方法里面都做了哪些事情
Overrideprotected 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方法中会调用client的executeWithLoadBalancer方法通过上面介绍我们知道client指的是RibbonLoadBalancingHttpClient而RibbonLoadBalancingHttpClient里面并没有executeWithLoadBalancer方法。(这里面会最终调用它的父类AbstractLoadBalancerAwareClient的executeWithLoadBalancer方法。)
具体代码如下
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException { /** * 创建一个RetryHandler这个很重要它是用来 * 决定利用RxJava的Observable是否进行重试的标准。 */ RequestSpecificRetryHandler handler getRequestSpecificRetryHandler(request, requestConfig); /** * 创建一个LoadBalancerCommand这个类用来创建Observable * 以及根据RetryHandler来判断是否进行重试操作。 */ LoadBalancerCommandT command LoadBalancerCommand.Tbuilder() .withLoadBalancerContext(this) .withRetryHandler(handler) .withLoadBalancerURI(request.getUri()) .build(); try { /** *command.submit()方法主要是创建了一个Observable(RxJava) *并且为这个Observable设置了重试次数这个Observable最终 *会回调AbstractLoadBalancerAwareClient.this.execute() *方法。 */ return command.submit( new ServerOperationT() { Override public ObservableT 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); } } }下面针对于每一块内容做详细说明 首先getRequestSpecificRetryHandler(request, requestConfig);这个方法其实调用的是RibbonLoadBalancingHttpClient的getRequestSpecificRetryHandler方法这个方法主要是返回一个RequestSpecificRetryHandler
Overridepublic RequestSpecificRetryHandler getRequestSpecificRetryHandler(RibbonApacheHttpRequest request, IClientConfig requestConfig) { /** *这个很关键请注意该类构造器中的前两个参数的值 *正因为一开始我也忽略了这两个值所以后续给我造 *成一定的干扰。 */ return new RequestSpecificRetryHandler(false, false, RetryHandler.DEFAULT, requestConfig);}接下来创建LoadBalancerCommand并将上一步获得的RequestSpecificRetryHandler作为参数内容。最后调用LoadBalancerCommand的submit方法。该方法内容太长具体代码细节就不在这里贴出了按照我个人的理解只贴出相应的伪代码
public ObservableT submit(final ServerOperationT operation) { //相同server的重试次数(去除首次请求) final int maxRetrysSame retryHandler.getMaxRetriesOnSameServer(); //集群内其他Server的重试个数 final int maxRetrysNext retryHandler.getMaxRetriesOnNextServer(); /** *创建一个Observable(RxJava),selectServer()方法是 *利用Ribbon选择一个Server并将其包装成Observable */ ObservableT o selectServer().concatMap(new Func1Server, ObservableT() { Override public ObservableT call(final Server server) { /** *这里会回调submit方法入参ServerOperation类的call方法 */ return operation.call(server).doOnEach(new ObserverT() {} } } if (maxRetrysSame 0) o o.retry(retryPolicy(maxRetrysSame, true)); if (maxRetrysNext 0 server null) o o.retry(retryPolicy(maxRetrysNext, false)); return o.onErrorResumeNext(new Func1Throwable, ObservableT() { Override public ObservableT call(Throwable e) { /** *转发请求失败时会进入此方法。通过此方法进行判断 *是否超过重试次数maxRetrysSame、maxRetrysNext。 */ } });}operation.call()方法最终会调用RibbonLoadBalancingHttpClient的execute方法该方法内容如下
Overridepublic RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, final IClientConfig configOverride) throws Exception { /** * 组装参数(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); /** * 发送转发请求 */ final HttpResponse httpResponse this.delegate.execute(httpUriRequest); /** * 返回结果 */ return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI());}可以看到上面方法主要做的就是组装请求参数(包括各种超时时间)然后发起转发请求最终获取相应结果。
说到这里zuul转发一次请求的基本原理就说完了。让我们再回顾下整个流程。
zuul的转发是通过RibbonRoutingFilter这个Filter进行操作的。在转发之前zuul利用Hystrix将此次转发请求包装成一个HystrixCommand正应为这样才使得zuul具有了降级(Fallback)的功能同时HystrixCommand是具备超时时间的(默认是1s)。而且Zuul默认采用的隔离级别是信号量模式。在HystrixCommand内部zuul再次将请求包装成一个Observable(有关RxJava的知识请参照其官方文档)。并且为Observable设置了重试次数。
事实真的是这样吗当我看到源码中为Observable设置重试次数的时候我以为这就是zuul的重试逻辑。遗憾的是我的想法是错误的。还记得上面我说的getRequestSpecificRetryHandler(request, requestConfig);这个方法吗?(不记得的同学可以回过头来再看下)这个方法返回的是RequestSpecificRetryHandler这个类而且在创建该类时构造器的前两个参数都为false。(这一点非常重要)。这两个参数分别是okToRetryOnConnectErrors和okToRetryOnAllErrors。
我原本的想法是这个请求被包装成Observable如果这次请求因为超时出现异常或者其他异常这样就会触发Observable的重试机制(RxJava)但是事实并非如此为什么呢原因就是上面的那两个参数当出现了超时异常的时候在触发重试机制之前会调用RequestSpecificRetryHandler的isRetriableException()方法该方法的作用是用来判断是否执行重试动作具体代码如下
Overridepublic boolean isRetriableException(Throwable e, boolean sameServer) { //此时该值为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 { //此时该值为false return okToRetryOnConnectErrors isConnectionException(e); }}说道这里zuul转发一次请求的基本原理大概了解了同时也验证了一个事实就是实现zuul进行重试的逻辑并不是Observable的重试机制。那么问题来了是什么使zuul具有重试功能的呢
怎么开启zuul的重试机制
开启Zuul重试的功能在原有的配置基础上需要额外进行以下设置
在pom中添加spring-retry的依赖(maven工程)设置zuul.retryabletrue(该参数默认为false)
具体properties文件内容如下
server.port8081spring.application.nameZUUL-CLIENT#路由信息zuul.routes.api-a.serviceIdEUREKA-CLIENTzuul.routes.api-a.path/api-a/**#是否开启重试功能zuul.retryabletrue#同一个Server重试的次数(除去首次)ribbon.MaxAutoRetries3#切换相同Server的次数ribbon.MaxAutoRetriesNextServer0eureka.client.service-url.defaultZonehttp://localhost:8761/eureka为了模拟出Zuul重试的功能需要对后端应用服务进行改造改造后的内容如下
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)达到Zuul转发超时情况(Zuul默认连接超时未2s、read超时时间为5s)从而触发Zuul的重试功能。这时候在此访问localhost:8081/api-a/hello时查看应用服务后台会发现最终打印三次request is coming...
通过现象看本质接下来简单介绍下Zuul重试的原理。首先如果你工程classpath中存在spring-retry那么zuul在初始化的时候就不会创建RibbonLoadBalancingHttpClient而是创建RetryableRibbonLoadBalancingHttpClient具体源代码如下
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; }}所以请求到来需要转发的时候(AbstractLoadBalancerAwareClient类中executeWithLoadBalancer方法会调用AbstractLoadBalancerAwareClient.this.execute())其实调用的是RetryableRibbonLoadBalancingHttpClient的execute方法(而不是没有重试时候RibbonLoadBalancingHttpClient的execute方法)源码内容如下
Overridepublic 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方法内容如下
private RibbonApacheHttpResponse executeWithRetry(RibbonApacheHttpRequest request, LoadBalancedRetryPolicy retryPolicy, RetryCallbackRibbonApacheHttpResponse, 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);}按照我的理解主要逻辑如下
Overridepublic RibbonApacheHttpResponse execute(final RibbonApacheHttpRequest request, final IClientConfig configOverride) throws Exception { /** *创建RequestConfig(请求信息) */ final RequestConfig requestConfig builder.build(); final LoadBalancedRetryPolicy retryPolicy loadBalancedRetryPolicyFactory.create(this.getClientName(), this); /** * 创建RetryCallbck的实现类用来完成重试逻辑 */ RetryCallback retryCallback new RetryCallback() {}; //创建Spring-retry的模板类RetryTemplate。 RetryTemplate retryTemplate new RetryTemplate(); /** *设置重试规则即在什么情况下进行重试 *什么情况下停止重试。源码中这部分存在 *一个判断判断的根据就是在zuul工程 *的propertris中配置的zuul.retryable *该参数内容为true才可以具有重试功能。 */ retryTemplate.setRetryPolicy(xxx); /** *发起请求 */ return retryTemplate.execute(callback);}到此为止我们不仅知道了zuul路由一次请求的整体过程也明确了zuul因后端超时而触发重试的原理。可是似乎还存在着一个问题就是超时问题。前面说过zuul把路由请求这个过程包装成一个HystrixCommnd而在我的propertries文件中并没有设置Hystrix的超时时间(默认时间为1s)而read的超时时间是5s(前面源码部分介绍过)。这里就会有人问因为最外层是采用Hystrix而Hystrix此时已经超时了为什么还允许它内部继续使用spring-retry进行重试呢带着这个问题我查看了官方GitHub上的issues发现有人对此问题提出过疑问。作者给出的回复是Hystrix超时的时候并不会打断内部重试的操作。
其实说实话这块内容我并不是很理解(可能是因为Hystrix源码了解较少)带着这个问题我给作者发了一封邮件邮件对话内容如下
我的(英语水平不好大家见谅) 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 作者的回复(重点) 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 李刚 spring_holy163.com wrote: 虽然得到了作者的确认但是这部分内容始终还是没有完全理解后续还要看看Hystrix的源码。
Edgware.RC1版本的优化
在Edgware.RC1版本中作者修改了代码并不使用Ribbon的默认值而是将ConnectTimeout以及ReadTimeout都赋值为1S),同时调整Hystrix的超时时间时间为(2S).具体信息内容如下https://github.com/spring-cloud/spring-cloud-netflix/pull/2261
同时作者也阐明了利用Hystrix包装使用Ribbon时关于超时时间的设置规则(以下内容来自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 potentialretries that might be made. For example, if your Ribbon connection timeout is one second andthe Ribbon client might retry the request three times, than your Hystrix timeout shouldbe slightly more than three seconds. 以上全部内容就是本人对Zuul重试机制的理解由于水平有限可能有些问题没有阐述清楚还请大家多多留言讨论。
最后感谢Spring4all社区提供这个平台能让大家交流学习Spring相关知识。