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

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回退机制

扫描二维码关注公众号,回复: 9614035 查看本文章

服务网关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 一起来实现。

  1. 添加Spring Retry依赖

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

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
  1. 开启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设置超时时间和重试机制的总结

  1. 测试

我们对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.ReadTimeoutribbon.SocketTimeout

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

设置用指定 url 进行路由时,使用 zuul.host.connect-timeout-milliszuul.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

发布了647 篇原创文章 · 获赞 816 · 访问量 98万+

猜你喜欢

转载自blog.csdn.net/cckevincyh/article/details/101927358