服务网关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。

 

posted on 2018-09-27 20:43  duanxz  阅读(8524)  评论(0编辑  收藏  举报