Spring Cloud——Spring Cloud GateWay熔断、降级、限流

实现熔断降级

在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免的会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。

为什么在网关上请求失败需要快速返回给客户端?

因为当一个客户端请求发生故障的时候,这个请求会一直堆积在网关上,当然只有一个这种请求,网关肯定没有问题(如果一个请求就能造成整个系统瘫痪,那这个系统可以下架了),但是网关上堆积多了就会给网关乃至整个服务都造成巨大的压力,甚至整个服务宕掉。因此要对一些服务和页面进行有策略的降级,以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的响应,所以需要网关上请求失败需要快速返回给客户端。

Spring Cloud Gateway 集成熔断、限流

集成 Hystrix 熔断降级,引用hystrix依赖,在filters下加入熔断降级配置,设置降级后返回的路由,同时配置默认使用信号量隔离、3秒主动超时。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
server:
  port: 8082

spring:
  application:
    name: gateway
  redis:
      host: localhost
      port: 6379
      password: 123456
  cloud:
    gateway:
      routes:
        - id: rateLimit_route
          uri: http://localhost:8000
          order: 0
          predicates:
            - Path=/test/**
          filters:
            - StripPrefix=1
            - name: Hystrix
              args:
                name: defaultfallback
                fallbackUri: forward:/defaultfallback

# hystrix 信号量隔离,1.5秒后自动超时
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE
          thread:
            timeoutInMilliseconds: 1500
  # hystrix.shareSecurityContext 属性为true。这样做会自动配置一个Hystrix 并发策略插件钩子,
  # 然后会将SecurityContext从你当前主线程传输到一个使用Hystrix Command注解的地方。
  shareSecurityContext: true

这里的配置,使用了两个过滤器:

  • 1)过滤器StripPrefix,作用是去掉请求路径的最前面n个部分截取掉。
    StripPrefix=1就代表截取路径的个数为1,比如前端过来请求/test/good/1/view,匹配成功后,路由到后端的请求路径就会变成http://localhost:8888/good/1/view

  • 2)过滤器Hystrix,作用是通过Hystrix进行熔断降级
    当上游的请求,进入了Hystrix熔断降级机制时,就会调用fallbackUri配置的降级地址。需要注意的是,还需要单独设置Hystrix的commandKey的超时时间

fallbackUri配置的网关降级地址的代码如下:

@RestController
public class FallbackController {

    @GetMapping("/defaultfallback")
    public Response defaultfallback() {
        Response response = new Response();
        response.setCode("100");
        response.setMessage("服务暂时不可用");
        return response;
    }
}

分布式限流

漏桶算法


漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(Traffic Policing),漏桶算法的描述如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;
  • 如果桶是空的,则不需流出水滴;
  • 可以以任意速率流入水滴到漏桶;
  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

令牌桶算法

从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

SpringCloudGateway限流方案

在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用在Redis内的通过执行Lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:

Spring Cloud Gateway 默认实现 Redis限流,如果扩展只需要实现Ratelimter接口即可,同时也可以通过自定义KeyResolver来指定限流的Key,比如我们需要根据用户、IP、URI来做限流等等,通过exchange对象可以获取到请求信息。

引入redis-reactive依赖

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

<!--springboot2.X默认使用lettuce连接池,需要引入commons-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.54</version>
</dependency>

application.yml下添加限流配置:

server:
  port: 8081
spring:
  cloud:
    gateway:
      routes:
        - id: order-center
          uri: lb://order-center
          predicates:
            - Path=/order/**
          filters:
            - name: RequestRateLimiter
              args:
                # 使用SpEL名称引用Bean,与上面新建的RateLimiterConfig类中的bean的name相同
                key-resolver: '#{@userKeyResolver}'
                # 每秒最大访问次数
                redis-rate-limiter.replenishRate: 200
                 # 令牌桶最大容量
                redis-rate-limiter.burstCapacity: 200
  application:
    name: cloud-gateway
  redis:
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        max-active: 200 #连接池最大连接数(使用负值表示没有限制)
        max-idle: 20 # 连接池中的最大空闲连接
        min-idle: 5 #连接池中的最小空闲连接
        max-wait: 1000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
  • filter名称必须是RequestRateLimiter
  • redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求
  • redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数
  • key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。

限流配置类

@Configuration
public class RequestRateLimiterConfig {

    /**
     * 按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态
     */
    @Bean
    @Primary
    KeyResolver apiKeyResolver() {
        //按URL限流
        return exchange -> Mono.just(exchange.getRequest().getPath().toString());
    }

    /**
     * 这里根据用户ID限流,请求路径中必须携带userId参数
     */
    @Bean
    KeyResolver userKeyResolver() {
        //按用户限流
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }

    /**
     * 通过exchange对象可以获取到请求信息,这边用了HostName
     */
    @Bean
    KeyResolver ipKeyResolver() {
        //按IP来限流
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }

}

健康检查配置

actuator健康检查配置,为之后的功能提供支持,此部分比较简单,不再赘述,加入以下maven依赖和配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# actuator 监控配置
management:
  #actuator端口 如果不配置做默认使用上面8080端口
  server:
    port: 8080
  endpoints:
    web:
      exposure:
        #默认值访问health,info端点  用*可以包含全部端点
        include: "*"
      #修改访问路径 2.0之前默认是/; 2.0默认是/actuator可以通过这个属性值修改
      base-path: /actuator
  endpoint:
    shutdown:
      enabled: true #打开shutdown端点
    health:
      show-details: always #获得健康检查中所有指标的详细信息

统一配置跨域请求

现在的请求通过经过gateWay网关时,需要在网关统一配置跨域请求,需求所有请求通过

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origins: "*"
            allowed-headers: "*"
            allow-credentials: true
            allowed-methods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION

整合Sentinel完成流控和降级

使用Sentinel作为gateWay的限流、降级、系统保护工具

Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:

GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。

ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。

其中网关限流规则 GatewayFlowRule 的字段解释如下:

  • resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
  • resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。
  • grade:限流指标维度,同限流规则的 grade 字段
  • count:限流阈值
  • intervalSec:统计时间窗口,单位是秒,默认是 1 秒
  • controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
  • burst:应对突发请求时额外允许的请求数目。
  • maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
  • paramItem:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:
  • parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。
  • fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。
  • pattern 和 matchStrategy:为后续参数匹配特性预留,目前未实现。

用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或通过 GatewayRuleManager.register2Property(property) 注册动态规则源动态推送(推荐方式)。

maven依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<!-- sentinel适配支持Spring Cloud Gateway-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>

配置文件

客户端配置:在配置文件中增加下列配置,dashboard就可以轻松管理客户端了,还有一种方式是在启动时加入

server:
  port: 8003
spring:
  application:
    name: gateway-center    
  cloud:
    sentinel:
      transport:
        ## VM
        ##-Djava.net.preferIPv4Stack=true -Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.api.port=8666 -Dproject.name=gateway -Dcsp.sentinel.app.type=1
        dashboard: localhost:8880
        port: 8880
    gateway:
      routes:
        - id: path_route
          uri: lb://user-center
          predicates:
            - Path=/user/**

配置gateway sentinel配置

@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
     * 自定义异常提示:当发生限流、熔断异常时,会返回定义的提示信息。
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public JsonSentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 由于sentinel的工作原理其实借助于全局的filter进行请求拦截并计算出是否进行限流、熔断等操作的,增加SentinelGateWayFilter配置
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * sentinel 不仅支持通过硬代码方式进行资源的申明,还能通过注解方式进行声明,
     * 为了让注解生效,还需要配置切面类SentinelResourceAspect
     * @return
     */
    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        return new SentinelResourceAspect();
    }

    @PostConstruct
    public void doInit() {
        initGatewayRules();
    }

    /**
     * 配置限流规则
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        //order-center 路由ID,和配置文件中的一致就行
        rules.add(new GatewayFlowRule("order-center")
                .setCount(1) // 限流阈值
                .setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
        );
        GatewayRuleManager.loadRules(rules);
    }
}

指定参数限流

上面的配置是针对整个路由来限流的,如果我们只想对某个路由的参数做限流,那么可以使用参数限流方式:

 rules.add(new GatewayFlowRule("path_route")
     .setCount(1)
     .setIntervalSec(1)
     .setParamItem(new GatewayParamFlowItem()
                .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM).setFieldName("vipType")
     )
 );

通过指定PARAM_PARSE_STRATEGY_URL_PARAM表示从url中获取参数,setFieldName指定参数名称

自定义API分组

假设我有下面两个路由,我想让这两个路由共用一个限流规则,那么我们可以自定义进行组合:

- id: product-center
  uri: lb://product-center
  predicates:
    - Path=/product/**
- id: order-center
  uri: lb://order-center
  predicates:
    - Path=/order/**

自定义分组代码:

private void initCustomizedApis() {
    Set<ApiDefinition> definitions = new HashSet<>();
    ApiDefinition api1 = new ApiDefinition("customized_api")
        .setPredicateItems(new HashSet<ApiPredicateItem>() {{
         // product完全匹配
         add(new ApiPathPredicateItem().setPattern("/product"));
         // order/开头的
         add(new ApiPathPredicateItem().setPattern("/order/**")
                .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX));
        }});
    definitions.add(api1);
    GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}

然后我们需要给customized_api这个资源进行配置:

 rules.add(new GatewayFlowRule("customized_api")
      .setCount(1)
      .setIntervalSec(1)
 ); 

自定义异常提示

当触发限流后页面显示的是Blocked by Sentinel: FlowException,正常情况下,就算给出提示也要跟后端服务的数据格式一样,如果你后端都是JSON格式的数据,那么异常的提示也要是JSON的格式,所以问题来了,我们怎么去自定义异常的输出?

前面我们有配置SentinelGatewayBlockExceptionHandler,需要重写SentinelGatewayBlockExceptionHandler中的输出方法

/**
 * @Author: huangyibo
 * @Date: 2021/8/22 18:02
 * @Description: 配置 限流后异常处理 JsonSentinelGatewayBlockExceptionHandler
 *                  重写 SentinelGatewayBlockExceptionHandler
 */

@Slf4j
@Component
public class JsonSentinelGatewayBlockExceptionHandler implements WebExceptionHandler {

    private List<ViewResolver> viewResolvers;
    private List<HttpMessageWriter<?>> messageWriters;

    public JsonSentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolvers;
        this.messageWriters = serverCodecConfigurer.getWriters();
    }

    private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
        //return response.writeTo(exchange, contextSupplier.get());

        ServerHttpResponse serverHttpResponse = exchange.getResponse();
        serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        byte[] datas = "{\"code\":403,\"msg\":\"限流了\"}".getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
        return serverHttpResponse.writeWith(Mono.just(buffer));
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        }
        // This exception handler only handles rejection by Sentinel.
        if (!BlockException.isBlockException(ex)) {
            return Mono.error(ex);
        }
        return handleBlockedRequest(exchange, ex)
                .flatMap(response -> writeResponse(response, exchange));
    }

    private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
        return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
    }

    private final Supplier<ServerResponse.Context> contextSupplier = () -> new ServerResponse.Context() {
        @Override
        public List<HttpMessageWriter<?>> messageWriters() {
            return JsonSentinelGatewayBlockExceptionHandler.this.messageWriters;
        }

        @Override
        public List<ViewResolver> viewResolvers() {
            return JsonSentinelGatewayBlockExceptionHandler.this.viewResolvers;
        }
    };
}
/**
 * 配置限流的异常处理器:JsonSentinelGatewayBlockExceptionHandler
 * 自定义异常提示:当发生限流、熔断异常时,会返回定义的提示信息。
 * @return
 */
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public JsonSentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
    // Register the block exception handler for Spring Cloud Gateway.
    return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}

参考:
https://www.cnblogs.com/crazymakercircle/p/11704077.html

https://blog.csdn.net/lalacrazy/article/details/82109685

https://www.cnblogs.com/yinjihuan/p/10772558.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容