Introduction
在这篇博客想和大家分享一下接口限流算法。接口限流算法是软件工程领域中一块很重要的组成部分,它在实际的应用场景有非常巨大的作用。在这里我就和大家分享一下几种不同的限流算法以及它们的优缺点。
接口限流算法大多数情况下应用于服务端的接口,它的主要作用有两个:
- 限制流量保护服务器资源:在后端服务中,许多资源的访问都是有限制的,最常见的是数据库。在流量高峰期我们需要保护数据库不被打挂,所以就需要对访问数据库的接口进行限流;
- 安全:对于密码破解,最常见的就是暴力破解,为了防止暴力破解,我们需要对相关接口进行限流,以保护敏感信息不会被暴力破解。
Algorithm
接下来我将简单介绍三个常见的限流算法,以及它们的特点。
Time Bucket
时间桶限流,也叫计数器限流算法,其实现原理很简单,使用redis设置一个计数值,每来一个请求就去查一下这个计数值是否为0,如果不为零则说明还有quota,允许请求;否则拒绝请求。
假设我们允许1分钟3000个请求,这个redis key的expire time就是1分钟,然后计数值就是3000。第一个请求到来,检查计数值大于0,允许执行,这个值减1,变成2999。假设第30秒的时候,服务器已经处理了3000个请求,后面又来了一个,检查计数值为0,拒绝请求,这样就起到了限流的作用。等到1分钟到了,这个计数值又重新变成了3000,又可以处理新来的请求。
但是这个算法有一个很致命的缺点,它无法解决瞬间的恶意攻击。假设有人在第一个1分钟的第59秒发起3000个请求,等到1分钟到了的时候,quota重置为3000,然后再发起3000个请求。在这种情况下,虽然有限流算法在,但实际上在两秒内,服务器接受了6000个请求,这是非常有可能把有限资源打挂的。
Leaky Bucket
为了更好地解决上述的问题,有一个更加smooth的请求曲线,漏桶算法应运而生。如果我们把请求比作是水,水来了都放到一个桶里,这个桶以限定的速度出水,当水在短时间内来得过多时,水就会溢出,这个时候就意味着请求被拒绝。
从图中可以看出,漏桶算法的流出速度是恒定的,所以它能够平滑请求的突发流量,实现流量整形,为服务提供一个稳定的流出流量。但这种设计也有其不合理的地方,真实的场景下往往会有意想不到的突发高峰,而漏桶算法的流出流量是恒定的,对于突发流量缺乏调整效率。
GCRA
为了容忍请求流量的尖峰问题,克服漏桶算法的缺点,有人提出了GCRA算法。GCRA(Generic Cell Rate Algorithm)一开始是由ATM(Asynchronous Transfer Mode)协会推荐使用的用于解决网络调度的限流算法,目前这个算法广泛应用于服务接口限流的场景。
GCRA算法的本质是一个令牌桶算法。在介绍GCRA前,简单介绍下令牌桶算法。令牌桶算法是和漏桶算法相对应的一个限流算法,系统以一个恒定的速度生产令牌放到桶里,每当处理一个请求时,需要从桶中获取令牌,才能被系统处理。这也是为什么令牌桶算法能够克服漏桶算法的原因。对于桶而言,漏桶算法是以恒定的速度流出,而令牌桶算法是以恒定的速度流入。就是这个差别使得令牌桶算法能够容忍短时间的请求尖峰。
比如我们期望的QPS是1000,桶的容量是5000。先看漏桶算法,假设第1s来了1600个请求,第2s来了400个请求。对于服务器来说,第1s处理了1000个请求,多出的600个请求需要在第2s处理;再看令牌桶算法,因为桶的容量是5000,第1s的时候1600个请求都拿到了令牌,所以第1s就能处理1600个请求,这个就是对突发流量的兼容。
所以对于令牌桶算法来说,最关键的地方在于如何去计算有多少quota(桶中的令牌数)。可能很常规的一种思路是维护一个计数器来记录quota,但是这里会有大量的锁的获取和释放操作(系统生成token需要增加,请求来了需要减少),开销很大。同时一个好的限流算法还应该能够支持灵活的调整限制,比如说临时调整QPS限制等,及时生效也是需要考虑的一个问题。
GCRA算法实际上只存储一个值叫TAT(Theoretical Arrival Time),这个是一个时间戳,它翻译成中文是理论到达时间,表示的是理论上令牌桶恢复满的时间。先来看这个值是怎么计算的,假设消耗的令牌数为n
,生成一个令牌的时间间隔是T
,当前时间为X
,那么此时这个TAT的计算就是TAT = X + n * T
。例如时间间隔是1s,消耗是30,那么在X
时刻计算的TAT = X + 30
。这个TAT
表示的是在30秒后,桶能够恢复成填满状态。
上面计算TAT其实就是请求到来的时候,计算自己需要多少个quota。那么这个TAT的限制在哪呢?我们只需要把计算公式中消耗的令牌数改成桶的容量t
即可。假设其余条件和上面一样,桶的容量是100,那么TAT的上限就是X + 100
。所以在某个时刻,瞬间拿到的令牌数不能超过100,这里就起到了限流的效果。
弄懂了这个TAT是怎么计算之后,其余的就非常简单了,整个流程我们走一遍:
- 在
t0
时刻,有10个请求,计算TAT = t0 + 10 * 1 = t0 + 10
,上限是t0 + 100 * 1 = t0 + 100
,TAT小于上限,所以请求被允许,更新TAT的值(存放到redis); - 在
t1
时刻,有30个请求,先从redis中拿出TAT,是t0 + 10
,计算新的TAT,TAT = t0 + 10 + 30 * 1 = t0 + 40
,上限是t1 + 100 * 1 = t0 + 101
,TAT还是小于上限,所以请求还是被允许,更新TAT的值(存放到redis); - 在
t3 = t0 + 3
时刻,有80个请求,先从redis中拿出TAT,是t0 + 40
,计算新的TAT,TAT = t0 + 40 + 80 * 1 = t0 + 120
,上限是t3 + 100 * 1 = t0 + 103
,此时TAT大于上限,所以请求被拒绝,这个时候TAT的值就不需要更新了。对于封装比较好的pkg来说,请求被拒绝后还会返回RetryAfter
和ResetAfter
这两个值,让调用方有更多信息。RetryAfter的值为上限减去TAT,即t0 + 120 - t0 - 103 = 17
,这个表明17秒后再请求就可以通过了。ResetAfter直接设置为上限。
经过上面的解释,应该大致了解了GCRA算法的工作原理,整个过程只会存储时间戳到Redis中,并没有其他额外的信息。上面我们还提到动态的调整,因为存放的是时间戳,而且每次计算时,计算上限用的都是当前时间戳,所以调整完之后,有请求来的时候这个上限的计算也是动态调整的。
Summary
限流算法在实际场景应用非常广泛,是一种保护服务可用性的必要手段。虽然从使用方的角度看接口很简单,但是不同算法的特点、使用场景以及实现原理都很不一样,像GCRA这种算法实现还非常巧妙。看起来很简单的限流算法其实还是有很多值得分析和研究的地方。谢谢你的支持!