0
点赞
收藏
分享

微信扫一扫

【云原生&微服务>SCG网关篇十二】Spring Cloud Gateway集成Sentinel API实现多种限流方式

文章目录

一、前言

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

聊了以下问题:

我们已经聊过了Spring Cloud Gateway的一种限流方式:使用内置的Filter(RequestRateLimiterGatewayFilterFactory)结合Redis使用令牌桶算法实现限流;这里我们再聊一下另外一种:Spring Cloud Gateway集成Sentinel实现限流。

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>

二、Gateway集成Sentinel API

sentinel服务1.6.0以上的版本可支持整合到网关进行统一流控:

sentinel提供了两种资源维度的限流:

  • route维度:在配置文件中配置路由,资源名为对应的 routeId,一般是对某个微服务进行限流;这种维度属于粗粒度的限流。
  • 自定义API维度:通过Sentinel 提供的API来自定义一些API分组,针对某一类的uri进行匹配限流,可以跨多个微服务;这种维度属于细粒度的限流。

gateway整合sentinel默认不支持 URL 粒度限流;因此通过 Spring Cloud Alibaba 接入时,将spring.cloud.sentinel.filter.enabled 配置为 false,以关闭流控控制台上的 URL 资源视图。

gateway整合Sentinel的maven依赖:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
    <version>1.8.0</version>
</dependency>

0、集成Sentinel的核心概念

整体集成原理如下:
请添加图片描述

1)GatewayFlowRule 和 ApiDefinition

网关的限流规则GatewayFlowRule:

  • 针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流;
    用户自定义的API分组ApiDefinition:
  • ApiDefinition可以看做是一些 URL 匹配的组合;限流的时候可以针对这个自定义的 API 分组进行限流。

2)GatewayFlowRule字段解释

网关限流规则 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:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)
  • matchStrategy:参数值的匹配策略,目前支持精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持)

可以通过GatewayRuleManager.loadRules(rules) 手动加载网关规则、 或 通过 GatewayRuleManager.register2Property(property) 注册规则(推荐方式);

特别注意:当使用 Spring Cloud Alibaba Sentinel 数据源模块时,需要注意网关流控规则数据源类型是 gw-flow,若将网关流控规则数据源指定为 flow 则不生效。

application.yml配置文件:

spring:
  application:
    name: nacos-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 106.15.139.143:8848
    gateway:
      discovery:
        locator:
          # 开启从注册中心动态创建路由的功能
          enabled: true
          # 是否使用service-id的小写,默认是大写
          lower-case-service-id: true
      routes:
        - id: gateway-nacos-service-route
          # 其中配置的lb://表示从注册中心获取服务,后面的gateway-nacos-provider表示目标服务在注册中心上的服务名
          uri: lb://gateway-nacos-provider
          predicates:
            - Path=/nacos/sentinel/** #被限流
          filters:
            - StripPrefix=2
        # 自定义Sentinel API分组限流
        - id: red_route
          uri: lb://gateway-nacos-provider
          predicates:
            - Path=/red/** #被限流
          filters:
            - StripPrefix=1
        - id: green_route
          uri: lb://gateway-nacos-provider
          predicates:
            - Path=/green/** #不被限流
          filters:
            - StripPrefix=1

下面的三个样例均依赖此application.yml配置文件。

1、针对Route维度限流

搞一个ConfigurationClass,在类初始化的最后阶段(@PostConstruct标注的方法)实例化一个GatewayFlowRule,并将其添加到GatewayRuleManager中;GatewayFlowRule中指定针对哪个Route限流、限流的规则是什么?

package com.saint.gateway.sentinel;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;

/**
 * 针对某个Route限流
 *
 * @author Saint
 */
@Configuration
public class GatewayConfiguration {
    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

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

    /**
     * 注入一个全局限流过滤器SentinelGatewayFilter
     * Filter的优先级最高
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 注入限流异常处理器
     * Filter的优先级最高
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 初始化限流规则
     */
    @PostConstruct
    public void doInit() {
        initGatewayRules();
    }

    /**
     * Route维度限流规则
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        // 这里表示1s仅允许通过一个请求,GatewayFlowRule构造函数中的如参为路由名
        GatewayFlowRule gatewayFlowRule = new GatewayFlowRule("gateway-nacos-service-route").setCount(1).setIntervalSec(1);
        rules.add(gatewayFlowRule);
        GatewayRuleManager.loadRules(rules);
    }
}

当前案例表示针对routeId为gateway-nacos-service-route的路由做限流:1s仅允许一个请求通过;

验证

在这里插入图片描述

当请求被限流时会返回429状态码,响应体内容为:

{"code":429,"message":"Blocked by Sentinel: ParamFlowException"}

2、针对API维度限流

自定义API分组限流,将/nacos/sentinel/**/red/**进行统一分组,并提供name=saint_customized_api,然后在初始化网关限流规则时,针对该name设置限流规则;

package com.saint.gateway.sentinel;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;

/**
 * 自定义API分组限流
 *
 * @author Saint
 */
@Configuration
public class GatewayConfiguration1 {
    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

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

    /**
     * 注入SentinelGatewayFilter
     * Filter的优先级最高
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 注入限流异常处理器
     * Filter的优先级最高
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 初始化限流规则
     */
    @PostConstruct
    public void doInit() {
        initCustomizedApis();
        initGatewayRules();
    }

    /**
     * 自定义API分组限流,将/nacos/sentinel/**和/red/**进行统一分组,并提供name=saint_customized_api,然后在初始化网关限流规则时,针对该name设置
     * 限流规则。同时,可以通过setMatchStrategy来设置不同path下的限流参数策略
     */
    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        // 自定义针对API限流的ApiDefinition,apiName可以随便取,在GatewayFlowRule中填入即可。
        ApiDefinition apiDefinition = new ApiDefinition("saint_customized_api");
        // 匹配下面请求路径的请求将被限流
        apiDefinition.setPredicateItems(new HashSet<ApiPredicateItem>() {
            {
                add(new ApiPathPredicateItem().setPattern("/nacos/sentinel/**")
                        .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                add(new ApiPathPredicateItem().setPattern("/red/**")
                        .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
            }
        });
        definitions.add(apiDefinition);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

    /**
     * 针对分组name来设置限流规则
     */
    private void initGatewayRules() {
        // 针对ApiDefinition进行限流
        GatewayFlowRule rule = new GatewayFlowRule("saint_customized_api")
                .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME).setCount(1)
                .setIntervalSec(1);
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(rule);
        GatewayRuleManager.loadRules(rules);
    }
}

验证

在这里插入图片描述

当我们访问/nacos/sentinel/**/red/**路径时才会被限流,其他路径均不会。

3、自定义限流异常返回值

Sentinel默认的异常处理器是com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler,我们可以根据这个Handler自定义一个WebExceptionHandler实现类。

1> 自定义WebExceptionHandler实现类:

package com.saint.gateway.sentinel;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.util.function.Supplier;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * 异常处理器
 */
public class MySentinelGatewayBlockExceptionHandler implements WebExceptionHandler {
    private List<ViewResolver> viewResolvers;
    private List<HttpMessageWriter<?>> messageWriters;
    private final Supplier<ServerResponse.Context> contextSupplier = () -> {
        return new ServerResponse.Context() {
            public List<HttpMessageWriter<?>> messageWriters() {
                return MySentinelGatewayBlockExceptionHandler.this.messageWriters;
            }

            public List<ViewResolver> viewResolvers() {
                return MySentinelGatewayBlockExceptionHandler.this.viewResolvers;
            }
        };
    };

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

    /**
     * 该方法的作用是,将限流的异常信息写回客户端
     */
    private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
        ServerHttpResponse serverHttpResponse = exchange.getResponse();
        serverHttpResponse.getHeaders().add("Content-type", "application/json;charset=UTF-8");
        byte[] datas = "{\"code\":999,\"msg\":\"访问人数太多了,让我歇歇吧\"}".getBytes();
        DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
        return serverHttpResponse.writeWith(Mono.just(buffer));
    }

    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        } else {
            return !BlockException
                    .isBlockException(ex) ? Mono.error(ex)
                    : this.handleBlockedRequest(exchange, ex).flatMap((response) -> this.writeResponse(response, exchange));
        }
    }

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

2> 将MySentinelGatewayBlockExceptionHandler注入到Spring容器中:

// 注入自定义的限流异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public MySentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
    return new MySentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}

验证

接着根据API分组限流的案例进行验证;

在这里插入图片描述

响应的状态码是200,body是我们自定义的body:

{"code":999,"msg":"访问人数太多了,让我歇歇吧"}

三、总结

sentinel提供了两种资源维度的限流:

  • route维度:在配置文件中配置路由,资源名为对应的 routeId,一般是对某个微服务进行限流;这种维度属于粗粒度的限流。
  • 自定义API维度:通过Sentinel 提供的API来自定义一些API分组,针对某一类的uri进行匹配限流,可以跨多个微服务;这种维度属于细粒度的限流。

本文的演示案例基于文章()

PS:Sentinel整合Nacos配置源:

spring:
  cloud: #配置SpringCloudGateway的路由
    sentinel:
      transport:
        dashboard: ip:port   #sentinel控制台的请求地址
      datasource:
        ds1:
          nacos:
            server-addr: ip:port
            data-id: ${spring.application.name}-${spring.profiles.active}-sentinel-gw-flow
            group-id: DEFAULT_GROUP
            data-type: json
            # 流控规则
            rule-type: gw-flow
        ds2:
          nacos:
            server-addr: ip:port
            data-id: ${spring.application.name}-${spring.profiles.active}-sentinel-gw-api-group
            group-id: DEFAULT_GROUP
            data-type: json
            # api类型
            rule-type: gw-api-group
      eager: true #立即加载
      log:
        # 日志存放目录
        dir: /data/sentinel/gateway

引入maven依赖:

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

相关推荐

0 条评论