顾名思义,限流就是限制流量,就像你带宽包有1个G的流量,用完了就没有了。通过限流,我们可以很好地控制系统的QPS,
从而达到保护系统的目的。

为什么需要限流

比如Web服务、对外API,这种类型的服务有以下几种可能导致机器被拖垮:

  • 用戶增长过快(好事)
  • 因为某个热点事件(微博热搜)
  • 竞争对象爬虫
  • 恶意的请求

这些情况都是无法预知的,不知道什么时候会10倍甚至20倍的流量打过来,如果真碰上这种情况,扩容是根本来不及的。
image.png

限流算法

常见的限流算法有:

  • 计数器算法
  • 漏铜(Leaky Bucket)算法
  • 令牌桶(Token Bucket)算法

    计数器算法

    计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。
    此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现。
    网关限流 - 图2
    这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题,如下图:
    网关限流 - 图3
    假设1min内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。

    滑动窗口算法

    滑动窗口算法是将时间周期分为N个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期。

如下图,假设时间周期为1min,将1min再分为2个小周期,统计每个小周期的访问数量,则可以看到第一个时间周期内,访问数量为75,第二个时间周期内,访问数量为100,超过100的访问则被限流掉了
网关限流 - 图4
由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。此算法可以很好的解决固定窗口算法的临界问题。

漏铜算法

漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。

网关限流 - 图5

令牌桶算法

令牌桶算法是程序以r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求达到时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略
网关限流 - 图6

GateWay限流

Spring Cloud GateWay 官方提供了RequestRateLimiterGateWayFilterFactory过滤器工厂,使用Redis和Lua脚本实现了令牌桶的方式
具体实现逻辑在RequestRateLimiterGateWayFilterFactory类中,lua脚本在如下图所示的文件夹中:
网关限流 - 图7
先以案例的形式来讲解如何在Spring Cloud Gateway中使用内置的限流过滤器工厂来实现限流。

首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-gateway</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifatId>spring-boot-starter-data-redis-reactive</artifactId>
  8. </dependency>

在配置文件中做以下的配置:

  1. server:
  2. port: 8081
  3. spring:
  4. cloud:
  5. gateway:
  6. routes:
  7. - id: limit_route
  8. uri: http://httpbin.org:80/get
  9. predicates:
  10. - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  11. filters:
  12. - name: RequestRateLimiter
  13. args:
  14. key-resolver: '#{@hostAddrKeyResolver}'
  15. redis-rate-limiter.replenishRate: 1
  16. redis-rate-limiter.burstCapacity: 3
  17. application:
  18. name: gateway-limiter
  19. redis:
  20. host: localhost
  21. port: 6379
  22. database: 0

在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:

  • burstCapacity,令牌桶总容量。
  • replenishRate,令牌桶每秒填充平均速率。
  • key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。

KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。

  1. public class HostAddrKeyResolver implements KeyResolver {
  2. @Override
  3. public Mono<String> resolve(ServerWebExchange exchange) {
  4. return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
  5. }
  6. }
  7. @Bean
  8. public HostAddrKeyResolver hostAddrKeyResolver() {
  9. return new HostAddrKeyResolver();
  10. }

可以根据uri去限流,这时KeyResolver代码如下:

  1. public class UriKeyResolver implements KeyResolver {
  2. @Override
  3. public Mono<String> resolve(ServerWebExchange exchange) {
  4. return Mono.just(exchange.getRequest().getURI().getPath());
  5. }
  6. }
  7. @Bean
  8. public UriKeyResolver uriKeyResolver() {
  9. return new UriKeyResolver();
  10. }

也可以以用户的维度去限流:

  1. @Bean
  2. KeyResolver userKeyResolver() {
  3. return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
  4. }

用jmeter进行压测,配置10thread去循环请求lcoalhost:8081,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过redis客户端去查看redis中存在的key。如下:
网关限流 - 图8
可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义可以看lua源代码。

用sentinel给gateway限流
https://cloud.tencent.com/developer/article/1808112