【Spring Cloud 基础设施搭建系列】Spring Cloud Demo项目 Zuul的路由重试和路由熔断

mac2022-06-30  32

文章目录

Zuul的路由熔断Zuul的路由重试Zuul超时时间hystrix 超时失效问题 参考源代码

Zuul的路由熔断

当我们的后端服务出现异常的时候,我们不希望将异常抛出给最外层,期望服务可以自动进行降级。Zuul给我们提供了这样的支持。当某个服务出现异常时,直接返回我们预设的信息。

我们通过自定义的fallback方法,并且将其指定给某个route来实现该route访问出问题的熔断处理。主要实现FallbackProvider接口来实现,FallbackProvider默认有两个方法,getRoute方法用来指明熔断拦截哪个服务,fallbackResponse方法用来定制返回内容。 实现类通过实现getRoute方法,告诉Zuul它是负责哪个route定义的熔断。而fallbackResponse方法则是告诉 Zuul 断路出现时,它会提供一个什么返回值来处理请求。

网上有很多不同版本的实现方式,Dalston及更低版本,要想为Zuul提供回退,需要实现ZuulFallbackProvider的getRoute()和fallbackResponse()方法.Edgware及更高版本通过实现FallbackProvider 接口,从而实现回退,FallbackProvider接口比ZuulFallbackProvider多了一个ClientHttpResponse fallbackResponse(Throwable cause); 方法,使用该方法,可获得造成回退的原因。Finchley版本好像改为了ClientHttpResponse fallbackResponse(String route, Throwable cause);

我当前的版本是Greenwich,FallbackProvider接口如下:

public interface FallbackProvider { String getRoute(); ClientHttpResponse fallbackResponse(String route, Throwable cause); }

如果要为所有路由提供默认回退,可以创建FallbackProvider类型的bean并使getRoute方法返回*或null,例如:

package com.cc.cloud.zuul; import com.netflix.hystrix.exception.HystrixTimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; /** * Zuul熔断 */ @Component public class ZuulFallback implements FallbackProvider { private final Logger logger = LoggerFactory.getLogger(ZuulFallback.class); @Override public String getRoute() { // 表明是为哪个微服务提供回退,*表示为所有微服务提供回退 // 还可以返回指定的service,比如cloud-service-order return "*"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { if (cause != null && cause.getCause() != null) { String reason = cause.getCause().getMessage(); logger.info("FallbackResponse Exception {}", reason); } if (cause instanceof HystrixTimeoutException) { return response(HttpStatus.GATEWAY_TIMEOUT); } else { return this.fallbackResponse(); } } private ClientHttpResponse fallbackResponse() { return this.response(HttpStatus.INTERNAL_SERVER_ERROR); } private ClientHttpResponse response(final HttpStatus status) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() { return status; } @Override public int getRawStatusCode() { return status.value(); } @Override public String getStatusText() { return status.getReasonPhrase(); } @Override public void close() { } @Override public InputStream getBody() { return new ByteArrayInputStream("The service is unavailable.".getBytes(StandardCharsets.UTF_8)); //返回前端的内容 } @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); //设置头 return httpHeaders; } }; } }

getRoute方法还可以返回指定的service,只需要返回指定的service名称即可,例如cloud-service-order。如果配置了路由的话,还可以返回路由的名称,具体的没怎么研究。不过有兴趣的可以参考:

Spring Cloud Edgware新特性之八:Zuul回退的改进

跟我学Spring Cloud(Finchley版)-18-Zuul深入

SpringCloud(七):Zuul的Fallback回退机制

服务网关zuul之五:熔断

现在我们重启cloud-zuul服务,然后改造一下cloud-service-order服务的controller,让它等待一段时间。

@GetMapping("/orders") @ResponseStatus(HttpStatus.OK) public List<String> getOrders() { List<String> orders = Lists.newArrayList(); try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } orders.add("order 1"); orders.add("order 2"); return orders; }

然后访问:http://localhost:8769/api/cloud-order/order/orders ,可以看到如下的结果:

并且可以看到cloud-zuul控制台打印如下:

2019-10-01 19:59:40.477 INFO 5770 --- [nio-8769-exec-9] com.cc.cloud.zuul.ZuulFallback : FallbackResponse Exception java.net.SocketTimeoutException: Read timed out

Zuul的路由重试

有时候因为网络或者其它原因,服务可能会暂时的不可用,这个时候我们希望可以再次对服务进行重试,Zuul也帮我们实现了此功能,需要结合Spring Retry 一起来实现。

添加Spring Retry依赖

首先在spring-cloud-zuul项目中添加Spring Retry依赖。

<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> 开启Zuul Retry

再配置文件中配置启用Zuul Retry,配置zuul.retryable设置为true,开启重试功能

zuul: #默认情况下,只要引入了zuul后,就会自动一个默认的路由配置,但有些时候我们可能不想要默认的路由配置规则,想自己进行定义 #忽略所有微服务,只路由指定的微服务 ignored-services: '*' routes: api-member: path: /cloud-member/** service-id: cloud-service-member api-order: path: /cloud-order/** service-id: cloud-service-order #为所有路由都增加一个通过的前缀 #需要访问/api/path... #全局配置去掉前缀,默认为true strip-prefix: true prefix: /api #是否开启重试功能,默认为false retryable: true # 配置没有提示但依然有效 ribbon: # 对当前实例的重试次数 MaxAutoRetries: 2 # 切换实例的重试次数 MaxAutoRetriesNextServer: 0 #是否所有操作都重试 OkToRetryOnAllOperations: false

这样我们就开启了Zuul的重试功能。

然后在配置一下ribbon的设置,IDEA配置没有提示,但是依然有效。

ribbon: # 对当前实例的重试次数 MaxAutoRetries: 2 # 切换实例的重试次数 MaxAutoRetriesNextServer: 0 #是否所有操作都重试 OkToRetryOnAllOperations: false

当OkToRetryOnAllOperations设置为false时,只会对get请求进行重试。如果设置为true,便会对所有的请求进行重试,如果是put或post等写操作,如果服务器接口没做幂等性,会产生不好的结果,所以OkToRetryOnAllOperations慎用。

如果不配置ribbon的重试次数,默认会重试一次

参考:springcloud之Feign、ribbon设置超时时间和重试机制的总结

测试

我们对cloud-service-order进行改造。

@GetMapping("/orders") @ResponseStatus(HttpStatus.OK) public List<String> getOrders() { logger.info("call getOrders..."); List<String> orders = Lists.newArrayList(); try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } orders.add("order 1"); orders.add("order 2"); return orders; }

然后我们重启cloud-service-order和cloud-zuul服务,访问:http://localhost:8769/api/cloud-order/order/orders

然后我们查看我们的cloud-service-order的控制台。

说明进行了三次的请求,也就是进行了两次的重试。这样也就验证了我们的配置信息,完成了Zuul的重试功能。

Zuul超时时间

zuul 中配置超时时间,分两种情况:

用 serviceId 进行路由时,使用 ribbon.ReadTimeout 和 ribbon.SocketTimeout

# 配置没有提示但依然有效 ribbon: # 对当前实例的重试次数 MaxAutoRetries: 2 # 切换实例的重试次数 MaxAutoRetriesNextServer: 0 #是否所有操作都重试 OkToRetryOnAllOperations: false # 请求连接的超时时间 ConnectTimeout: 10000 # 请求处理的超时时间 ReadTimeout: 10000

设置用指定 url 进行路由时,使用 zuul.host.connect-timeout-millis 和 zuul.host.socket-timeout-millis 设置。

zuul: host: #zuul.host.connect-timeout-millis,zuul.host.socket-timeout-millis这两个配置,这两个和上面的ribbon都是配超时的 #区别在于,如果路由方式是serviceId的方式,那么ribbon的生效,如果是url的方式,则zuul.host开头的生效 socket-timeout-millis: 10000 connect-timeout-millis: 10000

网上说如果zuul配置了熔断fallback的话,熔断超时也要配置,需要配置hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds,例如:

hystrix: command: default: execution: timeout: #执行是否启用超时,默认启用true enabled: true isolation: thread: #命令执行超时时间,默认1000ms timeoutInMilliseconds: 2000 ###开启Hystrix断路器 ## 引入Zuul的时候会引入Ribbon和Hystrix的依赖 feign: hystrix: enabled: true

结果控制台上打印如下:

2019-10-02 17:12:31.810 WARN 7212 --- [nio-8769-exec-1] o.s.c.n.z.f.r.s.AbstractRibbonCommand : The Hystrix timeout of 2000ms for the command cloud-service-order is set lower than the combination of the Ribbon read and connect timeout, 60000ms.

大概意思就是 Hystrix 的 超时时间小于 Ribbon的超时时间。为什么Ribbon的超时时间是60000ms呢?但是实际上服务也没有在2000ms之后就走到熔断。这个警告是AbstractRibbonCommand.java报告的,于是我开始查阅它的源码

protected static int getHystrixTimeout(IClientConfig config, String commandKey) { int ribbonTimeout = getRibbonTimeout(config, commandKey); DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance(); // 获取默认的hytrix超时时间 int defaultHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds", 0).get(); // 获取具体服务的hytrix超时时间,这里应该是hystrix.command.serviceA.execution.isolation.thread.timeoutInMilliseconds int commandHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command." + commandKey + ".execution.isolation.thread.timeoutInMilliseconds", 0).get(); int hystrixTimeout; // hystrixTimeout的优先级是 具体服务的hytrix超时时间 > 默认的hytrix超时时间 > ribbon超时时间 if (commandHystrixTimeout > 0) { hystrixTimeout = commandHystrixTimeout; } else if (defaultHystrixTimeout > 0) { hystrixTimeout = defaultHystrixTimeout; } else { hystrixTimeout = ribbonTimeout; } // 如果默认的或者具体服务的hytrix超时时间小于ribbon超时时间就会警告 if (hystrixTimeout < ribbonTimeout) { LOGGER.warn("The Hystrix timeout of " + hystrixTimeout + "ms for the command " + commandKey + " is set lower than the combination of the Ribbon read and connect timeout, " + ribbonTimeout + "ms."); } return hystrixTimeout; }

仔细查看发现ribbonTimeout是通过getRibbonTimeout()方法获取的

protected static int getRibbonTimeout(IClientConfig config, String commandKey) { int ribbonTimeout; // 默认为 2s if (config == null) { ribbonTimeout = 2000; } else { // 这里获取了四个参数,ReadTimeout,ConnectTimeout,MaxAutoRetries, MaxAutoRetriesNextServer,优先级:具体服务 > 默认 // 1. 请求处理的超时时间,默认 1s int ribbonReadTimeout = getTimeout(config, commandKey, "ReadTimeout", Keys.ReadTimeout, 1000); // 2. 请求连接的超时时间,默认 1s int ribbonConnectTimeout = getTimeout(config, commandKey, "ConnectTimeout", Keys.ConnectTimeout, 1000); // 3. 对当前实例的重试次数.默认 0 int maxAutoRetries = getTimeout(config, commandKey, "MaxAutoRetries", Keys.MaxAutoRetries, 0); // 4. 切换实例的重试次数,默认 1 int maxAutoRetriesNextServer = getTimeout(config, commandKey, "MaxAutoRetriesNextServer", Keys.MaxAutoRetriesNextServer, 1); // ribbonTimeout的计算方法 ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1); } return ribbonTimeout; }

原来 ribbonTimeout的计算方法为:

ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);

然后我们项目中的配置如下:

ribbon: # 对当前实例的重试次数 MaxAutoRetries: 2 # 切换实例的重试次数 MaxAutoRetriesNextServer: 0 #是否所有操作都重试 OkToRetryOnAllOperations: false # 请求连接的超时时间 ConnectTimeout: 10000 # 请求处理的超时时间 ReadTimeout: 10000 ribbonTimeout=(10000 + 10000) * (2 + 1) * (0 + 1) = 60000

网上说如果hystrixTimeout小于ribbonTimeout,可能在Ribbon切换实例进行重试的过程中就会触发熔断。但是实际我测试发现,貌似设置了hystrixTimeout小于ribbonTimeout还是不会提前走熔断。这一点我还是觉得很奇怪,可能是哪里配置有问题?希望有大神能帮我告诉我问题在哪?或者等我找到原因再来更新。

参考:

Zuul超时问题,微服务响应超时,zuul进行熔断

Zuul、Ribbon、Feign、Hystrix使用时的超时时间(timeout)设置问题

简单谈谈什么是Hystrix,以及SpringCloud的各种超时时间配置效果,和简单谈谈微服务优化


hystrix 超时失效问题

更新于2019年10月3日

前面说到我即使配置了hystrixTimeout,设置了timeoutInMilliseconds,但是hystrix的超时却不起作用。然后经过我的查阅和尝试,发现原来是因为zuul 默认的隔离级别是SEMAPHORE(可能以前的版本是THREAD?)可设置zuul.ribbonIsolationStrategy=THREAD将隔离策略改为THREAD。如果设置成SEMAPHORE,那么hytrix的超时将会失效。

如果用的是信号量隔离级别,那么hytrix的超时将会失效 当使用线程池隔离时,因为多了一层线程池,而且是用的RXJava实现,故可以直接支持hytrix的超时调用

如果使用的是信号量隔离,那么hytrix的超时将会失效,但是ribbon或者socket本身的超时机制依然是有效果的,而且超时后会释放掉信号

如果是信号量隔离,依然得注意hytrix设置的超时时间,因为它涉及到信号量的释放

当使用thread进行隔离的时候,Hystrix命令会通过从线程池分离一个单独的线程来执行。 Hystrix会暂停这个持有请求的线程,直到下游服务器收到响应,或者发生超时。

使用SEMAPHORE隔离时,会在请求线程上执行Hystrix命令.仅在从下游服务器收到响应后才检测超时.因此,如果您将Zuul / Hystrix配置为超时5秒,并且您的服务需要30秒才能完成.只有在30秒后,您的客户才会收到超时通知 - 即使服务响应成功。

除少数情况外,Netflix建议默认执行THREAD,SpringCloud Zuul默认集成SEMAPHORE模式。

简单总结下,hytrix的超时设置其实是起作用的,当然我这里说的是当hystrix超时时间比ribbon超时时间小的情况下,如果设置了隔离级别为THREAD的时候,当达到timeoutInMilliseconds设置的时间,会立马熔断告诉你服务不可用。如果是设置了SEMAPHORE,其实也是起作用的,只是会等到最终服务返回的时候才去熔断。比如如果你服务需要2秒钟才会响应,hystrix设置了1秒就熔断,ribbon设置成3秒。那么等到2秒服务返回了,这个时候依然会熔断告诉你服务不可用,即使服务响应成功了。

所以如果hystrix.command.default.execution.timeout.enabled为true,则会有两个执行方法超时的配置,一个就是ribbon的ReadTimeout,一个就是熔断器hystrix的timeoutInMilliseconds, 此时谁的值小谁生效。所以无论隔离级别设置为哪一种,hystrix的timeout设置一定要大于ribbon的设置。

参考:简单谈谈什么是Hystrix,以及SpringCloud的各种超时时间配置效果,和简单谈谈微服务优化

现在我们只需要加入zuul.ribbon-isolation-strategy: thread的配置即可。

zuul: #默认情况下,只要引入了zuul后,就会自动一个默认的路由配置,但有些时候我们可能不想要默认的路由配置规则,想自己进行定义 #忽略所有微服务,只路由指定的微服务 ignored-services: '*' routes: api-member: path: /cloud-member/** service-id: cloud-service-member api-order: path: /cloud-order/** service-id: cloud-service-order #为所有路由都增加一个通过的前缀 #需要访问/api/path... #全局配置去掉前缀,默认为true strip-prefix: true prefix: /api #是否开启重试功能,默认为false retryable: true host: #zuul.host.connect-timeout-millis,zuul.host.socket-timeout-millis这两个配置,这两个和上面的ribbon都是配超时的 #区别在于,如果路由方式是serviceId的方式,那么ribbon的生效,如果是url的方式,则zuul.host开头的生效 socket-timeout-millis: 10000 connect-timeout-millis: 10000 # 默认为SEMAPHORE,SEMAPHORE设置下hystrix超时不起效 ribbon-isolation-strategy: thread # 配置没有提示但依然有效 ribbon: # 对当前实例的重试次数 MaxAutoRetries: 2 # 切换实例的重试次数 MaxAutoRetriesNextServer: 0 #是否所有操作都重试 OkToRetryOnAllOperations: false # 请求连接的超时时间 ConnectTimeout: 10000 # 请求处理的超时时间 ReadTimeout: 10000 hystrix: command: default: execution: timeout: #执行是否启用超时,默认启用true enabled: true isolation: thread: #命令执行超时时间,默认1000ms timeoutInMilliseconds: 2000 ###开启Hystrix断路器 ## 引入Zuul的时候会引入Ribbon和Hystrix的依赖 # 似乎这个配置有没有都一样 feign: hystrix: enabled: true

这个feign的hystrix设置好像是不起作用的,但是网上有些配置上也加上了。

这个时候timeoutInMilliseconds: 2000就起作用了,在2000ms的时候就直接熔断了。

但是具体SEMAPHORE和THREAD的区别和作用我还没做更多的研究,总之zuul的复杂度比较大,大程度因为集成了hytrix, ribbon,导致设置超时,线程,隔离都有一定的复杂度,本身文档确没那么清楚。

很多地方还是需要debug分析源码才能避免踩坑。

参考:

十一、微服务网关之Zuul的Hystrix隔离策略和线程池

用网关zuul时,熔断hytrix里面的坑

使用zuul网关,hystrix不生效的问题,方法调用超时

参考

关于zuul,ribbon和hystrix的配置和说明

关于Hystrix与ribbon的超时时间配置问题

springcloud2.x 设置feign、ribbon和hystrix的超时问题(配置文件)

springcloud之Feign、ribbon设置超时时间和重试机制的总结

聊聊ribbon的超时时间设置

Spring Cloud Zuul 中 ribbon 和 hystrix 配置说明(Finchley版本)

【SpringCloud】Zuul在何种情况下使用Hystrix Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式。

zuul中开启了熔断机制,设置时间很长,后端返回成功后zuul却进入了fallback,请问这是怎么回事?

spring cloud zuul网关服务重试请求配置和源码分析

ribbon设置url级别的超时时间

Spring Cloud Zuul重试机制探秘

Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式。

Zuul超时问题,微服务响应超时,zuul进行熔断

服务网关zuul之五:熔断

Spring Cloud Edgware新特性之八:Zuul回退的改进

跟我学Spring Cloud(Finchley版)-18-Zuul深入

SpringCloud(七):Zuul的Fallback回退机制

Zuul、Ribbon、Feign、Hystrix使用时的超时时间(timeout)设置问题

Spring Cloud重试机制与各组件的重试总结 spring cloud连载第三篇补充之Zuul

Spring cloud Zuul 参数调优

简单谈谈什么是Hystrix,以及SpringCloud的各种超时时间配置效果,和简单谈谈微服务优化

Spring cloud 超时及重试配置【ribbon及其它http client】

Spring Cloud各组件重试总结

Spring Cloud各组件超时总结

Zuul、Ribbon、Feign、Hystrix使用时的超时时间(timeout)设置问题

zuul中hystrix默认timeout配置失效的原因

十一、微服务网关之Zuul的Hystrix隔离策略和线程池

用网关zuul时,熔断hytrix里面的坑

使用zuul网关,hystrix不生效的问题,方法调用超时

Zuul 超时、重试、并发参数设置

Hystrix在网关Zuul使用中遇到问题

源代码

https://gitee.com/cckevincyh/spring-cloud-demo/tree/zuul-hystrix-retry

c. 认证博客专家 MySQL Java Elasticsearch github:https://github.com/cckevincyhgitee:https://gitee.com/cckevincyh
最新回复(0)