前言
后端的api
接口一般都需要限制访问频率,一般的实现算法有令牌桶,漏桶等等。其中令牌桶支持突发流量,更合适访问流量整形。
关于令牌桶算法这里不再赘述,目前有两个golang
限流器库,github.com/juju/ratelimit和golang.org/x/time/rate,采用了延迟计算的方式实现令牌桶算法。本文主要是基于golang.org/x/time/rate的限流器进行实现,有两篇相关的文章《Golang限流器time/rate使用介绍》和《Golang限流器time/rate实现剖析》,有兴趣的同学可以查看一下。
限流响应头
限流响应头主要涉及四个字段:
- 未超出频率限制时:
X-RateLimit-Limit
:请求限制总量,对应了令牌桶算法中的突发值Burst
X-RateLimit-Remaining
:目前还可以请求的次数X-RateLimit-Reset
:多少秒才能恢复到满桶的状态
- 超出频率限制时:
Retry-After
:多少秒之后可以重试
golang.org/x/time/rate的Reserve方法返回一个*Reservation
对象,根据它的Delay方法,我们可以知道本次请求是否超出了频率限制:
delay
大于0,表示需要等待,即超出了频率限制。此时,我们根据delay
转换成秒数(至少一秒)即可。delay
等于0,表示不需要等待,即未超出频率限制。此时,我们除了Burst
,无法获取更多的有效信息。
为Reservation
增加信息
因为Reservation
缺少可以转化为X-RateLimit-Remaining
和X-RateLimit-Reset
的信息,我们需要在调用Reserve
时,保存一些相关数据(源码位置):
|
|
我们先看r.remainedTokens = int(math.Floor(tokens + 1e-9))
,它保存了调用Reserve
时,limiter
剩余的整数token
值。因为tokens
是float64
类型,会丢失一点精度,我们需要补全精度后,转为int
类型。
再看r.reset = r.limit.durationFromTokens(float64(r.lim.burst) - tokens)
,float64(r.lim.burst) - tokens
是已经消耗掉的token
数量,我们根据这个数量,转换为时长,即恢复这么多token
还需要多长时间。
实现fiber
频率限制中间件
Config
根据fiber中间件的实现规则,我们先创建一个配置结构:
|
|
利用这些配置,用户可以根据自己的需求使用中间件。
New
func New(config ...Config) func(*fiber.Ctx)
是中间件的工厂函数,根据用户传入的配置,返回一个fiber
中间件。
我们需要缓存下所有的限制器对象,存放在limiters
变量中,并用mu
控制并发访问:
|
|
当api
接口收到请求时,获取限制器的key
,再根据key
获取限制器,没找到的话根据配置为key
新建一个限制器:
|
|
然后,我们调用lim.Reserve
获取一个*Reservation
对象,根据其Delay()
返回值确定有没有超出频率限制:
|
|
其中X-RateLimit-Reset
和Retry-After
的值都转成秒为单位的整数,这是一个相对当前调用时间的值,客户端可以根据这个值作出相应的操作。
Set
最后,我们提供func Set(key string, lim *rate.Limiter)
方法,用户可以根据自己的需求提前设置不同的限制器。比如针对不同的api
用户,设置不同的访问频率以及突发值。
总结
本文主要针对api
接口的频率限制需求,在golang.org/x/time/rate的基础上为Reservation
对象增加方法,完善响应头中的信息,并应用到fiber框架中。
最后,我们可以发现基于令牌桶延迟计算实现频率限制的好处:
- 不需要定时器
- 不需要后台
goroutine
- 不需要队列
- 支持突发流量
仓库资源:
以上。