服务网关zuul之五:熔断
路由熔断
当我们的后端服务出现异常的时候,我们不希望将异常抛出给最外层,期望服务可以自动进行一降级。Zuul给我们提供了这样的支持。当某个服务出现异常时,直接返回我们预设的信息。
如果没有配置fallback,zuul调用时超时了,
我们通过自定义的fallback方法,并且将其指定给某个route来实现该route访问出问题的熔断处理。主要继承ZuulFallbackProvider接口来实现,ZuulFallbackProvider默认有两个方法,一个用来指明熔断拦截哪个服务,一个定制返回内容。
public interface FallbackProvider extends ZuulFallbackProvider { /** * Provides a fallback response based on the cause of the failed execution. * * @param cause cause of the main method failure * @return the fallback response */ ClientHttpResponse fallbackResponse(Throwable cause); }
实现类通过实现getRoute方法,告诉Zuul它是负责哪个route定义的熔断。而fallbackResponse方法则是告诉 Zuul 断路出现时,它会提供一个什么返回值来处理请求。
后来Spring又扩展了此类,丰富了返回方式,在返回的内容中添加了异常信息,因此最新版本建议直接继承类FallbackProvider
。
我们以上面的spring-cloud-producer服务为例,定制它的熔断返回内容。
package com.dxz.zuul.fallback; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; 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; @Component public class ProducerFallback implements FallbackProvider { private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class); // 指定要处理的 service。 @Override public String getRoute() { return "service-producter"; } public ClientHttpResponse fallbackResponse() { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return "OK"; } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("The service is unavailable.".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } @Override public ClientHttpResponse fallbackResponse(Throwable cause) { if (cause != null && cause.getCause() != null) { String reason = cause.getCause().getMessage(); logger.info("Excption {}", reason); } return fallbackResponse(); } }
路由转发配置:
zuul.routes.api-test.path: /api-test/** zuul.routes.consuer.path: /service-consumer/** zuul.routes.consuer.serviceId: service-consumer zuul.routes.producter.path: /service-producter/** zuul.routes.producter.serviceId: service-producter
为了便于演示,将zuul里的超时配置短些:
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 6000 hystrix.command.default.execution.timeout.enabled: true feign.hystrix.enabled: true spring.cloud.loadbalancer.retry.enabled: true ribbon.ReadTimeout: 6000 ribbon.ConnectTimeout: 6000
访问: http://127.0.0.1:8091/service-producter/book/getbook5/2?token=1
当服务出现异常时,打印相关异常信息,并返回”The service is unavailable.”。
Zuul 目前只支持服务级别的熔断,不支持具体到某个URL进行熔断。
路由重试
有时候因为网络或者其它原因,服务可能会暂时的不可用,这个时候我们希望可以再次对服务进行重试,Zuul也帮我们实现了此功能,需要结合Spring Retry 一起来实现。下面我们以上面的项目为例做演示。
添加Spring Retry依赖
首先在spring-cloud-zuul项目中添加Spring Retry依赖。
compile 'org.springframework.retry:spring-retry:1.2.2.RELEASE'
开启Zuul Retry
再配置文件中配置启用Zuul Retry
#是否开启重试功能 zuul.retryable=true #对当前服务的重试次数 ribbon.MaxAutoRetries=2 #切换相同Server的次数 ribbon.MaxAutoRetriesNextServer=0
这样我们就开启了Zuul的重试功能。
测试
我们对service-producer进行改造,在getbook5方法中添加定时,并且在请求的一开始打印参数。
@RestController @RequestMapping("/book") public class BookProducter { @Autowired private RestTemplate restTemplate; @RequestMapping(value = "/getbook5/{id}", method = RequestMethod.GET) public Book getbook5(@ApiParam("id编号") @PathVariable("id") Integer id) throws InterruptedException { System.out.println(">>>>>>>>/getbook5/" + id); int i = new Random().nextInt(20); TimeUnit.SECONDS.sleep(i); System.out.println("SLEEP=" + i + ">>>>>>>>/getbook5/" + id); if (id == 1) { return new Book(id, "《李自成》", 55, "姚雪垠", "人民文学出版社"); } else if (id == 2) { return new Book(id, "中国文学简史", 33, "林庚", "清华大学出版社"); } return new Book(id, "文学改良刍议", 33, "胡适", "无"); } }
重启 service-producter和zuul-demo项目。
访问地址:http://127.0.0.1:8091/service-producter/book/getbook5/3?token=1,当页面返回:The service is unavailable.
时查看项目service-producter后台日志如下:
说明进行了三次的请求,也就是进行了两次的重试。这样也就验证了我们的配置信息,完成了Zuul的重试功能。
注意
开启重试在某些情况下是有问题的,比如当压力过大,一个实例停止响应时,路由将流量转到另一个实例,很有可能导致最终所有的实例全被压垮。说到底,断路器的其中一个作用就是防止故障或者压力扩散。用了retry,断路器就只有在该服务的所有实例都无法运作的情况下才能起作用。这种时候,断路器的形式更像是提供一种友好的错误信息,或者假装服务正常运行的假象给使用者。
不用retry,仅使用负载均衡和熔断,就必须考虑到是否能够接受单个服务实例关闭和eureka刷新服务列表之间带来的短时间的熔断。如果可以接受,就无需使用retry。