【Hoxton.SR1版本】Spring Cloud Gateway之如何进行限流

目录

一、简介

二、常见的限流算法

三、 Spring Cloud Gateway限流

四、总结


一、简介

前面我们已经了解到Spring Cloud Gateway新一代网关主要有:路由转发、权限鉴定、统一日志处理、服务限流、熔断、分流等功能,今天我们来聊聊服务限流。

  • 为什么要限流?

想必大家都经历过双十一淘宝的抢购商品吧,可想而知双十一当天淘宝的并发流量有多大,那么淘宝是怎么扛住这么大的并发呢,想必也肯定使用到了限流策略。在高并发分布式系统中,往往都需要进行限流操作,原因有两点:

  1. 为了防止大量的请求致使服务器过载,导致服务器不可用;
  2. 为了防止网络攻击网站;

常见的限流方式,比如Hystrix限流:使用线程池进行隔离,超过线程池的负载,将会触发服务熔断的逻辑。在一般应用服务器中,比如Tomcat也是通过限制最大线程数来控制并发的;通过时间窗口的平均速度来控制流量;

一般开发高并发系统常见的限流有:

  1. 限制总并发数(比如数据库连接池、线程池)、
  2. 限制瞬时并发数(如 nginx 的 limit_conn 模块,用来限制瞬时并发连接数)、
  3. 限制时间窗口内的平均速率(如 Guava 的 RateLimiter、nginx 的 limit_req 模块,限制每秒的平均速率);
  4. 限制远程接口调用速率、限制 MQ 的消费速率。
  5. 还可以根据网络连接数、网络流量、CPU 或内存负载等来限流。

常见的限流维度有:

  • IP地址限流;
  • 针对用户限流;
  • 针对接口(URI)限流;

在微服务分布式系统中,限流通常放在网关这一层做,比如Nginx、zuul、Spring Cloud Gateway等;当然也可以在应用层通过Spring Aop切面编程方式做限流。

二、常见的限流算法

(一)、计数器算法

  • 算法原理

此算法采用计数器实现限流,一般我们会限制一秒钟的能够通过的请求数,比如限流QPS为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。

  • 算法实现思路

对于每次服务调用,可以通过AtomicLong原子操作类中的incrementAndGet()方法【即++i的原子操作方法】来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。

  • 算法示意图

  • 弊端

如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能把后面的所有请求都拒绝,我们把这种现象称为 "突刺现象"。

从上图中我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在这1秒里面,瞬间发送了200个请求。我们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求,可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。【为了消除"突刺现象",可以采用下面的漏桶算法来改进】 。

 

(二)、漏桶算法

  • 算法实现原理

漏桶算法其实很简单,可以粗略的认为就是注水漏水过程,往桶中以一定速率流出水,以任意速率流入水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理请求的速度是固定的,请求进来的速度是不固定的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。

  • 算法实现思路

准备一个队列,用来存放用户发卡的请求【保存请求的速度随机】,然后通过线程池(ScheduledExecutorService)来定期从队列中获取请求并执行【固定速度去获取请求来执行】,可以一次性获取多个并发执行。

  • 算法示意图

  • 弊端

无法应对短时间的突发流量,可能导致瞬间击垮应用程序。 

 

(三)、令牌桶算法

  • 算法实现原理

1)、所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)、根据限流大小,设置按照一定的速率往桶里添加令牌;
3)、桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)、请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)、令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流;

  • 算法实现思路

准备一个队列,用来保存Token令牌,然后通过一个线程池定期生成Token令牌放到队列中,每来一个请求,就从队列中获取一个令牌供此次请求使用,并继续执行。

  • 算法示意图

 

三、 Spring Cloud Gateway限流

Gateway官方提供了RequestRateLimiterGatewayFilterFactory这个类,使用Redis和lua脚本实现了令牌桶的方式,使用的算法是令牌桶算法。

RequestRateLimiter官方文档地址:https://docs.spring.io/spring-cloud-gateway/docs/2.2.4.RELEASE/reference/html/#the-requestratelimiter-gatewayfilter-factory

RequestRateLimiter GatewayFilter工厂使用一个RateLimiter实现来决定是否允许当前请求继续进行。如果不是,则返回HTTP 429的状态------太多请求(默认情况下)。此过滤器接受一个可选的键解析器参数和特定于速率限制器的参数

keyResolver是实现keyResolver接口的bean。在配置中,使用SpEL按名称引用bean。#{@myKeyResolver}是一个SpEL表达式,它引用一个名为myKeyResolver的bean。下面的清单显示了KeyResolver接口:

public interface KeyResolver {
    Mono<String> resolve(ServerWebExchange exchange);
}
  • redis-rate-limiter.“replenishRate”属性:指您希望用户在没有任何丢弃请求的情况下每秒允许执行的请求数量,令牌桶被填充的速率。
  • redis-rate-limiter.burstCapacity属性:用户在一秒内允许执行的最大请求数,令牌桶可以容纳的令牌的数量。将此值设置为零将阻止所有请求。
  • redis-rate-limiter.requestedTokens属性:一个请求需要多少个令牌,这是每个请求从桶中取出的令牌的数量,默认值为1。
  • key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。

下面通过案例来讲解如何在Spring Cloud Gateway中使用内置的限流过滤器工厂来实现限流。

【a】pom.xml中添加如下依赖

目前RequestRateLimiterGatewayFilterFactory的实现依赖于 Redis,所以我们需要引入spring-boot-starter-data-redis-reactive

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

【b】payment8001新增如下方法

/**
     * 测试RequestRateLimiter限流
     */
    @GetMapping("/requestRateLimiter/{name}")
    public String requestRateLimiter(@PathVariable("name") String name) {
        return "hello, [requestRateLimiter] the name is :" + name + ", the server port is " + serverPort;
    }

【c】application.yml中加入如下配置

server:
  port: 9528
spring:
  application:
    name: springcloud-gateway
  cloud:
    gateway:
      routes:
        ########################################【AddRequestHeader GatewayFilter添加请求头】######################################################
        - id: payment_service8001_gatewayAddRequestHeader  #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/gatewayAddRequestHeader/**
          filters:
            #将X-Request-red:blue头添加到所有匹配请求的下游请求头中
            - AddRequestHeader=X-Request-red, blue

        ########################################【AddRequestParameter GatewayFilter添加请求参数】######################################################
        - id: payment_service8001_gatewayAddRequestParameter #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/gatewayAddRequestParameter
          filters:  #过滤器(filters:过滤器,过滤规则)
            # 添加指定参数
            - AddRequestParameter=name, weishihuai

        ########################################【AddResponseHeader GatewayFilter添加响应头】######################################################
        - id: payment_service8001_gatewayAddResponseHeader #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/gatewayAddResponseHeader
          filters:  #过滤器(filters:过滤器,过滤规则)
            # 添加指定参数
            - AddResponseHeader=X-Response-Red, Blue

        ########################################【Hystrix GatewayFilter断路器】######################################################
        - id: payment_service8001_gatewayHystrixGatewayFilter #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/gatewayHystrixGatewayFilter
          filters:  #过滤器(filters:过滤器,过滤规则)
            - name: Hystrix
              args:
                name: fallbackcmd
                fallbackUri: forward:/fallback

        - id: payment_service8001_outerFallback-gatewayHystrix #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/gatewayOuterFallbackHystrixGatewayFilter
          filters:  #过滤器(filters:过滤器,过滤规则)
            - name: Hystrix
              args:
                name: fallbackcmd
                fallbackUri: forward:/gatewayFallback  #此处需要与下面- Path=/gatewayFallback对应
        - id: outer-gateway-fallback
          uri: http://localhost:8001
          predicates:
            #fallback时调用的方法 http://localhost:8001/gatewayFallback
            - Path=/gatewayFallback

        ########################################【PrefixPath  GatewayFilter路径前缀过滤器工厂】######################################################
        - id: payment_service8001_prefixPathGatewayFilter #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/prefixPathGatewayFilter
          filters:  #过滤器(filters:过滤器,过滤规则)
            #将把/api作为所有匹配请求的路径的前缀。因此,对/prefixPathGatewayFilter的请求将被发送到/api/prefixPathGatewayFilter
            - PrefixPath=/api

        ########################################【StripPrefix GatewayFilter Factory路径截取过滤器工厂】######################################################
        #如下配置表示,在访问localhost:9528/api/gatewayStripPrefix/**请求的,gateway网关将/api截取掉,请求分发到http://localhost:8001中去.
        - id: payment_service8001_gatewayStripPrefix #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            # 转发地址格式为 uri/gatewayStripPrefix, /api 部分会被下面的过滤器给截取掉.
            - Path=/api/gatewayStripPrefix/**
          filters:  #过滤器(filters:过滤器,过滤规则)
            # 截取路径位数  即截取/api
            - StripPrefix=1

        ########################################【RewritePath GatewayFilter Factory路径截取过滤器工厂】######################################################
        #如下配置表示,在访问localhost:9528/api/rewritePathGatewayFilter/**请求的,gateway网关将路径重写为localhost:9528/rewritePathGatewayFilter,然后请求分发到http://localhost:8001中去.
        - id: payment_service8001_rewritePathGatewayFilter #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/api/rewritePathGatewayFilter
          filters:  #过滤器(filters:过滤器,过滤规则)
            - RewritePath=/api(?<segment>/?.*), $\{segment}

        ##########################################【测试自定义权限过滤器】#################################################
        - id: payment_service8001_customAuthFilter #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/customAuthFilter/**
          filters:  #过滤器(filters:过滤器,过滤规则)
            - CustomAuth

        ##########################################【自定义过滤器工厂】#################################################
        - id: payment_service8001_customGatewayFilterFactory #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/customGatewayFilterFactory/**
          filters:  #过滤器(filters:过滤器,过滤规则)
            - CustomRequestTime=true

        ##########################################【测试自定义全局过滤器】#################################################
        - id: payment_service8001_customGatewayGlobalFilter #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/customGatewayGlobalFilter/**

        ##########################################【测试RequestRateLimiter限流】#################################################
        - id: payment_service8001_requestRateLimiter #路由ID
          uri: http://localhost:8001  #指定payment8001的访问地址,即匹配后提供服务的路由地址
          predicates:
            - Path=/requestRateLimiter/**
          filters:
            - name: RequestRateLimiter
              args:
                #配置RequestRateLimiter限流过滤器涉及的参数
                redis-rate-limiter.replenishRate: 1  #令牌桶每秒填充平均速率
                redis-rate-limiter.burstCapacity: 50  #令牌桶总容量
                redis-rate-limiter.requestedTokens: 1  #每个请求从桶中取出的令牌的数量
                key-resolver: '#{@hostNameAddressKeyResolver}' #用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象
#                key-resolver: '#{@userKeyResolver}' #用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象
#                key-resolver: '#{@uriKeyResolver}' #用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象
      discovery:
        locator:
          enabled: true   #开启从注册中心动态创建路由的功能,利用微服务名进行路由
  redis:
    #配置redis的信息
    host: localhost
    port: 6379
    database: 0
eureka:
  instance:
    hostname: springcloud-gateway-service
  client:
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://springcloud-eureka7001.com:7001/eureka/,http://springcloud-eureka7002.com:7002/eureka/   #集群版Eureka注册中心


【d】限流配置 - 根据IP地址限流

如果需要使用IP地址进行限流,则需要将请求发起的IP地址作为限流Key,如下配置:

package com.wsh.springcloud.keyResolver;

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

/**
 * @Description 自定义主机名称 KeyResolver
 * @Date 2020/8/23 21:00
 * @Author weishihuai
 * 说明: 获取请求用户ip作为限流key
 */
@Component
public class HostNameAddressKeyResolver implements KeyResolver {
    private static final Logger logger = LoggerFactory.getLogger(HostNameAddressKeyResolver.class);

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        String hostAddress = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
        logger.info("IP address: {}", hostAddress);
        //将用户IP地址作为限流Key
        return Mono.just(hostAddress);
    }
}

【e】限流配置 - 根据用户限流

如果你想根据用户来限流,则需要获取当前请求的用户 ID 或者用户名作为限流Key,如下配置:

package com.wsh.springcloud.keyResolver;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @Description 自定义用户 KeyResolver
 * @Date 2020/8/23 21:02
 * @Author weishihuai
 * 说明: 获取请求用户id作为限流key
 */
@Component
@Primary
public class UserKeyResolver implements KeyResolver {
    private static final Logger logger = LoggerFactory.getLogger(UserKeyResolver.class);

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        //获取请求参数中的userId作为限流Key
        String userId = exchange.getRequest().getQueryParams().getFirst("userId");
        logger.info("userKey: {}", userId);
        return Mono.just(userId);
    }
}

注意:请求参数中需要包含指定的限流Key, 否则会报错。

 

【f】限流配置 - 根据接口限流

如果需要根据接口的 URI 进行限流,则需要获取请求地址的 uri 作为限流 key,如下配置:

package com.wsh.springcloud.keyResolver;

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

/**
 * @Description 自定义URI KeyResolver
 * @Date 2020/8/23 21:01
 * @Author weishihuai
 * 说明: 获取请求地址的uri作为限流key
 */
@Component
public class UriKeyResolver implements KeyResolver {
    private static final Logger logger = LoggerFactory.getLogger(UriKeyResolver.class);

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        String path = exchange.getRequest().getURI().getPath();
        logger.info("interface url : {}", path);
        //将接口URL作为限流Key
        return Mono.just(path);
    }
}

【g】测试

启动项目,这里我们用 postman进行测试.

 

 

 

然后通过redis-cli客户端,通过keys *查看当前所有的键信息:

 

可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key,大括号中就是我们的限流Key。

  • timestamp:存储的是当前时间的秒数,也就是System.currentTimeMillis() / 1000或者Instant.now().getEpochSecond();
  • tokens:存储的是当前这秒钟的对应的可用的令牌数量;

注意点:

由于上述我们配置了三个KeyResolver,gateway启动的时候必须指定哪一个KeyResolver为主,否则将会报如下图错误:

Parameter 1 of method requestRateLimiterGatewayFilterFactory in org.springframework.cloud.gateway.config.GatewayAutoConfiguration required a single bean, but 3 were found:
	- hostNameAddressKeyResolver: defined in file [C:\Users\admin\Desktop\springcloud_Hoxton\springcloud-apigateway-gateway9528\target\classes\com\wsh\springcloud\keyResolver\HostNameAddressKeyResolver.class]
	- uriKeyResolver: defined in file [C:\Users\admin\Desktop\springcloud_Hoxton\springcloud-apigateway-gateway9528\target\classes\com\wsh\springcloud\keyResolver\UriKeyResolver.class]
	- userKeyResolver: defined in file [C:\Users\admin\Desktop\springcloud_Hoxton\springcloud-apigateway-gateway9528\target\classes\com\wsh\springcloud\keyResolver\UserKeyResolver.class]


Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

解决方法:标识其中一个KeyResolver为主要的限流Key,在类声明处加上: @Primary注解即可。

四、总结

本篇文章主要总结常见的限流算法,并且结合示例说明了如何在Spring Cloud Gateway网关中利用内置的过滤器实现限流。

Spring Cloud Gateway目前提供的限流还是相对比较简单的,在实际中我们的限流策略会有很多种情况,比如:

  • 每个接口的限流数量不同,可以通过配置中心动态调整;
  • 超过的流量被拒绝后可以返回固定的格式给调用方;
  • 对某个服务进行整体限流,可以将服务名称作为限流Key即可;
  • 当然还可以自定义RedisRateLimiter来实现自己的限流策略;

参考资料:

https://docs.spring.io/spring-cloud-gateway/docs/2.2.4.RELEASE/reference/html/#the-requestratelimiter-gatewayfilter-factory

https://www.cnblogs.com/linjiqin/p/9707713.html

https://blog.csdn.net/qq_35869079/article/details/88243754

https://www.jianshu.com/p/d60eefc3507a

https://blog.csdn.net/u010889990/article/details/81169328

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值