流量限制(限流)
一、限流介绍
流量限制(限流)解决的是在流量高峰期或者流量突增时服务的可用性问题。我们希望把流量限制在该服务所能接受的合理范围之内,不至于让服务被高流量击垮。
限流方法分类:
- 流量控制
- 流量整型
1、流量控制
流量控制可以分为:
- 单位时间调用量限流
- 并发调用量限流
1.1 单位时间调用量限流
单位时间调用量限流,控制的是一定时间内的请求数量的大小。
调用量限流常用的就是使用一个计数器统计单位时间内某个服务的访问量。一旦访问量超过了所设定的阈值,则该单位时间段内不允许服务继续响应请求,或者把接下来的请求放入队列中等待下一个单位时间段继续执行。
计数器需要在一个单位时间结束时对累积的请求量进行清零。
这个单位时间,也可以叫做时间窗口,其原理是将时间划分为固定大小的窗口,在每个窗口内限制请求的数量或速率。
比如说,限制某一个接口 1 分钟只能被访问 100 次,这个时间窗口就是 1 分钟。然后使用一个变量,来统计这 1 分钟内接口处理请求的数量,初始值为 0,表示接口当前的这一分钟内还没有处理请求。之后每处理一次请求,该变量加一,当变量值递增到 100 后,达到了限流阈值,则后续的请求将会被拒绝。等到当前 1 分钟结束之后,将统计变量重置为 0,重新开始新一分钟的计数。
上面描述的计数限流方法为:固定窗口计数器算法。时间窗口是固定的,实现方式比较简单。但是存在两个问题:
- 限流不平滑。限制 1 分钟接口访问阈值为 100 次,如果前 10 秒就访问了 100 次,那么后续的 50 秒将无法处理请求。
- 临界问题。限制 1 分钟接口访问阈值为 100 次,前 59 秒接口一次也没有访问,在最后的 1 秒时间内,突然接受到 100 次请求。在当前这一分钟结束之后,计数器清零。在接下来的 1 分钟,前 1 秒,又接收到 100 次请求。那么就是在 2 秒中的时间内,接收到了 200 次请求。超过了限流的阈值,而这突然的流量增长,可能会导致服务失败,无法应对突然激增的流量。
为了解决上述面临的问题,可以采用滑动窗口 (Sliding Window) 来进行限流。
在滑动窗口中,把单位时间设置为一个时间窗口,然后把时间窗口进行进一步的划分。比如说可以把 1 分钟这个单位时间划分成 60 个时间格,每格代表 1 秒。然后每过 1 秒,时间窗口就会往右滑动一格,每一个格子都有自己独立的计数器。再次使用上面的场景,限制 1 分钟接口访问阈值为 100 次,前 59 秒没有请求,当第 60 秒时突然接收到 100 次请求,当第 60 秒结束时,时间窗口向右(后)移动一格,此时该 1 分钟的时间窗口内已经有 100 次请求,达到了阈值。在下一分钟的第 1 秒,也就是第 61 秒时,又接收到 1 个请求,此时该 1 分钟的时间窗口内有 101 个请求,超过了请求阈值,拒绝访问。
从这个角度讲,可以认为固定窗口计数器算法实际上是滑动窗口算法的一种特例,只是它没有对时间窗口做进一步划分。由此可见,当滑动窗口的格子划分越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
疑问:比如 10 秒钟的时间窗口,请求阈值为 10,滑动窗口 1 秒移动一次,那么是否要限制,每次移动的这个格子(1 秒)钟的请求数量,比如
10/10 = 1
,限制每秒只能访问 1 次。如果不限制,那么只能解决临界问题以应对突发的流量激增,但是限流仍然不平滑。
很显然,当滑动窗口中的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。
1.2 并发调用量限流
并发调用量限流,需要限制方法被调用的并发数量,即同一时间并发数。在实现过程中,往往可以引入信号量 (Semaphore) 来完成这一目标。信号量也是一种计数器,用来保护一个或者多个共享资源的访问,它可以用来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理地使用资源。
2、流量整型
流量整型有两种方法,分别是:
- 漏桶算法
- 令牌桶算法
漏桶算法,是网络流量整形的常用算法之一。漏桶算法可以理解为是漏斗,液体倒进去以后,总是从下端的小口中以固定速率流出,也就是说不管突发流量有多大,漏桶都保证流量以恒定速率输出。
请求以不同的速率进入到漏桶中,通过漏桶算法进行整形之后只会以固定速率输出服务请求,而不管服务请求的变化有多么剧烈。另外,漏桶本身的容量肯定也不是无限大的,所以当桶中的请求数量超过了桶的容量,新来的请求也只能被丢弃掉了。
在很多应用场景中,除了要求能够限制请求的平均响应速率外,还要求可以应对突发流量。这时候漏桶算法可能就不合适了,而令牌桶算法更为适合。
令牌桶算法从某种程度上来说是漏桶算法的一种改进。令牌桶算法的原理是系统会以固定的速度往桶里放入令牌。如果请求需要被处理,则先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。而如果某一时间点的桶内存放着很多令牌,那么也就可以在这一时间点上响应很多的请求,因此服务请求的输入和输出都可以是变速的。
综上,令牌桶算法和漏桶算法的主要区别在于:漏桶算法能够强行限制数据的传输速率,而令牌桶算法能够在限制数据平均传输速率的同时还允许某种程度的突发传输。
二、问题总结
什么是固定窗口限流?
一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。
- 时间片段:把时间划分为连续的片段,每一个片段有固定的时间间隔,比如 1 秒。
- 时间窗口:需要统计调用量以判断是否达到请求阈值的时间段。
固定窗口中,时间窗口是固定的,不会随着时间的推移以时间片段为单位移动。当前时间窗口过去之后,计数器清零,之后统计下一个时间窗口的调用量。
什么是滑动窗口限流?
一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。
- 时间片段:把时间划分为连续的片段,每一个片段有固定的时间间隔,比如 1 秒。
- 时间窗口:需要统计调用量以判断是否达到请求阈值的时间段。
随着时间的推移,时间窗口会以时间判断为单位不断向后移动。
为了实现限流功能,需要定义一个计数器,统计滑动窗口内的请求数来判断是否达到了请求阈值。当有新请求时,检测窗口内计数是否已经达到阈值,如果没有则允许执行,如果达到阈值,则请求被拒绝或进入等待队列,或执行其他限流操作。
另外,每一个时间片段内也需要一个计数器来统计各个时间片段内的请求数。当窗口向右移动时,需要滑动窗口的计数器将刚才已经过去的时间片段中的请求数减掉。
滑动窗口限流相较于固定窗口限流有何优点?
没有临界问题,可以更灵活地应对突发流量。
什么是漏桶算法?
漏桶,可以理解为是漏斗,液体倒进去以后,总是从下端的小口中以固定速率流出。
也就是说,即使请求速率不固定,有突发流量,漏桶可以保证流量以恒定速率输出。
什么是令牌桶算法?
令牌桶,不是漏的,桶里用来盛放令牌,系统会以固定的速度往桶里放入令牌,有请求需要被处理,则从桶中获取一个令牌。当桶里没有令牌可取时,则拒绝服务。而如果某一时间点的桶内存放着很多令牌,那么也就可以在这一时间点上响应很多的请求,因此服务请求的输入和输出都可以是变速的。
令牌桶算法相较于漏桶算法有何优点?
可以应对突发流量。
三、Redisson 中的 RRateLimiter
1、接口源码
1 | public interface RRateLimiter extends RRateLimiterAsync, RExpirable { |
1.1 setRate 方法
setRate
方法用来设置限速器的速率限制。分两类:
trySetRate
,仅当之前没有设置过速率限制时,才会去设置。setRate
,设置速率限制,并清除限速器的状态(包括当前可用令牌数量字符串和令牌获取记录有序集合)
方法的参数有:
RateType mode
,限速模式,有两个选项,一个是集群所有RateLimiter
实例的总速率,一个是同一个RedissonClient
下所有RateLimiter
实例的总速率。long rate
,每次生成的令牌数量,也是令牌桶中存放令牌的最大值。Duration rateInterval
,生成令牌的时间间隔,也是时间窗口大小。Duration keepAliveTime
,限速器处于空闲状态时的存活时间
trySetRate
方法返回值为 boolean
类型,如果为 true 表示设定速率限制成功,false 表示设置失败。setRate
方法因为会覆盖设置所以没有返回值。
1.2 acquire 方法
acquire
方法用来获取令牌。分为两类:
acquire
,在未获取到令牌前会阻塞。tryAcquire
,如果没有可用的令牌,不会阻塞,立刻返回。
方法参数有:
long permits
,要获取令牌的数量Duration timeout
,阻塞等待获取令牌的最长时间。
tryAcquire
方法返回值为 boolean
类型,如果为 true 表示获取到了令牌,如果未获取到令牌则返回 false,acquire
没有返回值,未获取到则一直阻塞。
2、RedissonRateLimiter
RedissonRateLimiter
实现了 RRateLimiter
接口,RedissonRateLimiter
需要在 Redis 中存储三种数据:
- 哈希表,用来存放限速器的配置,redis 中的 key 为
限速器名
,配置包括:- 限速器的模式,集群控制模式还是单
RedissonClient
控制模式 - 每次生成的令牌数量
- 生成令牌的时间间隔
- 限速器处于空闲状态时的存活时间
- 限速器的模式,集群控制模式还是单
- 字符串,用来存放当前可用的令牌数量,redis 中的 key 为
限速器名:value
- 有序集合,用来存放令牌获取记录,集合元素中包含获取令牌的数量,分数为时间戳,redis 中的 key 为
限速器名:permits
2.1 设置限速器的 Lua 脚本
2.1.1 trySetRateAsync
方法中的 Lua 脚本
1 | redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]); |
上面的脚本中,初始化的限速器的配置,包括每次生成的令牌数量,生成令牌的时间间隔和限速器存活时间。并且设置了限速器类型为集群流控还是单 RedissonClient
流控。
1 | if res == 1 and tonumber(ARGV[4]) > 0 then |
这段代码表示,如果哈希表配置设置成功,并且设置的存活时间大于 0,那么使用 pexpire
命令为哈希表设置过期时间,值取自配置中的 keepAliveTime
,单位为毫秒。
2.1.2 setRateAsync
方法中的 Lua 脚本
1 | local valueName = KEYS[2]; |
该脚本中增加了在限速器模式为单 RedissonClient
流控时,对存放可用令牌数量的字符串和令牌获取记录有序集合的删除操作。
限速器模式为单 RedissonClient
流控时,在字符串和有序集合的 redis 键后面会拼接 ServiceManager
的 ID,例如:{test-client}:value:0ad23297-cdaf-498a-bc75-9286a2e4a008
、{test-client}:permits:0ad23297-cdaf-498a-bc75-9286a2e4a008
2.2 获取令牌的 Lua 脚本
tryAcquireAsync
方法用来获取令牌,其中封装了 Lua 脚本,如下:
1 | -- 获取限速器配置 |
脚本中有 5 个 key,分别是:
KEYS[1]
,哈希表的 keyKEYS[2]
,集群流控模式下字符串的 keyKEYS[3]
,单RedissonClient
流控模式下字符串的 keyKEYS[4]
,有序集合的 keyKEYS[5]
,单RedissonClient
流控模式下有序集合的 key
有 3 个参数,分别是:
ARGV[1]
,获取令牌的数量ARGV[2]
,系统当前时间ARGV[3]
,ServiceManager
的随机 ID
2.2.1 脚本返回值
Lua 脚本声明了返回值 local res;
,该返回值代表的时此次获取操作需要等待的时间,
在 Lua 脚本中,返回值赋值代码为:
1 | res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2])); |
这里计算出来的是下一个令牌的释放时间,因为当前已经没有足够的可用令牌了,所以需要等待令牌释放后再次获取。
在 private CompletableFuture<Boolean> tryAcquireAsync(long permits, long timeoutInMillis)
方法中,第一次调用 Lua 脚本获取令牌获取失败,返回了要等待的时间,经过了指定时间之后,该方法内部会再次调用 Lua 脚本获取令牌:
1 | RFuture<Long> future = tryAcquireAsync(RedisCommands.EVAL_LONG, permits); |
2.2.2 疑问
上面脚本中有一个地方需要注意:
1 | if released > 0 then |
这里的 tonumber (currentValue) + released > tonumber (rate)
的分支特别疑惑,不理解为什么会存在可用令牌数量 + 待释放令牌数量大于令牌桶令牌上限的情况,感觉两者之和最多应为等于关系。
查看代码提交记录,发现是 Issue 的修复,这块的修复逻辑请参考:
- Redisson Rate Limiter -> Hard Limit #3639 | Issues | github.com/redisson
- 剩余可用令牌数计算错误 #5751 | Issues | github.com/redisson
2.3 获取当前可用令牌数量 Lua 脚本
1 | local rate = redis.call('hget', KEYS[1], 'rate'); |
脚本中有 5 个 key,分别是:
KEYS[1]
,哈希表的 keyKEYS[2]
,集群流控模式下字符串的 keyKEYS[3]
,单RedissonClient
流控模式下字符串的 keyKEYS[4]
,有序集合的 keyKEYS[5]
,单RedissonClient
流控模式下有序集合的 key
有 1 个参数:
ARGV[1]
,系统当前时间
相关链接
剩余可用令牌数计算错误 #5751 | Issues | github.com/redisson
Redisson Rate Limiter -> Hard Limit #3639 | Issues | github.com/redisson
OB links
OB tags
#分布式 #微服务 #并发