Loading

网关限流

常见的限流算法

(1) 计数器

计数器限流算法是最简单的一种限流实现方式。其本质是通过维护一个单位时间内的计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零
算法缺陷:当第十秒的时候已经接收了10个请求时,在下次重置之前都会拒绝其他请求,限流不够平滑。

(2) 漏桶算法

漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。 
为了更好的控制流量,漏桶算法需要通过两个变量进行控制:一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。
算法缺陷:虽能够使访问流量变得平滑,但无法处理突发流量。

(3) 令牌桶算法

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

基于Filter的限流

SpringCloudGateway官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory 实现。在过滤器工厂中是通过Redis和lua脚本结合的方式进行流量控制。 

(1) 环境搭建

导入redis的依赖

首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifatId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
准备redis

(2) 修改application.yml配置文件

在application.yml配置文件中加入限流的配置,代码如下: 
  spring:
    application:
      name: api-gateway #指定服务名
    cloud:
      gateway:
        routes:
        - id: order-service
          uri: lb://shop-service-order
          filters:
          - RewritePath=/order-service/(?<segment>.*), /$\{segment}
          - name: RequestRateLimiter
            args:
              # 使用SpEL从容器中获取对象
              key-resolver: '#{@pathKeyResolver}'
              # 令牌桶每秒填充平均速率
              redis-rate-limiter.replenishRate: 1
              # 令牌桶的总容量
              redis-rate-limiter.burstCapacity: 3
    redis:
      host: localhost
      port: 6379
在 application.yml 中添加了redis的信息,并配置了RequestRateLimiter的限流过滤器:
  burstCapacity,令牌桶总容量。
  replenishRate,令牌桶每秒填充平均速率。
  key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。 

(3) 配置KeyResolver

为了达到不同的限流效果和规则,可以通过实现 KeyResolver 接口,定义不同请求类型的限流键。 
@Configuration
public class KeyResolverConfiguration {
  /**
   * 基于请求路径的限流
   */
  @Bean
  public KeyResolver pathKeyResolver() {
      return exchange -> Mono.just(
              exchange.getRequest().getPath().toString()
      );
  }
  /**
   * 基于请求ip地址的限流
   */
  @Bean
  public KeyResolver ipKeyResolver() {
      return exchange -> Mono.just(
              exchange.getRequest().getHeaders().getFirst("X-Forwarded-For")
      );
  }
  /**
   * 基于用户的限流
   */
  @Bean
  public KeyResolver userKeyResolver() {
      return exchange -> Mono.just(
              exchange.getRequest().getQueryParams().getFirst("user")
      );
    }
}
使用Jmetter模拟5组线程访问,会发现如下结果,当达到令牌桶的总容量3时,其他的请求会返回429错误。 
通过reids的MONITOR命令可以监听redis的执行过程。这时候Redis中会有对应的数据: 
大括号中就是我们的限流Key,这边是IP,本地的就是localhost
  timestamp:存储的是当前时间的秒数,也就是System.currentTimeMillis() / 1000或者Instant.now().getEpochSecond()
  tokens:存储的是当前这秒钟的对应的可用的令牌数量。
Spring Cloud Gateway目前提供的限流还是相对比较简单的,在实际中我们的限流策略会有很多种情况,比如:
  1、对不同接口的限流
  2、被限流后的友好提示
这些可以通过自定义RedisRateLimiter来实现自己的限流策略,这里我们不做讨论。

基于Sentinel的限流

Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。 
从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:
  route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId。
  自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组。
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 分组维度进行限流。

(1)环境搭建

导入Sentinel 的响应依赖
<dependency>
  <groupId>com.alibaba.csp</groupId>
  <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
  <version>x.y.z</version>
</dependency>

(2) 编写配置类

package cn.yyj.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
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 配置限流过滤器
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 配置初始化的限流参数
     * 用于指定资源的限流规则.
     * 1.资源名称 (路由id)
     * 2.配置统计时间
     * 3.配置限流阈值
     */
    @PostConstruct
    public void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(new GatewayFlowRule("product-service")
                .setCount(1)
                .setIntervalSec(1)
        );
//        rules.add(new GatewayFlowRule("product_api")
//                .setCount(1)
//                .setIntervalSec(1)
//        );
        GatewayRuleManager.loadRules(rules);
    }

    /**
     * 自定义API限流分组
     * 1.定义分组
     * 2.对小组配置限流规则
     */
//    @PostConstruct
//    private void initCustomizedApis() {
//        Set<ApiDefinition> definitions = new HashSet<>();
//        ApiDefinition api1 = new ApiDefinition("product_api")
//                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
//                    add(new ApiPathPredicateItem().setPattern("/product-service/product/**"). //以/product-service/product/开头的所有url
//                            setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
//                }});
//        ApiDefinition api2 = new ApiDefinition("order_api")
//                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
//                    add(new ApiPathPredicateItem().setPattern("/order-service/order")); //完全匹配/order-service/order 的url
//                }});
//        definitions.add(api1);
//        definitions.add(api2);
//        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
//    }
}
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可。
@PostConstruct定义初始化的加载方法,用于指定资源的限流规则。这里资源的名称为 order-service ,统计时间是1秒内,限流阈值是1。表示每秒只能访问一个请求。 

(3)网关配置

  spring:
    application:
      name: api-gateway #指定服务名
    redis:
      host: 127.0.0.1
      port: 6379
      database: 0
    cloud:
      gateway:
        routes:
        - id: order-service
          uri: lb://shop-service-order
          predicates:
          - Path=/order-service/**
          filters:
          - RewritePath=/order-service/(?<segment>.*), /$\{segment}
在一秒钟内多次访问http://localhost:8080/order-service/order/buy/1就可以看到限流启作用了。 

(4)自定义异常提示

当触发限流后页面显示的是Blocked by Sentinel: FlowException。为了展示更加友好的限流提示,Sentinel支持自定义异常处理。
您可以在 GatewayCallbackManager 注册回调进行定制:
  setBlockHandler :注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为BlockRequestHandler 。默认实现为 DefaultBlockRequestHandler ,当被限流时会返回类似于下面的错误信息: Blocked by Sentinel: FlowException 。 
    /**
     * 自定义限流处理器
     */
    @PostConstruct
    public void initBlockHandlers() {
        BlockRequestHandler blockHandler = (serverWebExchange, throwable) -> {
            Map<String, Object> map = new HashMap<>();
            map.put("code", HttpStatus.TOO_MANY_REQUESTS);
            map.put("message", "当前访问人数太多请稍后再试!");
            return ServerResponse.status(HttpStatus.OK)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .body(BodyInserters.fromObject(map));
        };
        GatewayCallbackManager.setBlockHandler(blockHandler);
    }

(5) 参数限流

上面的配置是针对整个路由来限流的,如果我们只想对某个路由的参数做限流,那么可以使用参数限流方式:
        rules.add(new GatewayFlowRule("product-service")
        .setCount(1)
        .setIntervalSec(1)
        .setParamItem(new GatewayParamFlowItem()
                        .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
                        .setFieldName("id")));
通过指定PARAM_PARSE_STRATEGY_URL_PARAM表示从url中获取参数,setFieldName指定参数名称。

(6) 自定义API分组

/**
* 自定义API限流分组
* 1.定义分组
* 2.对小组配置限流规则
*/
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/product-service/product/**"). // 以/product-service/product/开头的所有url
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/order-service/order")); // 完全匹配/order-service/order的url
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}

 添加到限流规则中:

        rules.add(new GatewayFlowRule("product_api") // initCustomizedApis方法中定义的分组名称
                .setCount(1)
                .setIntervalSec(1));

 

posted @ 2021-07-29 14:15  1640808365  阅读(563)  评论(0编辑  收藏  举报