【云原生&微服务>SCG网关篇七】Spring Cloud Gateway基于内置Filter实现限流、熔断、重试

一、前言

至此微服务网关系列文章已出:

  1. 【云原生&微服务>SCG网关篇一】为什么要有网关、生产环境如何选择网关
  2. 云原生&微服务>SCG网关篇二】生产上那些灰度发布方式
  3. 【云原生&微服务>SCG网关篇三】Spring Cloud Gateway是什么、详细使用案例
  4. 云原生&微服务>SCG网关篇四】Spring Cloud Gateway内置的11种PredicateFactory如何使用
  5. 【云原生&微服务>SCG网关篇五】Spring Cloud Gateway自定义PredicateFactory
  6. 【云原生&微服务>SCG网关篇六】Spring Cloud Gateway内置的18种Filter使用姿势

聊了以下问题:

  1. 为什么要有网关?网关的作用是什么?
  2. 网关的分类?
  3. 网关的技术选型?
  4. 使用网关时常用的灰度发布方式有哪些?
  5. Spring Cloud Gateway是什么?详细使用案例?
  6. Spring Cloud Gateway内置的11种PredicateFactory
  7. 如何自定义PredicateFactory?
  8. Spring Cloud Gateway内置的18种常用的Filter

本文接着聊Spring Cloud Gateway基于内置Filter如何实现限流、熔断、重试。

PS:SpringCloud版本信息:

<properties>
    <spring-boot.version>2.4.2</spring-boot.version>
    <spring-cloud.version>2020.0.1</spring-cloud.version>
    <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合spring cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合spring cloud alibaba-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

二、结合Redis实现限流(RequestRateLimiterGatewayFilterFactory)

Spring Cloud Gateway可以使用RequestRateLimiter GatewayFilter factory结合Redis基于令牌桶算法实现限流功能;如果请求无法通过限流,则会返回HTTP 429 - Too Many Requests

RequestRateLimiter 采用可选的keyResolver参数和特定于速率限制器的参数;

keyResolver参数是实现KeyResolver接口的Bean,KeyResolver接口让 可插入策略 派生出限制请求的密钥;
KeyResolver的默认实现是PrincipalNameKeyResolver,它从ServerWebExchange中检索出Principal并调用Principal.getName()。

结合Redis实现限流需要在gateway项目中引入spring-boot-starter-data-redis-reactive的Spring Boot starter,因为Redis的实现基于Stripe工作的。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

否者会报错:
在这里插入图片描述

RequestRateLimiter速率限制器的参数如下:

  1. redis-rate-limiter.replenishRate,在不丢弃请求的情况下,允许用户每秒执行的请求数,即:每秒令牌桶的填充速率
  2. redis-rate-limiter.burstCapacity,表示允许用户在一秒钟内执行的最大请求数,即:令牌桶可以容纳的令牌数;将此值设置为零会阻止所有请求。
  3. redis-rate-limiter.requestedTokens,表示一个请求需要多少个令牌,即:针对每个请求从令牌桶提取的令牌数,默认为1。

通过设置redis-rate-limiter.replenishRateredis-rate-limiter.burstCapacity相等可以实现稳定限流速率。

如果将redis-rate-limiter.burstCapacity设置为高于 redis-rate-limiter.replenishRate,则允许临时的挤满 / 冲突

  • 这种情况下,速率限制器(rate limiter)会在两次挤满 / 突发之间留出一段时间(根据令牌桶填充速率决定速率,比如:填充速率是5,令牌最大数是10,则富余5个令牌,可以临时顶1s),因为两次连续的挤满 / 冲突发会导致请求丢失(HTTP 429 - Too Many Requests)。

1、不指定KeyResolver的限流

结合Redis实现的限流,当不指定KeyResolver参数时,会采用KeyResolver的默认实现,对所有请求进行限流;

示例:

redis:
  host: 127.0.0.1
  port: 6379

server:
  port: 9999

spring:
  cloud:
    gateway:
      routes:
        - id: add_request_parameter_route
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/**
          filters:
            # 请求数限流 名字不能随便写 ,使用默认的factory,默认使用令牌桶算法
            - name: RequestRateLimiter
              args:
                # 针对请求IP进行限流
#                key-resolver: "#{@ipKeyResolver}"
                # 在不丢弃请求的情况下,允许用户每秒执行的请求数,即令牌桶填充速率:1/s(一秒放一个)
                redis-rate-limiter.replenishRate: 1
                # 表示允许用户在一秒钟内执行的最大请求数,即:令牌桶可以容纳的令牌数;将此值设置为零会阻止所有请求
                redis-rate-limiter.burstCapacity: 1
                # 表示一个请求需要多少个令牌,默认为1
                redis-rate-limiter.requestedTokens: 1

这里表示对所有的请求,每秒仅允许一个请求过去,其余的请求都会被拦截,报错:HTTP 429 - Too Many Requests

在这里插入图片描述

2、指定KeyResolver的限流

KeyResolver接口主要用于设置限流请求的key;我们可以通过指定KeyResolver将请求进行分批限流,比如针对同一个IP的请求做限流;

1> 传建一个KeyResolver接口的实现类:IpKeyResolver

package com.saint.gateway.filter;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * KeyResolver接口主要用于设置限流请求的key,通过实现该接口指定需要对当前请求中对哪些因素进行流量控制。
 */
@Component("ipKeyResolver")
public class IpKeyResolver implements KeyResolver {
    
    
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
    
    
        // 根据请求IP来限流
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
}

2> 在yaml中配置KeyResolver

redis:
  host: 127.0.0.1
  port: 6379

server:
  port: 9999

spring:
  cloud:
    gateway:
      routes:
        - id: add_request_parameter_route
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/**
#            - Path=/gateway/**
          filters:
            # 请求数限流 名字不能随便写 ,使用默认的factory,默认使用令牌桶算法
            - name: RequestRateLimiter
              args:
                # 针对请求IP进行限流
                key-resolver: "#{@ipKeyResolver}"
                # 在不丢弃请求的情况下,允许用户每秒执行的请求数,即令牌桶填充速率:1/s(一秒放一个)
                redis-rate-limiter.replenishRate: 1
                # 表示允许用户在一秒钟内执行的最大请求数,即:令牌桶可以容纳的令牌数;将此值设置为零会阻止所有请求
                redis-rate-limiter.burstCapacity: 1
                # 表示一个请求需要多少个令牌,默认为1
                redis-rate-limiter.requestedTokens: 1

三、熔断

1、SpringCloudCircuitBreakerFilterFactory

Spring Cloud Gateway可以使用CircuitBreaker GatewayFilter factory基于Spring Cloud CircuitBreaker APIs在断路器中包裹网关路由;Spring Cloud CircuitBreaker中存在很多支持Spring Cloud Gateway使用的库,比如:开箱即用的Resilience4J

和RequestRateLimiter结合Redis实现限流需要引入spring-boot-starter-data-redis-reactive一样,CircuitBreaker需要引入spring-cloud-starter-circuitbreaker-reactor-resilience4j

<!--熔断、降级-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

关于Spring Cloud CircuitBreaker,参考官方文章:https://cloud.spring.io/spring-cloud-circuitbreaker/reference/html/spring-cloud-circuitbreaker.html

1)针对所有的请求断路

1> 自定义一个ReactiveResilience4JCircuitBreakerFactory:

package com.saint.gateway.config;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

/**
 * @author Saint
 */
@Configuration
public class GatewayConfiguration {
    
    

    @Bean(name = "myCircuitBreaker")
    public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
    
    
        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                // 超时规则,默认1s,这里是3秒
                .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(3)).build())
                .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
                .build());
    }
}

断路器CircuitBreaker中配置了路由的超时时间为3s,默认为1s。

2> 在application.yml中指定过滤器Filter:

spring:
  cloud:
    gateway:
      routes:
        - id: add_request_parameter_route
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/**
          filters:
            - CircuitBreaker=myCircuitBreaker

访问Spring Cloud Gateway详细使用案例中的/hello/timeout接口,其中线程睡眠了5s,所以一定会超时。

在这里插入图片描述

到这里超时的场景已经验证完毕,再修改CircuitBreaker的配置,将超时时间修改为6s,再访问一下接口:

在这里插入图片描述

已经可以正常响应。

当断路器生效、断路之后,我们想自定义它返回的信息,可以通过制定断路后的fallbackURI实现。

指定断路后的fallbackURI(gateway内部)

Spring Cloud CircuitBreaker filter可以指定一个可选择的参数fallbackUri,用于指定断路后重定向的URI。重定向的URI可以是Gateway项目内部的Controller、Route,也可以是外部的链接。

1> 在Gateway项目中新增一个Controller:

package com.saint.gateway.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class FallbackController {
    
    
    @RequestMapping("/defaultFallback")
    public Map defaultFallback() {
    
    
        Map map = new HashMap<>();
        map.put("code", 999);
        map.put("message", "server error");
        return map;
    }
}

2> 在application.yml文件中指定fallbackURI:

server:
  port: 9999
spring:
  cloud:
    gateway:
      routes:
        - id: add_request_parameter_route
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/**
          filters:
            - name: CircuitBreaker
              args:
                name: myCircuitBreaker
                # 指定断路后的重定向地址
                fallbackUri: forward:/defaultFallback

当断路器断路后,会将请求重定向到当前Gateway服务的/defaultFallback(http://127.0.0.1:9999/defaultFallback)下。

在这里插入图片描述

由于请求已经被重定向,所以返回的状态码是200,而不再是504,Response Body也变成了相应重定向后地址的响应体。

指定断路后的fallbackURI(gateway外部)

我们也可以通过将外部地址配置到Route的方式,进而fallback到路由上,实现短路后fallback重定向到外部链接的需求。

server:
  port: 9999
spring:
  cloud:
    gateway:
      routes:
        - id: ingredients-fallback
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/fallback
          # 通过过滤器将地址重写为:/hello/sayParam
          filters:
            - SetPath=/hello/sayParam
        - id: add_request_parameter_route
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/**
          filters:
            - name: CircuitBreaker
              args:
                name: myCircuitBreaker
                # 指定断路后的重定向地址(route的方式)
                fallbackUri: forward:/fallback

示例中将外部地址http://127.0.0.1:9001//hello/sayParam配置到Path为/fallback的Route上,并且在断路中配置fallbackUri为:forward:/fallback;即当断路器断路时,fallback地址为:http://127.0.0.1:9001//hello/sayParam

在这里插入图片描述

2)针对返回的状态码 断路

在某些情况下,我们可能希望根据Route返回的状态码使断路器跳闸;断路器配置对象拥有一个状态码的集合,如果路由返回的状态码在集合中,则断路器跳闸;状态码可以使用代表码值的整数 或 HttpStatus枚举的字符串。

server:
  port: 9999

spring:
  cloud:
    gateway:
      routes:
        - id: add_request_parameter_route
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/**
          filters:
            - name: CircuitBreaker
              args:
                name: myCircuitBreaker
                # 指定断路后的重定向地址(gateway内部controller)
                fallbackUri: forward:/defaultFallback
                # 当且仅当出现如下状态码时,断路器才会生效
                statusCodes:
                  - 500
                  - "NOT_FOUND"

当路由返回的状态码为500、或404(“NOT_FOUND”)时会触发断路器,其余状态码均不会。

在这里插入图片描述

/hello/retryRoute接口会中会直接报错,返回500,断路器将500的状态码进行了拦截。

如果不配置statusCodes,那么针对/hello/retryRoute接口的熔断器不会生效,依旧是直接报错。

2、FallbackHeadersGatewayFilterFactory

FallbackHeaders factory 可以将一些Spring Cloud CircuitBreaker执行的异常细节添加到fallbackUri的请求头中,请求头中的属性可以包括:

  • executionExceptionTypeHeaderName (“Execution-Exception-Type”)
  • executionExceptionMessageHeaderName (“Execution-Exception-Message”)
  • rootCauseExceptionTypeHeaderName (“Root-Cause-Exception-Type”)
  • rootCauseExceptionMessageHeaderName (“Root-Cause-Exception-Message”)
spring:
  cloud:
    gateway:
      routes:
        - id: ingredients-fallback
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/fallback
          # 通过过滤器将地址重写为:/hello/sayParam
          filters:
            - SetPath=/hello/sayParam
        - id: add_request_parameter_route
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/**
          filters:
            - name: CircuitBreaker
              args:
                name: myCircuitBreaker
                # 指定断路后的重定向地址(route的方式)
                fallbackUri: forward:/fallback
                # 当且仅当出现如下状态码时,断路器才会生效
                statusCodes:
                  - 500
                  - "NOT_FOUND"
            - name: FallbackHeaders
              args:
                executionExceptionTypeHeaderName: Saint-Test-Header

访问:http://127.0.0.1:9999/hello/retryRoute,debug fateway-center项目,可以发现请求进了FallbackHeadersGatewayFilterFactory,不过由于FallbackHeadersGatewayFilterFactory中无法从ServerWebExchange中获取到的circuitBreakerExecutionException属性值为null,所以没有异常信息记录到fallbackUri的请求头中;

在这里插入图片描述

从上图的代码逻辑也可以看出,只有当circuitBreakerExecutionException属性有值时,才会将异常信息记录到fallbackUri的请求头中。

四、重试(RetryGatewayFilterFactory)

Retry GatewayFilter factory支持以下参数:

  1. retries: 重试次数;
  2. statuses: 可以进行重试的响应的HTTP状态码, 使用org.springframework.http.HttpStatus表示;
  3. methods: 可以进行重试的HTTP methods, 使用org.springframework.http.HttpMethod表示;
  4. series: 可以进行重试的The series of status codes,使用org.springframework.http.HttpStatus.Series表示;
  5. exceptions: 可以进行重试的Thrown Exceptions 列表;
  6. backoff:为重试配置指数级的重试时间间隔;
    • firstBackoff *(factor ^ n)的重试间隔后执行重试,其中n是迭代(第几次重试);
    • 如果配置了maxBackoff,则重试的最大时间间隔为maxBackoff
    • 如果basedOnPreviousValue被设置为true,则重试时间间隔使用prevBackoff * factor 计算。
spring:
  cloud:
    gateway:
      routes:
        - id: add_request_parameter_route
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/**
          filters:
            # 重试
            - name: Retry
              args:
                # 请求重试次数,默认值是3
                retries: 3
                # 可以进行重试的状态码(500、404),
                statuses: INTERNAL_SERVER_ERROR,NOT_FOUND
                # 可以进行重试的Http Method
                methods: GET,POST
                # 重试的时间间隔配置
                backoff:
                  # 第一次重试的时间间隔
                  firstBackoff: 100ms
                  # 最大重试时间间隔
                  maxBackoff: 500ms
                  # 时间间隔因子
                  factor: 2
                  # 关闭根据上次重试时间间隔计算当前重试时间间隔功能
                  basedOnPreviousValue: false

示例效果:

请求地址:http://127.0.0.1:9999/hello/retryRoute

服务的日志输出:
在这里插入图片描述

第一次请求失败后,100ms进行了一次重试,再200ms之后进行了一次重试,再400ms之后进行了一次重试;一共重试三次。

五、总结

本文聊了Spring Cloud Gateway基于RequestRateLimiterGatewayFilterFactory实现限流、基于SpringCloudCircuitBreakerFilterFactory实现熔断、基于RetryGatewayFilterFactory实现重试;

另外本文相关案例全部来自:【云原生&微服务>SCG网关篇三】Spring Cloud Gateway是什么、详细使用案例

猜你喜欢

转载自blog.csdn.net/Saintmm/article/details/125783631