SpringCloud 13 —— 网关之Gateway (下) 路由配置

Gateway 禁用过滤器

如果想禁用GateWay,可以通过两种方式。

  1. 如果是代码bean配置将配置的Bean注释掉即可。
//    @Bean
//    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
//        return builder.routes()
//                .route(r -> r.path("/gateway/**")
//                        .filters(f -> f.stripPrefix(1)
//                                .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
//                        .uri("lb://ribbon-server")
//                        .order(0)
//                        .id("strippath_route")
//                )
//                .build();
//    }
  1. 如果是配置文件配置,
spring:
  cloud:
    gateway:
      enabled: false

Gateway 路由前缀

前面我们在前面讲Zuul的之后知道Zuul的路由前缀配置方式很简单,只需要
设置zuul.prefix即可。

而在Spring Cloud Getaway 为我们提供了很多原生的过滤器,我们可以通过StripPrefix、RewritePath 在默认过滤器中 进行前缀的设置,可以参照过滤器中 StripPrefix、过滤器 RewritePath、过滤器 Default 进行实现。

Gateway 路由跳转

前面我们在前面讲解Zuul的时候,路由跳转是通过在url中指定forward:进行跳转。不清楚的可以回头看看。

Spring Cloud Getaway 的路由跳转也可以使用forward:,使用方式一样的。比如下面我们要讲的Gateway FallBack 就是使用forward:处理异常问题

Gateway 限流

在前面网关扩展之zuul限流中我们介绍了两种限流手段,可以很方便实现。如果忘了的话可以回去看一下。

在 Spring Cloud Gateway 上实现限流是个不错的选择,只需要编写一个过滤器就可以了。有了前边过滤器的基础,写起来很轻松。Spring Cloud Gateway 已经内置了一个RequestRateLimiterGatewayFilterFactory,我们可以直接使用。

目前RequestRateLimiterGatewayFilterFactory的实现依赖于 Redis,Redis的实现基于Stripe的工作。它需要使用spring-boot-starter-data-redis-reactive Spring Boot启动器。

使用的算法是令牌桶算法。

Getaway实现限流所使用的lua脚本在gateway包下的script目录中,如下:

local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)

local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)

--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)

local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)

local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)

local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)

redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)

return { allowed_num, new_tokens }

新建项目模块cloud-gateways-gateway,引入依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

添加配置

server:
  port: 8711
spring:
  application:
    name: gateways-gateway
  profiles:
    active: rate_limiter_route
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/
---
spring:
  cloud:
    gateway:
      routes:
        - id: rate_limiter_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-09T11:30:11.965+08:00[Asia/Shanghai]
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 1
                key-resolver: "#{@userKeyResolver}"
  redis:
    host: ${redis.host}
    password: ${redis.pwd}
    port: 6379
  profiles: rate_limiter_route

限流配置描述

  • filter 名称必须是 RequestRateLimiter
  • redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求
  • redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数
  • key-resolver:使用 SpEL 按名称引用 bean

新建启动类

@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

限流Bean 配置

下面的配置给出了三种,分别是基于参数、基于ip、基于请求路径。用的时候只能用一个。

@Configuration
public class GatewayConfig {
    //基于参数
    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("cloud"));
    }
    // 基于ip
//    @Bean
//    public KeyResolver ipKeyResolver() {
//        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
//    }
      //基于请求路径
//    @Bean
//    KeyResolver apiKeyResolver() {
//        return exchange -> Mono.just(exchange.getRequest().getPath().value());
//    }
    
}

启动测试,多次频繁点击发送。可以看到返回429 Too Many Request。

Gateway 重试

前面我们在将Feign、Ribbon的时候已经提到过重试机制,就是在我们发送Http 请求的时候希望一次失败后可以再尝试发送一次,因为前几次可能因为网络等不确定原因。Spring Cloud Gateway对请求重试提供的一个GatewayFilter Factory

添加并active配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: retry_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-09T11:30:11.965+08:00[Asia/Shanghai]
          filters:
            - name: Retry
              args:
                retries: 3
                series:
                  - SERVER_ERROR
                statuses:
                  - INTERNAL_SERVER_ERROR
                  - BAD_GATEWAY
                methods:
                  - GET
                exceptions:
                  - java.io.IOException
                  - java.util.concurrent.TimeoutException
  profiles: retry_route

Spring Cloud Gateway 通过以下参数来控制重试机制: 。

  • retries:重试次数,默认值是 3 次

  • statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus

  • methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod

  • series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5 个值。

  • exceptions:应重试的引发异常的列表。

  • backoff:为重试配置的指数补偿。重试在的退避间隔后执行firstBackoff * (factorn),其中n为迭代。如果maxBackoff已配置,则应用的最大退避限制为maxBackoff。如果basedOnPreviousValue为true,则使用计算退避prevBackoff * factor。

源码分析

public static class RetryConfig implements HasRouteId {
    private String routeId;
    private int retries = 3;
    private List<Series> series;
    private List<HttpStatus> statuses;
    private List<HttpMethod> methods;
    private List<Class<? extends Throwable>> exceptions;
    private RetryGatewayFilterFactory.BackoffConfig backoff;

    public RetryConfig() {
        this.series = RetryGatewayFilterFactory.toList(Series.SERVER_ERROR);
        this.statuses = new ArrayList();
        this.methods = RetryGatewayFilterFactory.toList(HttpMethod.GET);
        this.exceptions = RetryGatewayFilterFactory.toList(IOException.class, TimeoutException.class);
    }
    ......

通过分析RetryConfig,我们可以看到重试支持的参数和默认值。

  • retries:重试次数,默认值是3次
  • series:状态码配置(分段),符合的某段状态码才会进行重试逻辑,默认值是SERVER_ERROR,值是5,也就是5XX(5开头的状态码),共有5个值:
public enum Series {
    INFORMATIONAL(1),
    SUCCESSFUL(2),
    REDIRECTION(3),
    CLIENT_ERROR(4),
    SERVER_ERROR(5);
}
  • statuses:状态码配置,和series不同的是这边是具体状态码的配置,取值请参考:org.springframework.http.HttpStatus
  • methods:指定哪些方法的请求需要进行重试逻辑,默认值是GET方法,取值如下:
public enum HttpMethod {
	GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}

exceptions:指定哪些异常需要进行重试逻辑,默认值是IOException、TimeoutException

修改ribbon-server

为了测试重试,我们重新加上随机休眠

int millis = new Random().nextInt(3000);
System.out.println("client线程休眠时间:"+millis);
Thread.sleep(millis);
if(millis>1000){
    throw new RuntimeException("error");
}

启动应用,访问127.0.0.1:8711/user/1,如果延迟时间超过1秒钟,就会抛异常,默认重试3次,3次之后仍然没有成功就会返回异常信息。

Gateway FallBack

前面我们前面章节讲到Zuul的FallBack,并且我们在讲Hystrix也说到在一个分布式系统架构中FallBack的作用。服务B出现的问题不应该影响服务A,在服务B可用的时候继续提供服务。这样保证了服务之间是隔离的,不会产生雪崩式的不可用。

Spring Cloud Gateway 提供了Hystrix GatewayFilter Factory,Hystrix GatewayFilter允许你向网关路由引入断路器,保护你的服务不受级联故障的影响,并允许你在下游故障时提供fallback响应。

添加hystrix 依赖

要在项目中启用Hystrix网关过滤器,需要添加对 spring-cloud-starter-netflix-hystrix的依赖 Spring Cloud Netflix.

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

应用配置

添加并启用配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: hystrix_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-09T11:30:11.965+08:00[Asia/Shanghai]
          filters:
            - name: Hystrix
              args:
                name: fallback
                fallbackUri: forward:/fallback/ribbon-server
  profiles: hystrix_route
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000

Hystrix GatewayFilter Factory 需要一个name参数,即HystrixCommand的名称,设置过滤器名称为Hystrix,并且设置fallback的地址为重定向到我们自己定义的接口。ribbon-server会作为参数。

FallBack 接收类

控制层接收服务名。这样如果一个服务不可用,会将服务不可用的信息返回。这里只是掩饰,可以用自己的业务去丰富这个功能。

@RestController
public class FallbackController {
    @RequestMapping("fallback/{name}")
    public Mono<ApiResult> systemFallback(@PathVariable String name) {
        String response = String.format("访问%s超时或者服务不可用", name);
        return Mono.just(ApiResult.failedMsg(response));
    }
}

启动应用,这次我们将ribbon-server 关掉。访问127.0.0.1:8711/user/1可以看到返回服务不可用信息。

Gateway 异常处理

跟Zuul一样,Spring Cloud Gateway的全局异常处理也不能直接用@ControllerAdvice来处理,通过自定义异常处理器来实现业务需求。
网关是给接口做代理转发的,后端对应的都是REST API,返回数据格式一般都是JSON。如果不做处理,当发生异常时,Gateway默认给出的错误信息是页面,不方便前端进行异常处理。
需要对异常信息进行处理,返回JSON格式的数据给客户端。

**添加并启用配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: exception_route
          uri: lb://ribbon-server
          predicates:
            - After=1999-05-09T11:30:11.965+08:00[Asia/Shanghai]
  profiles: exception_route

自定义异常
先看默认异常处理
org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler中的getRoutingFunction()方法就是控制返回格式的,原代码如下,默认异常信息返回的是HTML格式。这里我们自定义的时候需要改变为JSON的。

protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
    return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
}

原始的方法是通过status来获取对应的HttpStatus的,对应的是整形数字,如果你的状态字段不是status,需要重写此方法。

protected int getHttpStatus(Map<String, Object> errorAttributes) {
    return (Integer)errorAttributes.get("status");
}

重写后的如下

@Slf4j
public class GatewayExceptionHandler extends DefaultErrorWebExceptionHandler {

    public GatewayExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
                                   ErrorProperties errorProperties, ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    /**
     * 异常处理,定义返回报文格式
     */
    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Throwable error = super.getError(request);
        log.error(
                "请求发生异常,请求URI:{},请求方法:{},异常信息:{}",
                request.path(), request.methodName(), error.getMessage()
        );
        String errorMessage;
        if (error instanceof NotFoundException) {
            String serverId = StringUtils.substringAfterLast(error.getMessage(), "Unable to find instance for ");
            serverId = StringUtils.replace(serverId, "\"", StringUtils.EMPTY);
            errorMessage = String.format("无法找到%s服务", serverId);
        } else if (StringUtils.containsIgnoreCase(error.getMessage(), "connection refused")) {
            errorMessage = "目标服务拒绝连接";
        } else if (error instanceof TimeoutException) {
            errorMessage = "访问服务超时";
        } else if (error instanceof ResponseStatusException
                && StringUtils.containsIgnoreCase(error.getMessage(), HttpStatus.NOT_FOUND.toString())) {
            errorMessage = "未找到该资源";
        } else {
            errorMessage = "网关转发异常";
        }
        Map<String, Object> errorAttributes = new HashMap<>(2);
        errorAttributes.put("msg", errorMessage);
        errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        return errorAttributes;
    }
    // 设置返回内容为JSON
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }
    // 设置HttpStatus
    @Override
    protected int getHttpStatus(Map<String, Object> errorAttributes) {
        return (int) errorAttributes.get("code");
    }
}

覆盖默认的异常处理

重写errorWebExceptionHandler()方法,里面的实现用我们自己定义的异常处理(GatewayExceptionHandler)来实现,否则仍然走默认的。

@Configuration
public class GatewayErrorConfigure {

        private final ServerProperties serverProperties;
        private final ApplicationContext applicationContext;
        private final ResourceProperties resourceProperties;
        private final List<ViewResolver> viewResolvers;
        private final ServerCodecConfigurer serverCodecConfigurer;

        public GatewayErrorConfigure(ServerProperties serverProperties,
                                     ResourceProperties resourceProperties,
                                     ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                     ServerCodecConfigurer serverCodecConfigurer,
                                     ApplicationContext applicationContext) {
            this.serverProperties = serverProperties;
            this.applicationContext = applicationContext;
            this.resourceProperties = resourceProperties;
            this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
            this.serverCodecConfigurer = serverCodecConfigurer;
        }

        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
            GatewayExceptionHandler exceptionHandler = new GatewayExceptionHandler(
                    errorAttributes,
                    this.resourceProperties,
                    this.serverProperties.getError(),
                    this.applicationContext);
            exceptionHandler.setViewResolvers(this.viewResolvers);
            exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
            exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
            return exceptionHandler;
    }
}

启动应用,关闭ribbon-server,访问127.0.0.1:8711/user/1可以看到自定义的异常处理执行了。

相对于Zuul来说,目前Spring Cloud Gateway是Spring Cloud 官方主要推广的,当然如果你已经使用的是Zuul,也可以不用替换,因为一旦集群部署以后,一般企业也达不到这个瓶颈。

更新时间:2020-05-09 11:17:29

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/springcloud13网关之gateway下路由配置
最后更新:2020-05-09 11:17:29

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×