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回退机制
现在我们重启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、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分析源码才能避免踩坑。
参考:
参考
springcloud2.x 设置feign、ribbon和hystrix的超时问题(配置文件)
springcloud之Feign、ribbon设置超时时间和重试机制的总结
Spring Cloud Zuul 中 ribbon 和 hystrix 配置说明(Finchley版本)
【SpringCloud】Zuul在何种情况下使用Hystrix
Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式。
zuul中开启了熔断机制,设置时间很长,后端返回成功后zuul却进入了fallback,请问这是怎么回事?
spring cloud zuul网关服务重试请求配置和源码分析
Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式。
Spring Cloud Edgware新特性之八:Zuul回退的改进
跟我学Spring Cloud(Finchley版)-18-Zuul深入
SpringCloud(七):Zuul的Fallback回退机制
Zuul、Ribbon、Feign、Hystrix使用时的超时时间(timeout)设置问题
Spring Cloud重试机制与各组件的重试总结
spring cloud连载第三篇补充之Zuul
简单谈谈什么是Hystrix,以及SpringCloud的各种超时时间配置效果,和简单谈谈微服务优化
Spring cloud 超时及重试配置【ribbon及其它http client】
Zuul、Ribbon、Feign、Hystrix使用时的超时时间(timeout)设置问题
源代码
https://gitee.com/cckevincyh/spring-cloud-demo/tree/zuul-hystrix-retry