项目限流方案小结

2022-06-08

限流

限流器是什么?

当单位时间内,产生并发访问时,在诸如抢购场景下,只有10件库存。而用户如果都进行争夺则会产生所谓的超卖问题。这时候用限流器进行限制,则可以很好的解决。使得单位时间内只有指定线程数的客户可以访问,未访问到的客户则进行其他处理。

(redisson提供了封装好的限流器,我们使用其中的RRateLimiter限流器)

  1. 合法性验证限流:比如验证码、IP 黑名单等,这些手段可以有效的防止恶意攻击和爬虫采集;
  2. 容器限流:比如 Tomcat、Nginx 等限流手段,其中 Tomcat 可以设置最大线程数(maxThreads),当并发超过最大线程数会排队等待执行;而 Nginx 提供了两种限流手段:一是控制速率,二是控制并发连接数;
  3. 服务端限流:比如我们在服务器端通过限流算法实现限流

Nginx限流- 控制速率

我们需要使用 limit_req_zone 用来限制单位时间内的请求数,即速率限制,示例配置如下:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server { 
    location / { 
        limit_req zone=mylimit;
    }
}

以上配置表示,限制每个 IP 访问的速度为 2r/s,因为 Nginx 的限流统计是基于毫秒的,我们设置的速度是 2r/s,转换一下就是 500ms 内单个 IP 只允许通过 1 个请求,从 501ms 开始才允许通过第 2 个请求。

速率限制升级版

上面的速率控制虽然很精准但是应用于真实环境未免太苛刻了,真实情况下我们应该控制一个 IP 单位总时间内的总访问次数,而不是像上面那么精确但毫秒,我们可以使用 burst 关键字开启此设置,示例配置如下:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server { 
    location / { 
        limit_req zone=mylimit burst=4;
    }
}

burst=4 表示每个 IP 最多允许4个突发请求

Nginx限流- 控制并发数

利用 limit_conn_zone 和 limit_conn 两个指令即可控制并发数,示例配置如下:

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
    ...
    limit_conn perip 10;
    limit_conn perserver 100;
}

其中 limit_conn perip 10 表示限制单个 IP 同时最多能持有 10 个连接;limit_conn perserver 100 表示 server 同时能处理并发连接的总数为 100 个。

小贴士:只有当 request header 被后端处理后,这个连接才进行计数。

服务端限流

限流的常见算法有以下三种:

  1. 时间窗口算法
  2. 漏桶算法
  3. 令牌算法

1.时间窗口算法

所谓的滑动时间算法指的是以当前时间为截止时间,往前取一定的时间,比如往前取 60s 的时间,在这 60s 之内运行最大的访问数为 100,此时算法的执行逻辑为,先清除 60s 之前的所有请求记录,再计算当前集合内请求数量是否大于设定的最大请求数 100,如果大于则执行限流拒绝策略,否则插入本次请求记录并返回可以正常执行的标识给客户端。

我们可以借助 Redis 的有序集合 ZSet 来实现时间窗口算法限流,实现的过程是先使用 ZSet 的 key 存储限流的 ID,score 用来存储请求的时间,每次有请求访问来了之后,先清空之前时间窗口的访问量,统计现在时间窗口的个数和最大允许访问量对比,如果大于等于最大访问量则返回 false 执行限流操作,负责允许执行业务逻辑,并且在 ZSet 中添加一条有效的访问记录。

此实现方式存在的缺点有两个:

  • 使用 ZSet 存储有每次的访问记录,如果数据量比较大时会占用大量的空间,比如 60s 允许 100W 访问时;
  • 此代码的执行非原子操作,先判断后增加,中间空隙可穿插其他业务逻辑的执行,最终导致结果不准确。

2.漏桶算法

滑动时间算法有一个问题就是在一定范围内,比如 60s 内只能有 10 个请求,当第一秒时就到达了 10 个请求,那么剩下的 59s 只能把所有的请求都给拒绝掉,而漏桶算法可以解决这个问题。

漏桶算法类似于生活中的漏斗,无论上面的水流倒入漏斗有多大,也就是无论请求有多少,它都是以均匀的速度慢慢流出的。当上面的水流速度大于下面的流出速度时,漏斗会慢慢变满,当漏斗满了之后就会丢弃新来的请求;当上面的水流速度小于下面流出的速度的话,漏斗永远不会被装满,并且可以一直流出。

漏洞算法的实现步骤是,先声明一个队列用来保存请求,这个队列相当于漏斗,当队列容量满了之后就放弃新来的请求,然后重新声明一个线程定期从任务队列中获取一个或多个任务进行执行,这样就实现了漏桶算法。

Nginx 的控制速率其实使用的就是漏桶算法

3.令牌算法

在令牌桶算法中有一个程序以某种恒定的速度生成令牌,并存入令牌桶中,而每个请求需要先获取令牌才能执行,如果没有获取到令牌的请求可以选择等待或者放弃执行。